Skip to content

Commit 29bb62e

Browse files
committed
Handle inherited public APIs in CLDC11 checker
1 parent c61e7ba commit 29bb62e

File tree

1 file changed

+176
-39
lines changed

1 file changed

+176
-39
lines changed

.github/scripts/cldc11_api_compat_check.py

Lines changed: 176 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import argparse
1212
import os
13+
import re
1314
import subprocess
1415
import sys
1516
from 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+
5668
def 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

138230
def 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+
144260
def 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

191309
def 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

Comments
 (0)