1010
1111import argparse
1212import os
13+ import re
1314import subprocess
1415import sys
1516from dataclasses import dataclass , field
@@ -53,6 +54,17 @@ class JavapError(RuntimeError):
5354 pass
5455
5556
57+ @dataclass
58+ class ClassInfo :
59+ """Metadata about a compiled class."""
60+
61+ name : str
62+ api : ApiSurface
63+ supers : List [str ]
64+ is_public : bool
65+ kind : str
66+
67+
5668def discover_classes (root : str ) -> List [str ]:
5769 classes : List [str ] = []
5870 for base , _ , files in os .walk (root ):
@@ -124,15 +136,95 @@ def parse_members(javap_output: str) -> ApiSurface:
124136 return api
125137
126138
127- def collect_class_api_from_file (class_name : str , classes_root : str , javap_cmd : str ) -> ApiSurface :
139+ def parse_class_info (javap_output : str ) -> ClassInfo :
140+ api = ApiSurface ()
141+ supers : List [str ] = []
142+ pending : Optional [Tuple [str , bool , str ]] = None
143+ class_name : Optional [str ] = None
144+ is_public = False
145+ kind = "class"
146+
147+ header_pattern = re .compile (
148+ r"(?P<visibility>public|protected)?\s*(?P<kind>class|interface|enum)\s+" ,
149+ r"(?P<name>[\w.$]+)" ,
150+ r"(?:\s+extends\s+(?P<extends>[^\{]+?))?" ,
151+ r"(?:\s+implements\s+(?P<implements>[^\{]+))?" ,
152+ )
153+
154+ for raw_line in javap_output .splitlines ():
155+ line = raw_line .strip ()
156+ if not line or line .startswith ("Compiled from" ):
157+ continue
158+
159+ if class_name is None :
160+ match = header_pattern .search (line .rstrip ("{" ))
161+ if match :
162+ class_name = match .group ("name" )
163+ kind = match .group ("kind" )
164+ is_public = match .group ("visibility" ) == "public"
165+ extends_clause = match .group ("extends" )
166+ implements_clause = match .group ("implements" )
167+ for clause in (extends_clause , implements_clause ):
168+ if not clause :
169+ continue
170+ supers .extend ([part .strip () for part in clause .split (',' ) if part .strip ()])
171+ continue
172+
173+ if line .endswith ("{" ):
174+ continue
175+ if line .startswith ("descriptor:" ):
176+ if pending is None :
177+ continue
178+ descriptor = line .split (":" , 1 )[1 ].strip ()
179+ name , is_static , kind = pending
180+ api .add ((name , descriptor , is_static , kind ))
181+ pending = None
182+ continue
183+
184+ if line .startswith ("Runtime" ) or line .startswith ("Signature:" ) or line .startswith ("Exceptions:" ):
185+ pending = None
186+ continue
187+
188+ if "(" in line or line .endswith (";" ):
189+ if line .startswith ("//" ):
190+ continue
191+ if line .endswith (" class" ):
192+ continue
193+ if line .endswith ("interface" ):
194+ continue
195+
196+ is_static_member = " static " in f" { line } "
197+ if "(" in line :
198+ name_section = line .split ("(" )[0 ].strip ()
199+ name = name_section .split ()[- 1 ]
200+ kind = "method"
201+ else :
202+ name = line .rstrip (";" ).split ()[- 1 ]
203+ kind = "field"
204+ pending = (name , is_static_member , kind )
205+
206+ if class_name is None :
207+ raise ValueError ("Unable to determine class name from javap output" )
208+
209+ if not supers and kind == "class" and class_name != "java.lang.Object" :
210+ supers .append ("java.lang.Object" )
211+
212+ return ClassInfo (name = class_name , api = api , supers = supers , is_public = is_public , kind = kind )
213+
214+ def collect_class_info_from_file (class_name : str , classes_root : str , javap_cmd : str ) -> Optional [ClassInfo ]:
128215 class_path = os .path .join (classes_root , * class_name .split ("." )) + ".class"
216+ if not os .path .exists (class_path ):
217+ return None
129218 output = run_javap (class_path , javap_cmd )
130- return parse_members (output )
219+ return parse_class_info (output )
131220
132221
133- def collect_class_api_from_jdk (class_name : str , javap_cmd : str ) -> ApiSurface :
134- output = run_javap (class_name , javap_cmd )
135- return parse_members (output )
222+ def collect_class_info_from_jdk (class_name : str , javap_cmd : str ) -> Optional [ClassInfo ]:
223+ try :
224+ output = run_javap (class_name , javap_cmd )
225+ except JavapError :
226+ return None
227+ return parse_class_info (output )
136228
137229
138230def format_member (member : Member ) -> str :
@@ -141,28 +233,52 @@ def format_member(member: Member) -> str:
141233 return f"{ kind } : { static_prefix } { name } { descriptor } "
142234
143235
236+ def build_full_api (
237+ class_name : str ,
238+ lookup ,
239+ cache : Dict [str , Optional [ApiSurface ]],
240+ ) -> Optional [ApiSurface ]:
241+ if class_name in cache :
242+ return cache [class_name ]
243+
244+ info = lookup (class_name )
245+ if info is None :
246+ cache [class_name ] = None
247+ return None
248+
249+ merged = ApiSurface (set (info .api .methods ), set (info .api .fields ))
250+ for parent in info .supers :
251+ parent_api = build_full_api (parent , lookup , cache )
252+ if parent_api :
253+ merged .methods |= parent_api .methods
254+ merged .fields |= parent_api .fields
255+
256+ cache [class_name ] = merged
257+ return merged
258+
259+
144260def ensure_subset (
145261 source_classes : List [str ],
146- source_root : str ,
262+ source_lookup ,
147263 target_lookup ,
148264 target_label : str ,
149- javap_cmd : str ,
150265) -> Tuple [bool , List [str ]]:
151266 ok = True
152267 messages : List [str ] = []
268+ source_cache : Dict [str , Optional [ApiSurface ]] = {}
269+ target_cache : Dict [str , Optional [ApiSurface ]] = {}
153270
154271 for index , class_name in enumerate (sorted (source_classes ), start = 1 ):
155- try :
156- source_api = collect_class_api_from_file (class_name , source_root , javap_cmd )
157- except JavapError as exc :
272+ source_api = build_full_api (class_name , source_lookup , source_cache )
273+ if source_api is None :
158274 ok = False
159- messages .append (f"Failed to read { class_name } from { source_root } : { exc } " )
275+ messages .append (f"Failed to read { class_name } from source classes " )
160276 continue
161277
162278 if index % 25 == 0 :
163279 log (f" Processed { index } /{ len (source_classes )} classes for { target_label } subset check..." )
164280
165- target_api = target_lookup (class_name )
281+ target_api = build_full_api (class_name , target_lookup , target_cache )
166282 if target_api is None :
167283 ok = False
168284 messages .append (f"Missing class in { target_label } : { class_name } " )
@@ -178,37 +294,44 @@ def ensure_subset(
178294 return ok , messages
179295
180296
181- def collect_javaapi_map (javaapi_root : str , javap_cmd : str ) -> Dict [str , ApiSurface ]:
297+ def collect_javaapi_map (javaapi_root : str , javap_cmd : str ) -> Dict [str , ClassInfo ]:
182298 classes = discover_classes (javaapi_root )
183- api_map : Dict [str , ApiSurface ] = {}
299+ api_map : Dict [str , ClassInfo ] = {}
184300 for index , class_name in enumerate (classes , start = 1 ):
185- api_map [class_name ] = collect_class_api_from_file (class_name , javaapi_root , javap_cmd )
301+ info = collect_class_info_from_file (class_name , javaapi_root , javap_cmd )
302+ if info :
303+ api_map [class_name ] = info
186304 if index % 25 == 0 :
187305 log (f" Indexed { index } /{ len (classes )} vm/JavaAPI classes..." )
188306 return api_map
189307
190308
191309def write_extra_report (
192- cldc_classes : Dict [str , ApiSurface ],
193- javaapi_classes : Dict [str , ApiSurface ],
310+ cldc_classes : Dict [str , ClassInfo ],
311+ javaapi_classes : Dict [str , ClassInfo ],
312+ public_cldc : Set [str ],
194313 report_path : str ,
195314) -> None :
196315 lines : List [str ] = [
197316 "Extra APIs present in vm/JavaAPI but not in CLDC11" ,
198317 "" ,
199318 ]
200319
201- extra_classes = sorted (set (javaapi_classes ) - set (cldc_classes ))
320+ extra_classes = sorted (
321+ name
322+ for name , info in javaapi_classes .items ()
323+ if info .is_public and name not in public_cldc
324+ )
202325 if extra_classes :
203326 lines .append ("Classes only in vm/JavaAPI:" )
204327 lines .extend ([f" - { name } " for name in extra_classes ])
205328 lines .append ("" )
206329
207- shared_classes = set ( javaapi_classes ) & set ( cldc_classes )
330+ shared_classes = { name for name in javaapi_classes if name in public_cldc and javaapi_classes [ name ]. is_public }
208331 extra_members : List [str ] = []
209332 for class_name in sorted (shared_classes ):
210- javaapi_api = javaapi_classes [class_name ]
211- cldc_api = cldc_classes [class_name ]
333+ javaapi_api = javaapi_classes [class_name ]. api
334+ cldc_api = cldc_classes [class_name ]. api
212335 extra_methods , extra_fields = javaapi_api .extras_over (cldc_api )
213336 if not extra_methods and not extra_fields :
214337 continue
@@ -249,45 +372,59 @@ def main(argv: Optional[Iterable[str]] = None) -> int:
249372
250373 javap_cmd = args .javap or "javap"
251374
252- cldc_classes = discover_classes (args .cldc_classes )
253- if not cldc_classes :
375+ cldc_class_names = discover_classes (args .cldc_classes )
376+ if not cldc_class_names :
254377 print (f"No class files found under { args .cldc_classes } " , file = sys .stderr )
255378 return 1
256379
257- log (f"Discovered { len (cldc_classes )} CLDC11 classes; building API maps..." )
380+ log (f"Discovered { len (cldc_class_names )} CLDC11 classes; building API maps..." )
258381
259382 javaapi_map = collect_javaapi_map (args .javaapi_classes , javap_cmd )
260383 log (f"Collected API surface for { len (javaapi_map )} vm/JavaAPI classes" )
261384
262- def jdk_lookup (name : str ) -> Optional [ApiSurface ]:
263- try :
264- return collect_class_api_from_jdk (name , javap_cmd )
265- except JavapError :
266- return None
385+ cldc_lookup_cache : Dict [str , Optional [ClassInfo ]] = {}
386+ java_lookup_cache : Dict [str , Optional [ClassInfo ]] = {name : info for name , info in javaapi_map .items ()}
387+ jdk_lookup_cache : Dict [str , Optional [ClassInfo ]] = {}
388+
389+ def cldc_lookup (name : str ) -> Optional [ClassInfo ]:
390+ if name not in cldc_lookup_cache :
391+ cldc_lookup_cache [name ] = collect_class_info_from_file (name , args .cldc_classes , javap_cmd )
392+ return cldc_lookup_cache [name ]
393+
394+ def jdk_lookup (name : str ) -> Optional [ClassInfo ]:
395+ if name not in jdk_lookup_cache :
396+ jdk_lookup_cache [name ] = collect_class_info_from_jdk (name , javap_cmd )
397+ return jdk_lookup_cache [name ]
398+
399+ def javaapi_lookup (name : str ) -> Optional [ClassInfo ]:
400+ return java_lookup_cache .get (name )
267401
268- def javaapi_lookup (name : str ) -> Optional [ApiSurface ]:
269- return javaapi_map .get (name )
402+ public_cldc_classes = [
403+ name for name in cldc_class_names if (cldc_lookup (name ) and cldc_lookup_cache [name ].is_public )
404+ ]
405+
406+ if not public_cldc_classes :
407+ print ("No public classes discovered in CLDC11 output" , file = sys .stderr )
408+ return 1
270409
271410 log ("Validating CLDC11 API against Java SE 11..." )
272411 java_ok , java_messages = ensure_subset (
273- cldc_classes ,
274- args . cldc_classes ,
412+ public_cldc_classes ,
413+ cldc_lookup ,
275414 jdk_lookup ,
276415 "Java SE 11" ,
277- javap_cmd ,
278416 )
279417
280418 log ("Validating CLDC11 API against vm/JavaAPI..." )
281419 api_ok , api_messages = ensure_subset (
282- cldc_classes ,
283- args . cldc_classes ,
420+ public_cldc_classes ,
421+ cldc_lookup ,
284422 javaapi_lookup ,
285423 "vm/JavaAPI" ,
286- javap_cmd ,
287424 )
288425
289- cldc_map = {name : collect_class_api_from_file ( name , args . cldc_classes , javap_cmd ) for name in cldc_classes }
290- write_extra_report (cldc_map , javaapi_map , args .extra_report )
426+ cldc_map = {name : info for name , info in cldc_lookup_cache . items () if info is not None }
427+ write_extra_report (cldc_map , javaapi_map , set ( public_cldc_classes ), args .extra_report )
291428 log (f"Wrote extra API report to { args .extra_report } " )
292429
293430 messages = java_messages + api_messages
0 commit comments