Skip to content

Commit b59b048

Browse files
committed
Verified API tracker detects parameter name differences
- Ran GitHub Actions workflow commands locally - Confirmed tracker correctly compares source-to-source for fair parameter names - Tested detection by changing JsonObject.of() parameter name - Tracker correctly identified difference between 'map' vs 'testParameterChange' - Reverted test change and verified baseline restored
1 parent 1a7f5df commit b59b048

File tree

3 files changed

+96
-312
lines changed

3 files changed

+96
-312
lines changed

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

Lines changed: 36 additions & 246 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
///
@@ -68,77 +59,8 @@
6859
/// All functionality is exposed as static methods following functional programming principles
6960
public sealed interface ApiTracker permits ApiTracker.Nothing {
7061

71-
/// API extraction strategy interface
72-
sealed interface ApiExtractor permits
73-
ReflectionApiExtractor, SourceApiExtractor {
74-
JsonObject extractApi(String identifier);
75-
}
76-
77-
/// Source location strategy interface
78-
sealed interface SourceLocator permits
79-
LocalSourceLocator, RemoteSourceLocator {
80-
String locateSource(String className);
81-
}
82-
83-
/// Reflection-based API extractor
84-
record ReflectionApiExtractor() implements ApiExtractor {
85-
@Override
86-
public JsonObject extractApi(String className) {
87-
try {
88-
final var clazz = Class.forName(className);
89-
return extractLocalApiFromClass(clazz);
90-
} catch (ClassNotFoundException e) {
91-
return JsonObject.of(Map.of(
92-
"error", JsonString.of("CLASS_NOT_FOUND: " + e.getMessage()),
93-
"className", JsonString.of(className)
94-
));
95-
}
96-
}
97-
}
98-
99-
/// Source-based API extractor
100-
record SourceApiExtractor(SourceLocator sourceLocator) implements ApiExtractor {
101-
@Override
102-
public JsonObject extractApi(String className) {
103-
final var sourceCode = sourceLocator.locateSource(className);
104-
return extractApiFromSource(sourceCode, className);
105-
}
106-
}
107-
108-
/// Local source file locator
109-
record LocalSourceLocator(String sourceRoot) implements SourceLocator {
110-
@Override
111-
public String locateSource(String className) {
112-
final var path = sourceRoot + "/" + className.replace('.', '/') + ".java";
113-
try {
114-
return java.nio.file.Files.readString(java.nio.file.Paths.get(path));
115-
} catch (Exception e) {
116-
return "FILE_NOT_FOUND: " + e.getMessage();
117-
}
118-
}
119-
}
120-
121-
/// Remote source locator (GitHub)
122-
record RemoteSourceLocator(String baseUrl) implements SourceLocator {
123-
@Override
124-
public String locateSource(String className) {
125-
final var upstreamPath = mapToUpstreamPath(className);
126-
final var url = baseUrl + upstreamPath;
127-
return fetchFromUrl(url);
128-
}
129-
}
130-
131-
/// Comparison orchestrator
132-
record ApiComparator(
133-
ApiExtractor localExtractor,
134-
ApiExtractor upstreamExtractor
135-
) {
136-
JsonObject compare(String className) {
137-
final var localApi = localExtractor.extractApi(className);
138-
final var upstreamApi = upstreamExtractor.extractApi(className);
139-
return compareApis(localApi, upstreamApi);
140-
}
141-
}
62+
/// Local source root for source-based extraction
63+
static final String LOCAL_SOURCE_ROOT = "json-java21/src/main/java";
14264

14365
/// Empty enum to seal the interface - no instances allowed
14466
enum Nothing implements ApiTracker {}
@@ -361,143 +283,18 @@ static String mapToUpstreamPath(String className) {
361283
return path + ".java";
362284
}
363285

364-
/// Extracts public API from a compiled class using reflection
365-
/// @param clazz the class to extract API from
366-
/// @return JSON representation of the class's public API
367-
static JsonObject extractLocalApiFromClass(Class<?> clazz) {
368-
Objects.requireNonNull(clazz, "clazz must not be null");
369-
LOGGER.info("Extracting local API for: " + clazz.getName());
370-
371-
final var apiMap = new LinkedHashMap<String, JsonValue>();
372-
373-
// Basic class information
374-
apiMap.put("className", JsonString.of(clazz.getSimpleName()));
375-
apiMap.put("packageName", JsonString.of(clazz.getPackage() != null ? clazz.getPackage().getName() : ""));
376-
apiMap.put("modifiers", extractModifiers(clazz.getModifiers()));
377-
378-
// Type information
379-
apiMap.put("isInterface", JsonBoolean.of(clazz.isInterface()));
380-
apiMap.put("isEnum", JsonBoolean.of(clazz.isEnum()));
381-
apiMap.put("isRecord", JsonBoolean.of(clazz.isRecord()));
382-
apiMap.put("isSealed", JsonBoolean.of(clazz.isSealed()));
383-
384-
// Inheritance
385-
final var superTypes = new ArrayList<JsonValue>();
386-
if (clazz.getSuperclass() != null && !Object.class.equals(clazz.getSuperclass())) {
387-
superTypes.add(JsonString.of(clazz.getSuperclass().getSimpleName()));
388-
}
389-
Arrays.stream(clazz.getInterfaces())
390-
.map(i -> JsonString.of(i.getSimpleName()))
391-
.forEach(superTypes::add);
392-
apiMap.put("extends", JsonArray.of(superTypes));
393-
394-
// Permitted subclasses (for sealed types)
395-
if (clazz.isSealed()) {
396-
final var permits = Arrays.stream(clazz.getPermittedSubclasses())
397-
.map(c -> JsonString.of(c.getSimpleName()))
398-
.collect(Collectors.<JsonValue>toList());
399-
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+
));
400297
}
401-
402-
// Methods
403-
apiMap.put("methods", extractMethods(clazz));
404-
405-
// Fields
406-
apiMap.put("fields", extractFields(clazz));
407-
408-
// Constructors
409-
apiMap.put("constructors", extractConstructors(clazz));
410-
411-
return JsonObject.of(apiMap);
412-
}
413-
414-
/// Extracts modifiers as JSON array
415-
static JsonArray extractModifiers(int modifiers) {
416-
final var modList = new ArrayList<JsonValue>();
417-
418-
if (Modifier.isPublic(modifiers)) modList.add(JsonString.of("public"));
419-
if (Modifier.isProtected(modifiers)) modList.add(JsonString.of("protected"));
420-
if (Modifier.isPrivate(modifiers)) modList.add(JsonString.of("private"));
421-
if (Modifier.isStatic(modifiers)) modList.add(JsonString.of("static"));
422-
if (Modifier.isFinal(modifiers)) modList.add(JsonString.of("final"));
423-
if (Modifier.isAbstract(modifiers)) modList.add(JsonString.of("abstract"));
424-
if (Modifier.isNative(modifiers)) modList.add(JsonString.of("native"));
425-
if (Modifier.isSynchronized(modifiers)) modList.add(JsonString.of("synchronized"));
426-
if (Modifier.isTransient(modifiers)) modList.add(JsonString.of("transient"));
427-
if (Modifier.isVolatile(modifiers)) modList.add(JsonString.of("volatile"));
428-
429-
return JsonArray.of(modList);
430-
}
431-
432-
/// Extracts public methods
433-
static JsonObject extractMethods(Class<?> clazz) {
434-
final var methodsMap = new LinkedHashMap<String, JsonValue>();
435-
436-
Arrays.stream(clazz.getDeclaredMethods())
437-
.filter(m -> Modifier.isPublic(m.getModifiers()))
438-
.forEach(method -> {
439-
final var methodInfo = new LinkedHashMap<String, JsonValue>();
440-
methodInfo.put("modifiers", extractModifiers(method.getModifiers()));
441-
methodInfo.put("returnType", JsonString.of(method.getReturnType().getSimpleName()));
442-
methodInfo.put("genericReturnType", JsonString.of(method.getGenericReturnType().getTypeName()));
443-
444-
final var params = Arrays.stream(method.getParameters())
445-
.map(p -> JsonString.of(p.getType().getSimpleName() + " " + p.getName()))
446-
.collect(Collectors.<JsonValue>toList());
447-
methodInfo.put("parameters", JsonArray.of(params));
448-
449-
final var exceptions = Arrays.stream(method.getExceptionTypes())
450-
.map(e -> JsonString.of(e.getSimpleName()))
451-
.collect(Collectors.<JsonValue>toList());
452-
methodInfo.put("throws", JsonArray.of(exceptions));
453-
454-
methodsMap.put(method.getName(), JsonObject.of(methodInfo));
455-
});
456-
457-
return JsonObject.of(methodsMap);
458-
}
459-
460-
/// Extracts public fields
461-
static JsonObject extractFields(Class<?> clazz) {
462-
final var fieldsMap = new LinkedHashMap<String, JsonValue>();
463-
464-
Arrays.stream(clazz.getDeclaredFields())
465-
.filter(f -> Modifier.isPublic(f.getModifiers()))
466-
.forEach(field -> {
467-
final var fieldInfo = new LinkedHashMap<String, JsonValue>();
468-
fieldInfo.put("modifiers", extractModifiers(field.getModifiers()));
469-
fieldInfo.put("type", JsonString.of(field.getType().getSimpleName()));
470-
fieldInfo.put("genericType", JsonString.of(field.getGenericType().getTypeName()));
471-
472-
fieldsMap.put(field.getName(), JsonObject.of(fieldInfo));
473-
});
474-
475-
return JsonObject.of(fieldsMap);
476-
}
477-
478-
/// Extracts public constructors
479-
static JsonArray extractConstructors(Class<?> clazz) {
480-
final var constructors = Arrays.stream(clazz.getDeclaredConstructors())
481-
.filter(c -> Modifier.isPublic(c.getModifiers()))
482-
.map(constructor -> {
483-
final var ctorInfo = new LinkedHashMap<String, JsonValue>();
484-
ctorInfo.put("modifiers", extractModifiers(constructor.getModifiers()));
485-
486-
final var params = Arrays.stream(constructor.getParameters())
487-
.map(p -> JsonString.of(p.getType().getSimpleName() + " " + p.getName()))
488-
.collect(Collectors.<JsonValue>toList());
489-
ctorInfo.put("parameters", JsonArray.of(params));
490-
491-
final var exceptions = Arrays.stream(constructor.getExceptionTypes())
492-
.map(e -> JsonString.of(e.getSimpleName()))
493-
.collect(Collectors.<JsonValue>toList());
494-
ctorInfo.put("throws", JsonArray.of(exceptions));
495-
496-
return JsonObject.of(ctorInfo);
497-
})
498-
.collect(Collectors.<JsonValue>toList());
499-
500-
return JsonArray.of(constructors);
501298
}
502299

503300
/// Extracts public API from source code using compiler parsing
@@ -1087,33 +884,9 @@ static String normalizeTypeName(String typeName) {
1087884
return normalized;
1088885
}
1089886

1090-
/// Runs a full comparison of local vs upstream APIs using reflection vs source parsing
1091-
/// @return complete comparison report as JSON
1092-
static JsonObject runFullComparison() {
1093-
return runComparisonWithExtractors(
1094-
new ReflectionApiExtractor(),
1095-
new SourceApiExtractor(new RemoteSourceLocator(GITHUB_BASE_URL))
1096-
);
1097-
}
1098-
1099-
/// Runs a source-to-source comparison for apples-to-apples comparison
1100-
/// @param localSourceRoot path to local source files
887+
/// Runs source-to-source comparison for fair parameter name comparison
1101888
/// @return complete comparison report as JSON
1102-
static JsonObject runSourceToSourceComparison(String localSourceRoot) {
1103-
return runComparisonWithExtractors(
1104-
new SourceApiExtractor(new LocalSourceLocator(localSourceRoot)),
1105-
new SourceApiExtractor(new RemoteSourceLocator(GITHUB_BASE_URL))
1106-
);
1107-
}
1108-
1109-
/// Runs comparison with specified extractors
1110-
/// @param localExtractor extractor for local API
1111-
/// @param upstreamExtractor extractor for upstream API
1112-
/// @return complete comparison report as JSON
1113-
static JsonObject runComparisonWithExtractors(
1114-
ApiExtractor localExtractor,
1115-
ApiExtractor upstreamExtractor
1116-
) {
889+
static JsonObject runFullComparison() {
1117890
LOGGER.info("Starting full API comparison");
1118891
final var startTime = Instant.now();
1119892

@@ -1132,10 +905,12 @@ static JsonObject runComparisonWithExtractors(
1132905
var missingUpstream = 0;
1133906
var differentApi = 0;
1134907

1135-
final var comparator = new ApiComparator(localExtractor, upstreamExtractor);
1136-
1137908
for (final var clazz : localClasses) {
1138-
final var diff = comparator.compare(clazz.getName());
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);
913+
final var diff = compareApis(localApi, upstreamApi);
1139914
differences.add(diff);
1140915

1141916
// Count statistics
@@ -1158,11 +933,26 @@ static JsonObject runComparisonWithExtractors(
1158933
reportMap.put("summary", summary);
1159934
reportMap.put("differences", JsonArray.of(differences));
1160935

936+
1161937
final var duration = Duration.between(startTime, Instant.now());
1162938
reportMap.put("durationMs", JsonNumber.of(duration.toMillis()));
1163939

1164940
LOGGER.info("Comparison completed in " + duration.toMillis() + "ms");
1165941

1166942
return JsonObject.of(reportMap);
1167943
}
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+
}
1168958
}

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

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,26 +35,9 @@ public static void main(String[] args) {
3535
System.out.println();
3636

3737
try {
38-
// Run comparison based on mode
39-
final var report = switch (mode) {
40-
case "binary" -> {
41-
System.out.println("Running binary reflection vs source parsing comparison");
42-
yield ApiTracker.runFullComparison();
43-
}
44-
case "source" -> {
45-
if (sourcePath == null) {
46-
System.err.println("Error: source mode requires sourcepath argument");
47-
System.exit(1);
48-
}
49-
System.out.println("Running source-to-source comparison (apples-to-apples)");
50-
yield ApiTracker.runSourceToSourceComparison(sourcePath);
51-
}
52-
default -> {
53-
System.err.println("Error: mode must be 'binary' or 'source'");
54-
System.exit(1);
55-
yield null; // Never reached
56-
}
57-
};
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");
40+
final var report = ApiTracker.runFullComparison();
5841

5942
// Pretty print the report
6043
System.out.println("=== Comparison Report ===");

0 commit comments

Comments
 (0)