Skip to content

Commit 2867e1e

Browse files
author
Simon Massey
authored
move to source parsing for local api comparison (#12)
1 parent c399c90 commit 2867e1e

File tree

3 files changed

+148
-207
lines changed

3 files changed

+148
-207
lines changed

json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java

Lines changed: 73 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,6 @@
88
import jdk.sandbox.java.util.json.JsonBoolean;
99

1010
import java.io.IOException;
11-
import java.lang.reflect.Constructor;
12-
import java.lang.reflect.Field;
13-
import java.lang.reflect.Method;
14-
import java.lang.reflect.Modifier;
15-
import java.lang.reflect.ParameterizedType;
16-
import java.lang.reflect.Type;
1711
import java.net.URI;
1812
import java.net.http.HttpClient;
1913
import java.net.http.HttpRequest;
@@ -22,7 +16,7 @@
2216
import java.time.Duration;
2317
import java.time.Instant;
2418
import java.util.ArrayList;
25-
import java.util.Arrays;
19+
import java.util.ArrayList;
2620
import java.util.Collections;
2721
import java.util.LinkedHashMap;
2822
import java.util.List;
@@ -34,7 +28,6 @@
3428
import java.util.logging.Level;
3529
import java.util.logging.Logger;
3630
import java.util.stream.Collectors;
37-
import java.util.stream.Stream;
3831

3932
import javax.tools.DiagnosticCollector;
4033
import javax.tools.JavaCompiler;
@@ -44,14 +37,12 @@
4437
import javax.tools.ToolProvider;
4538

4639
import com.sun.source.tree.ClassTree;
47-
import com.sun.source.tree.CompilationUnitTree;
4840
import com.sun.source.tree.MethodTree;
4941
import com.sun.source.tree.ModifiersTree;
5042
import com.sun.source.tree.Tree;
5143
import com.sun.source.tree.VariableTree;
5244
import com.sun.source.util.JavacTask;
5345
import com.sun.source.util.TreePathScanner;
54-
import com.sun.source.util.Trees;
5546

5647
/// API Tracker module for comparing local and upstream JSON APIs
5748
///
@@ -61,9 +52,16 @@
6152
/// - Compare public APIs using compiler parsing
6253
/// - Generate structured diff reports
6354
///
55+
/// Modular design supports different extraction strategies:
56+
/// - Binary reflection for quick class introspection
57+
/// - Source parsing for accurate parameter names and signatures
58+
///
6459
/// All functionality is exposed as static methods following functional programming principles
6560
public sealed interface ApiTracker permits ApiTracker.Nothing {
6661

62+
/// Local source root for source-based extraction
63+
static final String LOCAL_SOURCE_ROOT = "json-java21/src/main/java";
64+
6765
/// Empty enum to seal the interface - no instances allowed
6866
enum Nothing implements ApiTracker {}
6967

@@ -76,6 +74,33 @@ enum Nothing implements ApiTracker {}
7674
// GitHub base URL for upstream sources
7775
static final String GITHUB_BASE_URL = "https://raw.githubusercontent.com/openjdk/jdk-sandbox/refs/heads/json/src/java.base/share/classes/";
7876

77+
/// Fetches content from a URL
78+
static String fetchFromUrl(String url) {
79+
final var httpClient = HttpClient.newBuilder()
80+
.connectTimeout(Duration.ofSeconds(10))
81+
.build();
82+
83+
try {
84+
final var request = HttpRequest.newBuilder()
85+
.uri(URI.create(url))
86+
.timeout(Duration.ofSeconds(30))
87+
.GET()
88+
.build();
89+
90+
final var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
91+
92+
if (response.statusCode() == 200) {
93+
return response.body();
94+
} else if (response.statusCode() == 404) {
95+
return "NOT_FOUND: Upstream file not found (possibly deleted or renamed)";
96+
} else {
97+
return "HTTP_ERROR: Status " + response.statusCode();
98+
}
99+
} catch (Exception e) {
100+
return "FETCH_ERROR: " + e.getMessage();
101+
}
102+
}
103+
79104
/// Discovers all classes in the local JSON API packages
80105
/// @return sorted set of classes from jdk.sandbox.java.util.json and jdk.sandbox.internal.util.json
81106
static Set<Class<?>> discoverLocalJsonClasses() {
@@ -258,150 +283,25 @@ static String mapToUpstreamPath(String className) {
258283
return path + ".java";
259284
}
260285

261-
/// Extracts public API from a compiled class using reflection
262-
/// @param clazz the class to extract API from
263-
/// @return JSON representation of the class's public API
264-
static JsonObject extractLocalApi(Class<?> clazz) {
265-
Objects.requireNonNull(clazz, "clazz must not be null");
266-
LOGGER.info("Extracting local API for: " + clazz.getName());
267-
268-
final var apiMap = new LinkedHashMap<String, JsonValue>();
269-
270-
// Basic class information
271-
apiMap.put("className", JsonString.of(clazz.getSimpleName()));
272-
apiMap.put("packageName", JsonString.of(clazz.getPackage() != null ? clazz.getPackage().getName() : ""));
273-
apiMap.put("modifiers", extractModifiers(clazz.getModifiers()));
274-
275-
// Type information
276-
apiMap.put("isInterface", JsonBoolean.of(clazz.isInterface()));
277-
apiMap.put("isEnum", JsonBoolean.of(clazz.isEnum()));
278-
apiMap.put("isRecord", JsonBoolean.of(clazz.isRecord()));
279-
apiMap.put("isSealed", JsonBoolean.of(clazz.isSealed()));
280-
281-
// Inheritance
282-
final var superTypes = new ArrayList<JsonValue>();
283-
if (clazz.getSuperclass() != null && !Object.class.equals(clazz.getSuperclass())) {
284-
superTypes.add(JsonString.of(clazz.getSuperclass().getSimpleName()));
285-
}
286-
Arrays.stream(clazz.getInterfaces())
287-
.map(i -> JsonString.of(i.getSimpleName()))
288-
.forEach(superTypes::add);
289-
apiMap.put("extends", JsonArray.of(superTypes));
290-
291-
// Permitted subclasses (for sealed types)
292-
if (clazz.isSealed()) {
293-
final var permits = Arrays.stream(clazz.getPermittedSubclasses())
294-
.map(c -> JsonString.of(c.getSimpleName()))
295-
.collect(Collectors.<JsonValue>toList());
296-
apiMap.put("permits", JsonArray.of(permits));
286+
/// Extracts local API from source file
287+
static JsonObject extractLocalApiFromSource(String className) {
288+
final var path = LOCAL_SOURCE_ROOT + "/" + className.replace('.', '/') + ".java";
289+
try {
290+
final var sourceCode = java.nio.file.Files.readString(java.nio.file.Paths.get(path));
291+
return extractApiFromSource(sourceCode, className);
292+
} catch (Exception e) {
293+
return JsonObject.of(Map.of(
294+
"error", JsonString.of("LOCAL_FILE_NOT_FOUND: " + e.getMessage()),
295+
"className", JsonString.of(className)
296+
));
297297
}
298-
299-
// Methods
300-
apiMap.put("methods", extractMethods(clazz));
301-
302-
// Fields
303-
apiMap.put("fields", extractFields(clazz));
304-
305-
// Constructors
306-
apiMap.put("constructors", extractConstructors(clazz));
307-
308-
return JsonObject.of(apiMap);
309-
}
310-
311-
/// Extracts modifiers as JSON array
312-
static JsonArray extractModifiers(int modifiers) {
313-
final var modList = new ArrayList<JsonValue>();
314-
315-
if (Modifier.isPublic(modifiers)) modList.add(JsonString.of("public"));
316-
if (Modifier.isProtected(modifiers)) modList.add(JsonString.of("protected"));
317-
if (Modifier.isPrivate(modifiers)) modList.add(JsonString.of("private"));
318-
if (Modifier.isStatic(modifiers)) modList.add(JsonString.of("static"));
319-
if (Modifier.isFinal(modifiers)) modList.add(JsonString.of("final"));
320-
if (Modifier.isAbstract(modifiers)) modList.add(JsonString.of("abstract"));
321-
if (Modifier.isNative(modifiers)) modList.add(JsonString.of("native"));
322-
if (Modifier.isSynchronized(modifiers)) modList.add(JsonString.of("synchronized"));
323-
if (Modifier.isTransient(modifiers)) modList.add(JsonString.of("transient"));
324-
if (Modifier.isVolatile(modifiers)) modList.add(JsonString.of("volatile"));
325-
326-
return JsonArray.of(modList);
327-
}
328-
329-
/// Extracts public methods
330-
static JsonObject extractMethods(Class<?> clazz) {
331-
final var methodsMap = new LinkedHashMap<String, JsonValue>();
332-
333-
Arrays.stream(clazz.getDeclaredMethods())
334-
.filter(m -> Modifier.isPublic(m.getModifiers()))
335-
.forEach(method -> {
336-
final var methodInfo = new LinkedHashMap<String, JsonValue>();
337-
methodInfo.put("modifiers", extractModifiers(method.getModifiers()));
338-
methodInfo.put("returnType", JsonString.of(method.getReturnType().getSimpleName()));
339-
methodInfo.put("genericReturnType", JsonString.of(method.getGenericReturnType().getTypeName()));
340-
341-
final var params = Arrays.stream(method.getParameters())
342-
.map(p -> JsonString.of(p.getType().getSimpleName() + " " + p.getName()))
343-
.collect(Collectors.<JsonValue>toList());
344-
methodInfo.put("parameters", JsonArray.of(params));
345-
346-
final var exceptions = Arrays.stream(method.getExceptionTypes())
347-
.map(e -> JsonString.of(e.getSimpleName()))
348-
.collect(Collectors.<JsonValue>toList());
349-
methodInfo.put("throws", JsonArray.of(exceptions));
350-
351-
methodsMap.put(method.getName(), JsonObject.of(methodInfo));
352-
});
353-
354-
return JsonObject.of(methodsMap);
355-
}
356-
357-
/// Extracts public fields
358-
static JsonObject extractFields(Class<?> clazz) {
359-
final var fieldsMap = new LinkedHashMap<String, JsonValue>();
360-
361-
Arrays.stream(clazz.getDeclaredFields())
362-
.filter(f -> Modifier.isPublic(f.getModifiers()))
363-
.forEach(field -> {
364-
final var fieldInfo = new LinkedHashMap<String, JsonValue>();
365-
fieldInfo.put("modifiers", extractModifiers(field.getModifiers()));
366-
fieldInfo.put("type", JsonString.of(field.getType().getSimpleName()));
367-
fieldInfo.put("genericType", JsonString.of(field.getGenericType().getTypeName()));
368-
369-
fieldsMap.put(field.getName(), JsonObject.of(fieldInfo));
370-
});
371-
372-
return JsonObject.of(fieldsMap);
373298
}
374299

375-
/// Extracts public constructors
376-
static JsonArray extractConstructors(Class<?> clazz) {
377-
final var constructors = Arrays.stream(clazz.getDeclaredConstructors())
378-
.filter(c -> Modifier.isPublic(c.getModifiers()))
379-
.map(constructor -> {
380-
final var ctorInfo = new LinkedHashMap<String, JsonValue>();
381-
ctorInfo.put("modifiers", extractModifiers(constructor.getModifiers()));
382-
383-
final var params = Arrays.stream(constructor.getParameters())
384-
.map(p -> JsonString.of(p.getType().getSimpleName() + " " + p.getName()))
385-
.collect(Collectors.<JsonValue>toList());
386-
ctorInfo.put("parameters", JsonArray.of(params));
387-
388-
final var exceptions = Arrays.stream(constructor.getExceptionTypes())
389-
.map(e -> JsonString.of(e.getSimpleName()))
390-
.collect(Collectors.<JsonValue>toList());
391-
ctorInfo.put("throws", JsonArray.of(exceptions));
392-
393-
return JsonObject.of(ctorInfo);
394-
})
395-
.collect(Collectors.<JsonValue>toList());
396-
397-
return JsonArray.of(constructors);
398-
}
399-
400-
/// Extracts public API from upstream source code using compiler parsing
300+
/// Extracts public API from source code using compiler parsing
401301
/// @param sourceCode the source code to parse
402302
/// @param className the expected class name
403303
/// @return JSON representation of the parsed API
404-
static JsonObject extractUpstreamApi(String sourceCode, String className) {
304+
static JsonObject extractApiFromSource(String sourceCode, String className) {
405305
Objects.requireNonNull(sourceCode, "sourceCode must not be null");
406306
Objects.requireNonNull(className, "className must not be null");
407307

@@ -701,7 +601,11 @@ static JsonObject compareApis(JsonObject local, JsonObject upstream) {
701601
Objects.requireNonNull(upstream, "upstream must not be null");
702602

703603
final var diffMap = new LinkedHashMap<String, JsonValue>();
704-
final var className = ((JsonString) local.members().get("className")).value();
604+
605+
// Extract class name safely
606+
final var localClassName = local.members().get("className");
607+
final var className = localClassName instanceof JsonString js ?
608+
js.value() : "Unknown";
705609

706610
diffMap.put("className", JsonString.of(className));
707611

@@ -980,7 +884,7 @@ static String normalizeTypeName(String typeName) {
980884
return normalized;
981885
}
982886

983-
/// Runs a full comparison of local vs upstream APIs
887+
/// Runs source-to-source comparison for fair parameter name comparison
984888
/// @return complete comparison report as JSON
985889
static JsonObject runFullComparison() {
986890
LOGGER.info("Starting full API comparison");
@@ -995,20 +899,17 @@ static JsonObject runFullComparison() {
995899
final var localClasses = discoverLocalJsonClasses();
996900
LOGGER.info("Found " + localClasses.size() + " local classes");
997901

998-
// Fetch upstream sources
999-
final var upstreamSources = fetchUpstreamSources(localClasses);
1000-
1001902
// Extract and compare APIs
1002903
final var differences = new ArrayList<JsonValue>();
1003904
var matchingCount = 0;
1004905
var missingUpstream = 0;
1005906
var differentApi = 0;
1006907

1007908
for (final var clazz : localClasses) {
1008-
final var localApi = extractLocalApi(clazz);
1009-
final var upstreamSource = upstreamSources.get(clazz.getName());
1010-
final var upstreamApi = extractUpstreamApi(upstreamSource, clazz.getName());
1011-
909+
final var className = clazz.getName();
910+
final var localApi = extractLocalApiFromSource(className);
911+
final var upstreamSource = fetchUpstreamSource(className);
912+
final var upstreamApi = extractApiFromSource(upstreamSource, className);
1012913
final var diff = compareApis(localApi, upstreamApi);
1013914
differences.add(diff);
1014915

@@ -1032,11 +933,26 @@ static JsonObject runFullComparison() {
1032933
reportMap.put("summary", summary);
1033934
reportMap.put("differences", JsonArray.of(differences));
1034935

936+
1035937
final var duration = Duration.between(startTime, Instant.now());
1036938
reportMap.put("durationMs", JsonNumber.of(duration.toMillis()));
1037939

1038940
LOGGER.info("Comparison completed in " + duration.toMillis() + "ms");
1039941

1040942
return JsonObject.of(reportMap);
1041943
}
944+
945+
/// Fetches single upstream source file
946+
static String fetchUpstreamSource(String className) {
947+
final var cached = FETCH_CACHE.get(className);
948+
if (cached != null) {
949+
return cached;
950+
}
951+
952+
final var upstreamPath = mapToUpstreamPath(className);
953+
final var url = GITHUB_BASE_URL + upstreamPath;
954+
final var source = fetchFromUrl(url);
955+
FETCH_CACHE.put(className, source);
956+
return source;
957+
}
1042958
}

json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerRunner.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,36 @@
77

88
/// Command-line runner for the API Tracker
99
///
10-
/// Usage: java io.github.simbo1905.tracker.ApiTrackerRunner [loglevel]
11-
/// where loglevel is one of: SEVERE, WARNING, INFO, FINE, FINER, FINEST
10+
/// Usage: java io.github.simbo1905.tracker.ApiTrackerRunner [loglevel] [mode] [sourcepath]
11+
///
12+
/// Arguments:
13+
/// - loglevel: SEVERE, WARNING, INFO, FINE, FINER, FINEST (default: INFO)
14+
/// - mode: binary|source (default: binary)
15+
/// - binary: Compare binary reflection (local) vs source parsing (remote)
16+
/// - source: Compare source parsing (local) vs source parsing (remote) for accurate parameter names
17+
/// - sourcepath: Path to local source files (required for source mode)
1218
public class ApiTrackerRunner {
1319

1420
public static void main(String[] args) {
15-
// Configure logging based on command line argument
21+
// Parse command line arguments
1622
final var logLevel = args.length > 0 ? Level.parse(args[0].toUpperCase()) : Level.INFO;
23+
final var mode = args.length > 1 ? args[1].toLowerCase() : "binary";
24+
final var sourcePath = args.length > 2 ? args[2] : null;
25+
1726
configureLogging(logLevel);
1827

1928
System.out.println("=== JSON API Tracker ===");
2029
System.out.println("Comparing local jdk.sandbox.java.util.json with upstream java.util.json");
2130
System.out.println("Log level: " + logLevel);
31+
System.out.println("Mode: " + mode);
32+
if (sourcePath != null) {
33+
System.out.println("Local source path: " + sourcePath);
34+
}
2235
System.out.println();
2336

2437
try {
25-
// Run the full comparison
38+
// Run comparison - now only source-to-source for fair parameter comparison
39+
System.out.println("Running source-to-source comparison for fair parameter names");
2640
final var report = ApiTracker.runFullComparison();
2741

2842
// Pretty print the report

0 commit comments

Comments
 (0)