From 218e2da4c1b65fd912eac74621af79a744c7d92c Mon Sep 17 00:00:00 2001 From: Simon Massey Date: Fri, 25 Jul 2025 22:36:53 +0100 Subject: [PATCH 1/6] refactor: Convert to multi-module Maven project --- .gitignore | 1 + api-tracker/pom.xml | 25 +++ .../simbo1905/tracker/ApiTrackerMain.java | 35 ++++ java-util-json-java21/pom.xml | 41 +++++ .../main/java/jdk/sandbox/demo/JsonDemo.java | 0 .../internal/util/json/JsonArrayImpl.java | 0 .../internal/util/json/JsonBooleanImpl.java | 0 .../internal/util/json/JsonNullImpl.java | 0 .../internal/util/json/JsonNumberImpl.java | 0 .../internal/util/json/JsonObjectImpl.java | 0 .../internal/util/json/JsonParser.java | 0 .../internal/util/json/JsonStringImpl.java | 0 .../internal/util/json/StableValue.java | 0 .../jdk/sandbox/internal/util/json/Utils.java | 0 .../java/jdk/sandbox/java/util/json/Json.java | 0 .../jdk/sandbox/java/util/json/JsonArray.java | 0 .../sandbox/java/util/json/JsonBoolean.java | 0 .../jdk/sandbox/java/util/json/JsonNull.java | 0 .../sandbox/java/util/json/JsonNumber.java | 0 .../sandbox/java/util/json/JsonObject.java | 0 .../java/util/json/JsonParseException.java | 0 .../sandbox/java/util/json/JsonString.java | 0 .../jdk/sandbox/java/util/json/JsonValue.java | 0 .../sandbox/java/util/json/package-info.java | 0 .../internal/util/json/JsonParserTests.java | 0 .../util/json/JsonPatternMatchingTests.java | 0 .../util/json/JsonRecordMappingTests.java | 0 .../java/util/json/JsonTypedUntypedTests.java | 0 .../java/util/json/ReadmeDemoTests.java | 0 pom.xml | 152 +++++++++--------- 30 files changed, 181 insertions(+), 73 deletions(-) create mode 100644 api-tracker/pom.xml create mode 100644 api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerMain.java create mode 100644 java-util-json-java21/pom.xml rename {src => java-util-json-java21/src}/main/java/jdk/sandbox/demo/JsonDemo.java (100%) rename {src => java-util-json-java21/src}/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java (100%) rename {src => java-util-json-java21/src}/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java (100%) rename {src => java-util-json-java21/src}/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java (100%) rename {src => java-util-json-java21/src}/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java (100%) rename {src => java-util-json-java21/src}/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java (100%) rename {src => java-util-json-java21/src}/main/java/jdk/sandbox/internal/util/json/JsonParser.java (100%) rename {src => java-util-json-java21/src}/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java (100%) rename {src => java-util-json-java21/src}/main/java/jdk/sandbox/internal/util/json/StableValue.java (100%) rename {src => java-util-json-java21/src}/main/java/jdk/sandbox/internal/util/json/Utils.java (100%) rename {src => java-util-json-java21/src}/main/java/jdk/sandbox/java/util/json/Json.java (100%) rename {src => java-util-json-java21/src}/main/java/jdk/sandbox/java/util/json/JsonArray.java (100%) rename {src => java-util-json-java21/src}/main/java/jdk/sandbox/java/util/json/JsonBoolean.java (100%) rename {src => java-util-json-java21/src}/main/java/jdk/sandbox/java/util/json/JsonNull.java (100%) rename {src => java-util-json-java21/src}/main/java/jdk/sandbox/java/util/json/JsonNumber.java (100%) rename {src => java-util-json-java21/src}/main/java/jdk/sandbox/java/util/json/JsonObject.java (100%) rename {src => java-util-json-java21/src}/main/java/jdk/sandbox/java/util/json/JsonParseException.java (100%) rename {src => java-util-json-java21/src}/main/java/jdk/sandbox/java/util/json/JsonString.java (100%) rename {src => java-util-json-java21/src}/main/java/jdk/sandbox/java/util/json/JsonValue.java (100%) rename {src => java-util-json-java21/src}/main/java/jdk/sandbox/java/util/json/package-info.java (100%) rename {src => java-util-json-java21/src}/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java (100%) rename {src => java-util-json-java21/src}/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java (100%) rename {src => java-util-json-java21/src}/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java (100%) rename {src => java-util-json-java21/src}/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java (100%) rename {src => java-util-json-java21/src}/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java (100%) diff --git a/.gitignore b/.gitignore index 667c04f..cea8c89 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ target/ .idea/ .claude/ +.aider* diff --git a/api-tracker/pom.xml b/api-tracker/pom.xml new file mode 100644 index 0000000..3e32962 --- /dev/null +++ b/api-tracker/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + + io.github.simbo1905 + java-util-json-java21-parent + 0.1-SNAPSHOT + + + java-util-json-java21-api-tracker + jar + + API Tracker + + + + io.github.simbo1905 + java-util-json-java21 + ${project.version} + + + diff --git a/api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerMain.java b/api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerMain.java new file mode 100644 index 0000000..68cb6e3 --- /dev/null +++ b/api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerMain.java @@ -0,0 +1,35 @@ +package io.github.simbo1905.tracker; + +import jdk.sandbox.java.util.json.Json; +import jdk.sandbox.java.util.json.JsonObject; +import jdk.sandbox.java.util.json.JsonString; +import jdk.sandbox.java.util.json.JsonValue; +import jdk.sandbox.java.util.json.JsonParseException; + +public class ApiTrackerMain { + public static void main(String[] args) { + String testJson = """ + { + "module": "api-tracker", + "status": "ok", + "dependencies": [ + "java-util-json-java21" + ], + "active": true + } + """ +; + System.out.println("Parsing test JSON in api-tracker module..."); + + try { + JsonValue parsedValue = Json.parse(testJson); + if (parsedValue instanceof JsonObject jsonObject) { + System.out.println("Successfully parsed JsonObject!"); + System.out.println("Module: " + ((JsonString) jsonObject.members().get("module")).value()); + System.out.println("Status: " + ((JsonString) jsonObject.members().get("status")).value()); + } + } catch (JsonParseException e) { + System.err.println("Failed to parse JSON: " + e.getMessage()); + } + } +} diff --git a/java-util-json-java21/pom.xml b/java-util-json-java21/pom.xml new file mode 100644 index 0000000..4e0851a --- /dev/null +++ b/java-util-json-java21/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + + io.github.simbo1905 + java-util-json-java21-parent + 0.1-SNAPSHOT + + + java-util-json-java21 + jar + + java.util.json Backport + + + + org.junit.jupiter + junit-jupiter-api + + + org.junit.jupiter + junit-jupiter-engine + + + org.assertj + assertj-core + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + \ No newline at end of file diff --git a/src/main/java/jdk/sandbox/demo/JsonDemo.java b/java-util-json-java21/src/main/java/jdk/sandbox/demo/JsonDemo.java similarity index 100% rename from src/main/java/jdk/sandbox/demo/JsonDemo.java rename to java-util-json-java21/src/main/java/jdk/sandbox/demo/JsonDemo.java diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java b/java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java similarity index 100% rename from src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java rename to java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java b/java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java similarity index 100% rename from src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java rename to java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java b/java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java similarity index 100% rename from src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java rename to java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java b/java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java similarity index 100% rename from src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java rename to java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java b/java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java similarity index 100% rename from src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java rename to java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java b/java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java similarity index 100% rename from src/main/java/jdk/sandbox/internal/util/json/JsonParser.java rename to java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java b/java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java similarity index 100% rename from src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java rename to java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java diff --git a/src/main/java/jdk/sandbox/internal/util/json/StableValue.java b/java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/StableValue.java similarity index 100% rename from src/main/java/jdk/sandbox/internal/util/json/StableValue.java rename to java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/StableValue.java diff --git a/src/main/java/jdk/sandbox/internal/util/json/Utils.java b/java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/Utils.java similarity index 100% rename from src/main/java/jdk/sandbox/internal/util/json/Utils.java rename to java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/Utils.java diff --git a/src/main/java/jdk/sandbox/java/util/json/Json.java b/java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/Json.java similarity index 100% rename from src/main/java/jdk/sandbox/java/util/json/Json.java rename to java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/Json.java diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonArray.java b/java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonArray.java similarity index 100% rename from src/main/java/jdk/sandbox/java/util/json/JsonArray.java rename to java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonArray.java diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java b/java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java similarity index 100% rename from src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java rename to java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonNull.java b/java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNull.java similarity index 100% rename from src/main/java/jdk/sandbox/java/util/json/JsonNull.java rename to java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNull.java diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonNumber.java b/java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNumber.java similarity index 100% rename from src/main/java/jdk/sandbox/java/util/json/JsonNumber.java rename to java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNumber.java diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonObject.java b/java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonObject.java similarity index 100% rename from src/main/java/jdk/sandbox/java/util/json/JsonObject.java rename to java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonObject.java diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonParseException.java b/java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonParseException.java similarity index 100% rename from src/main/java/jdk/sandbox/java/util/json/JsonParseException.java rename to java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonParseException.java diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonString.java b/java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonString.java similarity index 100% rename from src/main/java/jdk/sandbox/java/util/json/JsonString.java rename to java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonString.java diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonValue.java b/java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonValue.java similarity index 100% rename from src/main/java/jdk/sandbox/java/util/json/JsonValue.java rename to java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonValue.java diff --git a/src/main/java/jdk/sandbox/java/util/json/package-info.java b/java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/package-info.java similarity index 100% rename from src/main/java/jdk/sandbox/java/util/json/package-info.java rename to java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/package-info.java diff --git a/src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java b/java-util-json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java similarity index 100% rename from src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java rename to java-util-json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java diff --git a/src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java b/java-util-json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java similarity index 100% rename from src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java rename to java-util-json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java diff --git a/src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java b/java-util-json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java similarity index 100% rename from src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java rename to java-util-json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java diff --git a/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java b/java-util-json-java21/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java similarity index 100% rename from src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java rename to java-util-json-java21/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java diff --git a/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java b/java-util-json-java21/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java similarity index 100% rename from src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java rename to java-util-json-java21/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java diff --git a/pom.xml b/pom.xml index 03aca4c..c717cd0 100644 --- a/pom.xml +++ b/pom.xml @@ -1,80 +1,86 @@ - - 4.0.0 + + + 4.0.0 - jdk-sandbox - json-experimental - 0.1-SNAPSHOT - jar + io.github.simbo1905 + java-util-json-java21-parent + 0.1-SNAPSHOT + pom - java.util.json Backport for JDK 21+ - Early access to future java.util.json API - tracking OpenJDK sandbox development - https://simbo1905.github.io/java.util.json.Java21/ + java.util.json Backport Parent + A backport of the upcoming java.util.json API for Java 21+ + https://simbo1905.github.io/java.util.json.Java21/ - - - GNU General Public License, version 2, with the Classpath Exception - https://www.gnu.org/licenses/old-licenses/gpl-2.0.html - repo - - + + + GNU General Public License, version 2, with the Classpath Exception + https://openjdk.org/legal/gplv2+ce.html + + - - 21 - 21 - 5.13.1 - 1.13.1 - + + + Simon + simon@simon.com + simon + https://github.com/simbo1905 + + - - - org.junit.jupiter - junit-jupiter-api - ${junit.jupiter.version} - test - - - org.junit.jupiter - junit-jupiter-engine - ${junit.jupiter.version} - test - - - org.junit.platform - junit-platform-launcher - ${junit.platform.version} - test - - - org.junit.platform - junit-platform-console - ${junit.platform.version} - test - - - org.assertj - assertj-core - 3.26.3 - test - - + + scm:git:git://github.com/simbo1905/java.util.json.Java21.git + scm:git:ssh://github.com:simbo1905/java.util.json.Java21.git + https://github.com/simbo1905/java.util.json.Java21/tree/main + - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - 21 - 21 - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.2.5 - - - + + java-util-json-java21 + api-tracker + + + + UTF-8 + 21 + 21 + 5.10.2 + 3.25.3 + 3.2.5 + + + + + + org.junit.jupiter + junit-jupiter-api + ${junit.jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.jupiter.version} + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + + From 96729c2a99ace47c86fb9d3bd08367d428ccda77 Mon Sep 17 00:00:00 2001 From: Simon Massey Date: Fri, 25 Jul 2025 22:56:46 +0100 Subject: [PATCH 2/6] build: Enforce stricter compiler settings and fix warnings --- .../simbo1905/tracker/ApiTrackerMain.java | 35 ---- .../pom.xml | 12 +- .../simbo1905/tracker/ApiTrackerMain.java | 167 ++++++++++++++++++ .../pom.xml | 20 +-- .../main/java/jdk/sandbox/demo/JsonDemo.java | 0 .../internal/util/json/JsonArrayImpl.java | 0 .../internal/util/json/JsonBooleanImpl.java | 0 .../internal/util/json/JsonNullImpl.java | 0 .../internal/util/json/JsonNumberImpl.java | 0 .../internal/util/json/JsonObjectImpl.java | 0 .../internal/util/json/JsonParser.java | 0 .../internal/util/json/JsonStringImpl.java | 0 .../internal/util/json/StableValue.java | 0 .../jdk/sandbox/internal/util/json/Utils.java | 0 .../java/jdk/sandbox/java/util/json/Json.java | 0 .../jdk/sandbox/java/util/json/JsonArray.java | 0 .../sandbox/java/util/json/JsonBoolean.java | 0 .../jdk/sandbox/java/util/json/JsonNull.java | 0 .../sandbox/java/util/json/JsonNumber.java | 0 .../sandbox/java/util/json/JsonObject.java | 0 .../java/util/json/JsonParseException.java | 0 .../sandbox/java/util/json/JsonString.java | 0 .../jdk/sandbox/java/util/json/JsonValue.java | 0 .../sandbox/java/util/json/package-info.java | 0 .../internal/util/json/JsonParserTests.java | 0 .../util/json/JsonPatternMatchingTests.java | 0 .../util/json/JsonRecordMappingTests.java | 0 .../java/util/json/JsonTypedUntypedTests.java | 0 .../java/util/json/ReadmeDemoTests.java | 0 pom.xml | 51 +++++- 30 files changed, 224 insertions(+), 61 deletions(-) delete mode 100644 api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerMain.java rename {api-tracker => json-java21-api-tracker}/pom.xml (65%) create mode 100644 json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerMain.java rename {java-util-json-java21 => json-java21}/pom.xml (69%) rename {java-util-json-java21 => json-java21}/src/main/java/jdk/sandbox/demo/JsonDemo.java (100%) rename {java-util-json-java21 => json-java21}/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java (100%) rename {java-util-json-java21 => json-java21}/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java (100%) rename {java-util-json-java21 => json-java21}/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java (100%) rename {java-util-json-java21 => json-java21}/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java (100%) rename {java-util-json-java21 => json-java21}/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java (100%) rename {java-util-json-java21 => json-java21}/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java (100%) rename {java-util-json-java21 => json-java21}/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java (100%) rename {java-util-json-java21 => json-java21}/src/main/java/jdk/sandbox/internal/util/json/StableValue.java (100%) rename {java-util-json-java21 => json-java21}/src/main/java/jdk/sandbox/internal/util/json/Utils.java (100%) rename {java-util-json-java21 => json-java21}/src/main/java/jdk/sandbox/java/util/json/Json.java (100%) rename {java-util-json-java21 => json-java21}/src/main/java/jdk/sandbox/java/util/json/JsonArray.java (100%) rename {java-util-json-java21 => json-java21}/src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java (100%) rename {java-util-json-java21 => json-java21}/src/main/java/jdk/sandbox/java/util/json/JsonNull.java (100%) rename {java-util-json-java21 => json-java21}/src/main/java/jdk/sandbox/java/util/json/JsonNumber.java (100%) rename {java-util-json-java21 => json-java21}/src/main/java/jdk/sandbox/java/util/json/JsonObject.java (100%) rename {java-util-json-java21 => json-java21}/src/main/java/jdk/sandbox/java/util/json/JsonParseException.java (100%) rename {java-util-json-java21 => json-java21}/src/main/java/jdk/sandbox/java/util/json/JsonString.java (100%) rename {java-util-json-java21 => json-java21}/src/main/java/jdk/sandbox/java/util/json/JsonValue.java (100%) rename {java-util-json-java21 => json-java21}/src/main/java/jdk/sandbox/java/util/json/package-info.java (100%) rename {java-util-json-java21 => json-java21}/src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java (100%) rename {java-util-json-java21 => json-java21}/src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java (100%) rename {java-util-json-java21 => json-java21}/src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java (100%) rename {java-util-json-java21 => json-java21}/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java (100%) rename {java-util-json-java21 => json-java21}/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java (100%) diff --git a/api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerMain.java b/api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerMain.java deleted file mode 100644 index 68cb6e3..0000000 --- a/api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerMain.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.github.simbo1905.tracker; - -import jdk.sandbox.java.util.json.Json; -import jdk.sandbox.java.util.json.JsonObject; -import jdk.sandbox.java.util.json.JsonString; -import jdk.sandbox.java.util.json.JsonValue; -import jdk.sandbox.java.util.json.JsonParseException; - -public class ApiTrackerMain { - public static void main(String[] args) { - String testJson = """ - { - "module": "api-tracker", - "status": "ok", - "dependencies": [ - "java-util-json-java21" - ], - "active": true - } - """ -; - System.out.println("Parsing test JSON in api-tracker module..."); - - try { - JsonValue parsedValue = Json.parse(testJson); - if (parsedValue instanceof JsonObject jsonObject) { - System.out.println("Successfully parsed JsonObject!"); - System.out.println("Module: " + ((JsonString) jsonObject.members().get("module")).value()); - System.out.println("Status: " + ((JsonString) jsonObject.members().get("status")).value()); - } - } catch (JsonParseException e) { - System.err.println("Failed to parse JSON: " + e.getMessage()); - } - } -} diff --git a/api-tracker/pom.xml b/json-java21-api-tracker/pom.xml similarity index 65% rename from api-tracker/pom.xml rename to json-java21-api-tracker/pom.xml index 3e32962..b7a2524 100644 --- a/api-tracker/pom.xml +++ b/json-java21-api-tracker/pom.xml @@ -5,21 +5,21 @@ 4.0.0 - io.github.simbo1905 - java-util-json-java21-parent + io.github.simbo1905.json + json-java21-parent 0.1-SNAPSHOT - java-util-json-java21-api-tracker + json-java21-api-tracker jar API Tracker - io.github.simbo1905 - java-util-json-java21 + io.github.simbo1905.json + json-java21 ${project.version} - + \ No newline at end of file diff --git a/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerMain.java b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerMain.java new file mode 100644 index 0000000..c9c02c8 --- /dev/null +++ b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerMain.java @@ -0,0 +1,167 @@ +package io.github.simbo1905.tracker; + +import jdk.sandbox.java.util.json.Json; +import jdk.sandbox.java.util.json.JsonArray; +import jdk.sandbox.java.util.json.JsonBoolean; +import jdk.sandbox.java.util.json.JsonObject; +import jdk.sandbox.java.util.json.JsonString; +import jdk.sandbox.java.util.json.JsonValue; +import jdk.sandbox.java.util.json.JsonParseException; + +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.logging.ConsoleHandler; +import java.util.logging.SimpleFormatter; + +/** + * Main entry point for the API Tracker tool. + * + * This tool analyzes Java API structures and tracks changes between + * the OpenJDK sandbox java.util.json implementation and this backport. + */ +public class ApiTrackerMain { + private static final Logger LOGGER = Logger.getLogger(ApiTrackerMain.class.getName()); + + static { + // Configure logging with a clean formatter + Logger rootLogger = Logger.getLogger(""); + rootLogger.setLevel(Level.INFO); + + // Remove default handlers + for (var handler : rootLogger.getHandlers()) { + rootLogger.removeHandler(handler); + } + + // Add console handler with simple formatting + ConsoleHandler consoleHandler = new ConsoleHandler(); + consoleHandler.setLevel(Level.ALL); + consoleHandler.setFormatter(new SimpleFormatter() { + @Override + public synchronized String format(java.util.logging.LogRecord record) { + return String.format("[%s] %s - %s%n", + record.getLevel().getName(), + record.getLoggerName(), + formatMessage(record) + ); + } + }); + rootLogger.addHandler(consoleHandler); + } + + public static void main(String[] args) { + LOGGER.info("Starting API Tracker v0.1-SNAPSHOT"); + + // Validate our JSON parsing works correctly + if (!validateJsonBackport()) { + LOGGER.severe("JSON backport validation failed"); + System.exit(1); + } + + LOGGER.info("JSON backport validation successful"); + + // TODO: Implement API analysis logic + LOGGER.info("API Tracker initialized successfully"); + } + + /** + * Validates that the JSON backport is working correctly. + * Tests various JSON structures to ensure compatibility. + */ + private static boolean validateJsonBackport() { + try { + // Test complex JSON structure + String testJson = """ + { + "apiTracker": { + "version": "0.1-SNAPSHOT", + "modules": { + "core": "java-util-json-java21", + "tracker": "java-util-json-java21-api-tracker" + }, + "features": [ + "API extraction", + "Structural comparison", + "GitHub integration" + ], + "config": { + "autoTrack": true, + "createIssues": true, + "trackInterval": 86400 + } + } + } + """; + + LOGGER.fine("Parsing test JSON structure"); + JsonValue parsedValue = Json.parse(testJson); + + if (!(parsedValue instanceof JsonObject root)) { + LOGGER.severe("Expected JsonObject but got: " + parsedValue.getClass().getName()); + return false; + } + + // Navigate the structure to validate parsing + JsonObject apiTracker = (JsonObject) root.members().get("apiTracker"); + String version = ((JsonString) apiTracker.members().get("version")).value(); + + if (!"0.1-SNAPSHOT".equals(version)) { + LOGGER.severe("Version mismatch: expected 0.1-SNAPSHOT, got " + version); + return false; + } + + // Validate nested objects + JsonObject modules = (JsonObject) apiTracker.members().get("modules"); + LOGGER.fine("Found modules: " + modules.members().size()); + + // Validate array handling + JsonArray features = (JsonArray) apiTracker.members().get("features"); + LOGGER.fine("Found features: " + features.values().size()); + + // Validate boolean handling + JsonObject config = (JsonObject) apiTracker.members().get("config"); + boolean autoTrack = ((JsonBoolean) config.members().get("autoTrack")).value(); + + if (!autoTrack) { + LOGGER.warning("autoTrack is disabled in test configuration"); + } + + // Test edge cases + testEdgeCases(); + + LOGGER.info("All JSON validation tests passed"); + return true; + + } catch (JsonParseException e) { + LOGGER.log(Level.SEVERE, "JSON parsing failed", e); + return false; + } catch (ClassCastException e) { + LOGGER.log(Level.SEVERE, "Unexpected JSON structure", e); + return false; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Unexpected error during validation", e); + return false; + } + } + + /** + * Tests edge cases for JSON parsing. + */ + private static void testEdgeCases() throws JsonParseException { + // Empty object + Json.parse("{}"); + + // Empty array + Json.parse("[]"); + + // Null value + Json.parse("{\"key\": null}"); + + // Unicode handling + Json.parse("{\"unicode\": \"Hello \\u4e16\\u754c\"}"); + + // Number formats + Json.parse("{\"int\": 42, \"float\": 3.14, \"exp\": 1.23e-4}"); + + LOGGER.fine("Edge case tests completed"); + } +} \ No newline at end of file diff --git a/java-util-json-java21/pom.xml b/json-java21/pom.xml similarity index 69% rename from java-util-json-java21/pom.xml rename to json-java21/pom.xml index 4e0851a..10ade61 100644 --- a/java-util-json-java21/pom.xml +++ b/json-java21/pom.xml @@ -5,12 +5,12 @@ 4.0.0 - io.github.simbo1905 - java-util-json-java21-parent + io.github.simbo1905.json + json-java21-parent 0.1-SNAPSHOT - java-util-json-java21 + json-java21 jar java.util.json Backport @@ -19,23 +19,17 @@ org.junit.jupiter junit-jupiter-api + test org.junit.jupiter junit-jupiter-engine + test org.assertj assertj-core + test - - - - - org.apache.maven.plugins - maven-surefire-plugin - - - - \ No newline at end of file + diff --git a/java-util-json-java21/src/main/java/jdk/sandbox/demo/JsonDemo.java b/json-java21/src/main/java/jdk/sandbox/demo/JsonDemo.java similarity index 100% rename from java-util-json-java21/src/main/java/jdk/sandbox/demo/JsonDemo.java rename to json-java21/src/main/java/jdk/sandbox/demo/JsonDemo.java diff --git a/java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java similarity index 100% rename from java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java diff --git a/java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java similarity index 100% rename from java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java diff --git a/java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java similarity index 100% rename from java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java diff --git a/java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java similarity index 100% rename from java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java diff --git a/java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java similarity index 100% rename from java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java diff --git a/java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java similarity index 100% rename from java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java diff --git a/java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java similarity index 100% rename from java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java diff --git a/java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/StableValue.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/StableValue.java similarity index 100% rename from java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/StableValue.java rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/StableValue.java diff --git a/java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/Utils.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/Utils.java similarity index 100% rename from java-util-json-java21/src/main/java/jdk/sandbox/internal/util/json/Utils.java rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/Utils.java diff --git a/java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/Json.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/Json.java similarity index 100% rename from java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/Json.java rename to json-java21/src/main/java/jdk/sandbox/java/util/json/Json.java diff --git a/java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonArray.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonArray.java similarity index 100% rename from java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonArray.java rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonArray.java diff --git a/java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java similarity index 100% rename from java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java diff --git a/java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNull.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNull.java similarity index 100% rename from java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNull.java rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNull.java diff --git a/java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNumber.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNumber.java similarity index 100% rename from java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNumber.java rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNumber.java diff --git a/java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonObject.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonObject.java similarity index 100% rename from java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonObject.java rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonObject.java diff --git a/java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonParseException.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonParseException.java similarity index 100% rename from java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonParseException.java rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonParseException.java diff --git a/java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonString.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonString.java similarity index 100% rename from java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonString.java rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonString.java diff --git a/java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonValue.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonValue.java similarity index 100% rename from java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/JsonValue.java rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonValue.java diff --git a/java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/package-info.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/package-info.java similarity index 100% rename from java-util-json-java21/src/main/java/jdk/sandbox/java/util/json/package-info.java rename to json-java21/src/main/java/jdk/sandbox/java/util/json/package-info.java diff --git a/java-util-json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java b/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java similarity index 100% rename from java-util-json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java rename to json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java diff --git a/java-util-json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java b/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java similarity index 100% rename from java-util-json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java rename to json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java diff --git a/java-util-json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java b/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java similarity index 100% rename from java-util-json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java rename to json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java diff --git a/java-util-json-java21/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java b/json-java21/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java similarity index 100% rename from java-util-json-java21/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java rename to json-java21/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java diff --git a/java-util-json-java21/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java b/json-java21/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java similarity index 100% rename from java-util-json-java21/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java rename to json-java21/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java diff --git a/pom.xml b/pom.xml index c717cd0..086aeb5 100644 --- a/pom.xml +++ b/pom.xml @@ -4,8 +4,8 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - io.github.simbo1905 - java-util-json-java21-parent + io.github.simbo1905.json + json-java21-parent 0.1-SNAPSHOT pom @@ -36,17 +36,23 @@ - java-util-json-java21 - api-tracker + json-java21 + json-java21-api-tracker UTF-8 - 21 - 21 + 21 5.10.2 3.25.3 + + + 3.4.0 + 3.3.1 + 3.13.0 3.2.5 + 3.4.2 + 3.1.2 @@ -75,12 +81,43 @@ + + org.apache.maven.plugins + maven-clean-plugin + ${maven-clean-plugin.version} + + + org.apache.maven.plugins + maven-resources-plugin + ${maven-resources-plugin.version} + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + + -Xlint:all + -Werror + + + org.apache.maven.plugins maven-surefire-plugin ${maven-surefire-plugin.version} + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + org.apache.maven.plugins + maven-install-plugin + ${maven-install-plugin.version} + - + \ No newline at end of file From 29eb89fda2635d6d65152576a478a6a453d5e6cd Mon Sep 17 00:00:00 2001 From: Simon Massey Date: Sat, 26 Jul 2025 07:48:14 +0100 Subject: [PATCH 3/6] feat: Add API tracker to compare local and upstream JSON APIs - Implement API tracker module with reflection for local classes - Add compiler parsing for upstream source analysis - Create comparison logic to identify API differences - Add GitHub Action for daily API tracking (cron: 2 AM UTC) - Include command-line runner for manual testing The tracker discovers local classes, fetches upstream sources from GitHub, extracts public APIs using both reflection and compiler parsing, then generates a detailed comparison report showing any differences. Closes #7 --- .github/workflows/daily-api-tracker.yml | 59 ++ .gitignore | 1 + CODING_STYLE_LLM.md | 133 +++ json-java21-api-tracker/pom.xml | 63 ++ .../github/simbo1905/tracker/ApiTracker.java | 976 ++++++++++++++++++ .../simbo1905/tracker/ApiTrackerRunner.java | 51 + .../simbo1905/tracker/ApiTrackerTest.java | 230 +++++ .../tracker/CompilerApiLearningTest.java | 297 ++++++ .../simbo1905/tracker/LoggingControl.java | 52 + .../src/test/resources/JsonObject.java | 101 ++ mvn-test-no-boilerplate.sh | 71 ++ 11 files changed, 2034 insertions(+) create mode 100644 .github/workflows/daily-api-tracker.yml create mode 100644 CODING_STYLE_LLM.md create mode 100644 json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java create mode 100644 json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerRunner.java create mode 100644 json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java create mode 100644 json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/CompilerApiLearningTest.java create mode 100644 json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/LoggingControl.java create mode 100644 json-java21-api-tracker/src/test/resources/JsonObject.java create mode 100755 mvn-test-no-boilerplate.sh diff --git a/.github/workflows/daily-api-tracker.yml b/.github/workflows/daily-api-tracker.yml new file mode 100644 index 0000000..121f14c --- /dev/null +++ b/.github/workflows/daily-api-tracker.yml @@ -0,0 +1,59 @@ +name: Daily API Tracker + +on: + schedule: + # Run daily at 2 AM UTC + - cron: '0 2 * * *' + workflow_dispatch: # Allow manual trigger for testing + +jobs: + track-api: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Cache Maven dependencies + uses: actions/cache@v4 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + + - name: Build project + run: mvn clean compile -DskipTests + + - name: Run API Tracker + run: | + mvn exec:java \ + -pl json-java21-api-tracker \ + -Dexec.mainClass="io.github.simbo1905.tracker.ApiTrackerRunner" \ + -Dexec.args="INFO" \ + -Djava.util.logging.ConsoleHandler.level=INFO + + - name: Create issue if differences found + if: failure() + uses: actions/github-script@v7 + with: + script: | + const title = 'API differences detected between local and upstream'; + const body = `The daily API tracker found differences between our local implementation and upstream. + + Check the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details. + + Date: ${new Date().toISOString().split('T')[0]}`; + + github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: title, + body: body, + labels: ['api-tracking', 'upstream-sync'] + }); \ No newline at end of file diff --git a/.gitignore b/.gitignore index cea8c89..4658f71 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ target/ .claude/ .aider* +CLAUDE.md diff --git a/CODING_STYLE_LLM.md b/CODING_STYLE_LLM.md new file mode 100644 index 0000000..48853b3 --- /dev/null +++ b/CODING_STYLE_LLM.md @@ -0,0 +1,133 @@ +# Java DOP Coding Standards #################### + +This file is a Gen AI summary of CODING_STYLE.md to use less tokens of context window. Read the original file for full details. + +IMPORTANT: We do TDD so all code must include targeted unit tests. +IMPORTANT: Never disable tests written for logic that we are yet to write we do Red-Green-Refactor coding. + +## Core Principles + +* Use Records for all data structures. Use sealed interfaces for protocols. +* Prefer static methods with Records as parameters +* Default to package-private scope +* Package-by-feature, not package-by-layer +* Create fewer, cohesive, wide packages (functionality modules or records as protocols) +* Use public only when cross-package access is required +* Use JEP 467 Markdown documentation examples: `/// good markdown` not legacy `/** bad html */` +* Apply Data-Oriented Programming principles and avoid OOP +* Use Stream operations instead of traditional loops. Never use `for(;;)` with mutable loop variables use + `Arrays.setAll` +* Prefer exhaustive destructuring switch expressions over if-else statements +* Use destructuring switch expressions that operate on Records and sealed interfaces +* Use anonymous variables in record destructuring and switch expressions +* Use `final var` for local variables, parameters, and destructured fields +* Apply JEP 371 "Local Classes and Interfaces" for cohesive files with narrow APIs + +## Data-Oriented Programming + +* Separate data (immutable Records) from behavior (never utility classes always static methods) +* Use immutable generic data structures (maps, lists, sets) and take defense copies in constructors +* Write pure functions that don't modify state +* Leverage Java 21+ features: + * Records for immutable data + * Pattern matching for structural decomposition + * Sealed classes for exhaustive switches + * Virtual threads for concurrent processing + +## Package Structure + +* Use default (package-private) access as the standard. Do not use 'private' or 'public' by default. +* Limit public to genuine cross-package APIs +* Prefer package-private static methods. Do not use 'private' or 'public' by default. +* Limit private to security-related code +* Avoid anti-patterns: boilerplate OOP, excessive layering, dependency injection overuse + +## Constants and Magic Numbers + +* **NEVER use magic numbers** - always use enum constants +* **NEVER write large if-else-if statements over known types** - will not be exhaustive and creates bugs when new types are added. Use exhaustive switch statements over bounded sets such as enum values or sealed interface permits + +## Functional Style + +* Combine Records + static methods for functional programming +* Emphasize immutability and explicit state transformations +* Reduce package count to improve testability +* Implement Algebraic Data Types pattern with Function Modules +* Modern Stream Programming +* Use Stream API instead of traditional loops +* Write declarative rather than imperative code +* Chain operations without intermediate variables +* Support immutability throughout processing +* Example: `IntStream.range(0, 100).filter(i -> i % 2 == 0).sum()` instead of counting loops +* Always use final variables in functional style. +* Prefer `final var` with self documenting names over `int i` or `String s` but its not possible to do that on a `final` variable that is not yet initialized so its a weak preference not a strong one. +* Avoid just adding new functionality to the top of a method to make an early return. It is fine to have a simple guard statement. Yet general you should pattern match over the input to do different things with the same method. Adding special case logic is a code smell that should be avoided. + +## Documentation using JEP 467 Markdown documentation + +IMPORTANT: You must not write JavaDoc comments that start with `/**` and end with `*/` +IMPORTANT: You must "JEP 467: Markdown Documentation Comments" that start all lines with `///` + +Here is an example of the correct format for documentation comments: + +```java +/// Returns a hash code value for the object. This method is +/// supported for the benefit of hash tables such as those provided by +/// [java.util.HashMap]. +/// +/// The general contract of `hashCode` is: +/// +/// - Whenever it is invoked on the same object more than once during +/// an execution of a Java application, the `hashCode` method +/// - If two objects are equal according to the +/// [equals][#equals(Object)] method, then calling the +/// - It is _not_ required that if two objects are unequal +/// according to the [equals][#equals(Object)] method, then +/// +/// @return a hash code value for this object. +/// @see java.lang.Object#equals(java.lang.Object) +``` + +## Logging + +- Use Java's built-in logging: `java.util.logging.Logger` +- Log levels: Use appropriate levels (FINE, FINER, INFO, WARNING, SEVERE) + - **FINE**: Production-level debugging, default for most debug output + - **FINER**: Verbose debugging, detailed internal flow, class resolution details + - **INFO**: Important runtime information +- LOGGER is a static field: `static final Logger LOGGER = Logger.getLogger(ClassName.class.getName());` where use the primary interface or the package as the logger name with the logger package-private and shared across the classes when the package is small enough. +- Use lambda logging for performance: `LOGGER.fine(() -> "message " + variable);` + +# Compile, Test, Debug Loop + +- **Check Compiles**: Focusing on the correct mvn module run without verbose logging and do not grep the output to see compile errors: + ```bash + ./mvn-test-no-boilerplate.sh -pl json-java21-api-tracker -Djava.util.logging.ConsoleHandler.level=SEVERE + ``` +- **Debug with Verbose Logs**: Use `-Dtest=` to focus on just one or two test methods, or one class, using more logging to debug the code: + ```bash + ./mvn-test-no-boilerplate.sh -pl json-java21-api-tracker -Dtest=XXX -Djava.util.logging.ConsoleHandler.level=FINER + ``` +- **No Grep Filtering**: Use logging levels to filter output, do not grep the output for compile errors, just run less test methods with the correct logging to reduce the output to a manageable size. Filtering hides problems and needs more test excution to find the same problems which wastes time. + +## Modern Java Singleton Pattern: Sealed Interfaces + +**Singleton Object Anti-Pattern**: Traditional singleton classes with private constructors and static instances are legacy should be avoided. With a functional style we can create a "package-private companion module" of small package-private methods with `sealed interfacee GoodSingletonModule permits Nothing { enum Nothing extends GoodSingletonModule{}; /* static functional methods here */ }`. + +### Assertions and Input Validation + +1. On the public API entry points use `Objects.assertNonNull()` to ensure that the inputs are legal. +2. After that on internal method that should be passed only valid data use `assert` to ensure that the data is valid. + - e.g. use `assert x==y: "unexpected x="+x+" y="+y;` as `mvn` base should be run with `-ea` to enable assertions. +3. Often there is an `orElseThrow()` which can be used so the only reason to use `assert` is to add more logging to the error message. +4. Consider using the validations of `Object` and `Arrays` and the like to ensure that the data is valid. + - e.g. `Objects.requireNonNull(type, "type must not be null")` or `Arrays.checkIndex(index, array.length)`. + +## JEP References + +[JEP 467](https://openjdk.org/jeps/467): Markdown Documentation in JavaDoc +[JEP 371](https://openjdk.org/jeps/371): Local Classes and Interfaces +[JEP 395](https://openjdk.org/jeps/395): Records +[JEP 409](https://openjdk.org/jeps/409): Sealed Classes +[JEP 440](https://openjdk.org/jeps/440): Record Patterns +[JEP 427](https://openjdk.org/jeps/427): Pattern Matching for Switch diff --git a/json-java21-api-tracker/pom.xml b/json-java21-api-tracker/pom.xml index b7a2524..eb03e82 100644 --- a/json-java21-api-tracker/pom.xml +++ b/json-java21-api-tracker/pom.xml @@ -15,11 +15,74 @@ API Tracker + + 24 + + io.github.simbo1905.json json-java21 ${project.version} + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + test + + + + + relaxed + + + + org.apache.maven.plugins + maven-compiler-plugin + + + -Xlint:all + + + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.release} + + --enable-preview + -Xlint:all + -Werror + + + + + org.apache.maven.plugins + maven-surefire-plugin + + --enable-preview + + + + \ No newline at end of file diff --git a/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java new file mode 100644 index 0000000..8fd6f61 --- /dev/null +++ b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java @@ -0,0 +1,976 @@ +package io.github.simbo1905.tracker; + +import jdk.sandbox.java.util.json.JsonArray; +import jdk.sandbox.java.util.json.JsonObject; +import jdk.sandbox.java.util.json.JsonString; +import jdk.sandbox.java.util.json.JsonValue; +import jdk.sandbox.java.util.json.JsonNumber; +import jdk.sandbox.java.util.json.JsonBoolean; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.tools.DiagnosticCollector; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; + +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.ModifiersTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.JavacTask; +import com.sun.source.util.TreePathScanner; +import com.sun.source.util.Trees; + +/// API Tracker module for comparing local and upstream JSON APIs +/// +/// This module provides functionality to: +/// - Discover local JSON API classes via reflection +/// - Fetch corresponding upstream sources from GitHub +/// - Compare public APIs using compiler parsing +/// - Generate structured diff reports +/// +/// All functionality is exposed as static methods following functional programming principles +public sealed interface ApiTracker permits ApiTracker.Nothing { + + /// Empty enum to seal the interface - no instances allowed + enum Nothing implements ApiTracker {} + + // Package-private logger shared across the module + static final Logger LOGGER = Logger.getLogger(ApiTracker.class.getName()); + + // Cache for HTTP responses to avoid repeated fetches + static final Map FETCH_CACHE = new ConcurrentHashMap<>(); + + // GitHub base URL for upstream sources + static final String GITHUB_BASE_URL = "https://raw.githubusercontent.com/openjdk/jdk-sandbox/refs/heads/json/src/java.base/share/classes/"; + + /// Discovers all classes in the local JSON API packages + /// @return sorted set of classes from jdk.sandbox.java.util.json and jdk.sandbox.internal.util.json + static Set> discoverLocalJsonClasses() { + LOGGER.fine("Discovering local JSON classes"); + final var classes = new TreeSet>((a, b) -> a.getName().compareTo(b.getName())); + + // Known public API classes + final var publicApiClasses = List.of( + "jdk.sandbox.java.util.json.Json", + "jdk.sandbox.java.util.json.JsonValue", + "jdk.sandbox.java.util.json.JsonObject", + "jdk.sandbox.java.util.json.JsonArray", + "jdk.sandbox.java.util.json.JsonString", + "jdk.sandbox.java.util.json.JsonNumber", + "jdk.sandbox.java.util.json.JsonBoolean", + "jdk.sandbox.java.util.json.JsonNull", + "jdk.sandbox.java.util.json.JsonParseException" + ); + + // Known internal implementation classes + final var internalClasses = List.of( + "jdk.sandbox.internal.util.json.JsonParser", + "jdk.sandbox.internal.util.json.JsonObjectImpl", + "jdk.sandbox.internal.util.json.JsonArrayImpl", + "jdk.sandbox.internal.util.json.JsonStringImpl", + "jdk.sandbox.internal.util.json.JsonNumberImpl" + ); + + // Load all known classes + Stream.concat(publicApiClasses.stream(), internalClasses.stream()) + .forEach(className -> { + try { + final var clazz = Class.forName(className); + classes.add(clazz); + LOGGER.finer(() -> "Loaded class: " + className); + } catch (ClassNotFoundException e) { + LOGGER.fine(() -> "Class not found (might not exist yet): " + className); + } + }); + + LOGGER.fine(() -> "Discovered " + classes.size() + " classes"); + return Collections.unmodifiableSet(classes); + } + + /// Fetches upstream source files from GitHub for the given local classes + /// @param localClasses set of local classes to fetch upstream sources for + /// @return map of className to source code (or error message if fetch failed) + static Map fetchUpstreamSources(Set> localClasses) { + Objects.requireNonNull(localClasses, "localClasses must not be null"); + LOGGER.fine(() -> "Fetching upstream sources for " + localClasses.size() + " classes"); + + final var results = new LinkedHashMap(); + final var httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + + for (final var clazz : localClasses) { + final var className = clazz.getName(); + final var cachedSource = FETCH_CACHE.get(className); + + if (cachedSource != null) { + LOGGER.fine(() -> "Using cached source for: " + className); + results.put(className, cachedSource); + continue; + } + + // Map package name from jdk.sandbox.* to standard java.* + final var upstreamPath = mapToUpstreamPath(className); + final var url = GITHUB_BASE_URL + upstreamPath; + + LOGGER.fine(() -> "Fetching: " + url); + + try { + final var request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(Duration.ofSeconds(30)) + .GET() + .build(); + + final var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 200) { + final var body = response.body(); + FETCH_CACHE.put(className, body); + results.put(className, body); + LOGGER.fine(() -> "Successfully fetched: " + className); + } else if (response.statusCode() == 404) { + final var error = "NOT_FOUND: Upstream file not found (possibly deleted or renamed)"; + results.put(className, error); + LOGGER.fine(() -> "Not found: " + className); + } else { + final var error = "HTTP_ERROR: Status " + response.statusCode(); + results.put(className, error); + LOGGER.warning(() -> "HTTP error for " + className + ": " + response.statusCode()); + } + } catch (Exception e) { + final var error = "FETCH_ERROR: " + e.getMessage(); + results.put(className, error); + LOGGER.log(Level.WARNING, "Error fetching " + className, e); + } + } + + return Collections.unmodifiableMap(results); + } + + /// Maps local class name to upstream GitHub path + static String mapToUpstreamPath(String className) { + // Remove jdk.sandbox prefix and map to standard packages + String path = className + .replace("jdk.sandbox.java.util.json", "java/util/json") + .replace("jdk.sandbox.internal.util.json", "jdk/internal/util/json") + .replace('.', '/'); + + return path + ".java"; + } + + /// Extracts public API from a compiled class using reflection + /// @param clazz the class to extract API from + /// @return JSON representation of the class's public API + static JsonObject extractLocalApi(Class clazz) { + Objects.requireNonNull(clazz, "clazz must not be null"); + LOGGER.fine(() -> "Extracting local API for: " + clazz.getName()); + + final var apiMap = new LinkedHashMap(); + + // Basic class information + apiMap.put("className", JsonString.of(clazz.getSimpleName())); + apiMap.put("packageName", JsonString.of(clazz.getPackage() != null ? clazz.getPackage().getName() : "")); + apiMap.put("modifiers", extractModifiers(clazz.getModifiers())); + + // Type information + apiMap.put("isInterface", JsonBoolean.of(clazz.isInterface())); + apiMap.put("isEnum", JsonBoolean.of(clazz.isEnum())); + apiMap.put("isRecord", JsonBoolean.of(clazz.isRecord())); + apiMap.put("isSealed", JsonBoolean.of(clazz.isSealed())); + + // Inheritance + final var superTypes = new ArrayList(); + if (clazz.getSuperclass() != null && !Object.class.equals(clazz.getSuperclass())) { + superTypes.add(JsonString.of(clazz.getSuperclass().getSimpleName())); + } + Arrays.stream(clazz.getInterfaces()) + .map(i -> JsonString.of(i.getSimpleName())) + .forEach(superTypes::add); + apiMap.put("extends", JsonArray.of(superTypes)); + + // Permitted subclasses (for sealed types) + if (clazz.isSealed()) { + final var permits = Arrays.stream(clazz.getPermittedSubclasses()) + .map(c -> JsonString.of(c.getSimpleName())) + .collect(Collectors.toList()); + apiMap.put("permits", JsonArray.of(permits)); + } + + // Methods + apiMap.put("methods", extractMethods(clazz)); + + // Fields + apiMap.put("fields", extractFields(clazz)); + + // Constructors + apiMap.put("constructors", extractConstructors(clazz)); + + return JsonObject.of(apiMap); + } + + /// Extracts modifiers as JSON array + static JsonArray extractModifiers(int modifiers) { + final var modList = new ArrayList(); + + if (Modifier.isPublic(modifiers)) modList.add(JsonString.of("public")); + if (Modifier.isProtected(modifiers)) modList.add(JsonString.of("protected")); + if (Modifier.isPrivate(modifiers)) modList.add(JsonString.of("private")); + if (Modifier.isStatic(modifiers)) modList.add(JsonString.of("static")); + if (Modifier.isFinal(modifiers)) modList.add(JsonString.of("final")); + if (Modifier.isAbstract(modifiers)) modList.add(JsonString.of("abstract")); + if (Modifier.isNative(modifiers)) modList.add(JsonString.of("native")); + if (Modifier.isSynchronized(modifiers)) modList.add(JsonString.of("synchronized")); + if (Modifier.isTransient(modifiers)) modList.add(JsonString.of("transient")); + if (Modifier.isVolatile(modifiers)) modList.add(JsonString.of("volatile")); + + return JsonArray.of(modList); + } + + /// Extracts public methods + static JsonObject extractMethods(Class clazz) { + final var methodsMap = new LinkedHashMap(); + + Arrays.stream(clazz.getDeclaredMethods()) + .filter(m -> Modifier.isPublic(m.getModifiers())) + .forEach(method -> { + final var methodInfo = new LinkedHashMap(); + methodInfo.put("modifiers", extractModifiers(method.getModifiers())); + methodInfo.put("returnType", JsonString.of(method.getReturnType().getSimpleName())); + methodInfo.put("genericReturnType", JsonString.of(method.getGenericReturnType().getTypeName())); + + final var params = Arrays.stream(method.getParameters()) + .map(p -> JsonString.of(p.getType().getSimpleName() + " " + p.getName())) + .collect(Collectors.toList()); + methodInfo.put("parameters", JsonArray.of(params)); + + final var exceptions = Arrays.stream(method.getExceptionTypes()) + .map(e -> JsonString.of(e.getSimpleName())) + .collect(Collectors.toList()); + methodInfo.put("throws", JsonArray.of(exceptions)); + + methodsMap.put(method.getName(), JsonObject.of(methodInfo)); + }); + + return JsonObject.of(methodsMap); + } + + /// Extracts public fields + static JsonObject extractFields(Class clazz) { + final var fieldsMap = new LinkedHashMap(); + + Arrays.stream(clazz.getDeclaredFields()) + .filter(f -> Modifier.isPublic(f.getModifiers())) + .forEach(field -> { + final var fieldInfo = new LinkedHashMap(); + fieldInfo.put("modifiers", extractModifiers(field.getModifiers())); + fieldInfo.put("type", JsonString.of(field.getType().getSimpleName())); + fieldInfo.put("genericType", JsonString.of(field.getGenericType().getTypeName())); + + fieldsMap.put(field.getName(), JsonObject.of(fieldInfo)); + }); + + return JsonObject.of(fieldsMap); + } + + /// Extracts public constructors + static JsonArray extractConstructors(Class clazz) { + final var constructors = Arrays.stream(clazz.getDeclaredConstructors()) + .filter(c -> Modifier.isPublic(c.getModifiers())) + .map(constructor -> { + final var ctorInfo = new LinkedHashMap(); + ctorInfo.put("modifiers", extractModifiers(constructor.getModifiers())); + + final var params = Arrays.stream(constructor.getParameters()) + .map(p -> JsonString.of(p.getType().getSimpleName() + " " + p.getName())) + .collect(Collectors.toList()); + ctorInfo.put("parameters", JsonArray.of(params)); + + final var exceptions = Arrays.stream(constructor.getExceptionTypes()) + .map(e -> JsonString.of(e.getSimpleName())) + .collect(Collectors.toList()); + ctorInfo.put("throws", JsonArray.of(exceptions)); + + return JsonObject.of(ctorInfo); + }) + .collect(Collectors.toList()); + + return JsonArray.of(constructors); + } + + /// Extracts public API from upstream source code using compiler parsing + /// @param sourceCode the source code to parse + /// @param className the expected class name + /// @return JSON representation of the parsed API + static JsonObject extractUpstreamApi(String sourceCode, String className) { + Objects.requireNonNull(sourceCode, "sourceCode must not be null"); + Objects.requireNonNull(className, "className must not be null"); + + // Check for fetch errors + if (sourceCode.startsWith("NOT_FOUND:") || + sourceCode.startsWith("HTTP_ERROR:") || + sourceCode.startsWith("FETCH_ERROR:")) { + final var errorMap = Map.of( + "error", JsonString.of(sourceCode), + "className", JsonString.of(className) + ); + return JsonObject.of(errorMap); + } + + LOGGER.fine(() -> "Extracting upstream API for: " + className); + + final var compiler = ToolProvider.getSystemJavaCompiler(); + if (compiler == null) { + return JsonObject.of(Map.of( + "error", JsonString.of("JavaCompiler not available"), + "className", JsonString.of(className) + )); + } + + final var diagnostics = new DiagnosticCollector(); + final var fileManager = compiler.getStandardFileManager(diagnostics, null, StandardCharsets.UTF_8); + + try { + // Extract simple class name from fully qualified name + final var simpleClassName = className.substring(className.lastIndexOf('.') + 1); + + // Create compilation units + final var compilationUnits = new ArrayList(); + compilationUnits.add(new InMemoryJavaFileObject(className, sourceCode)); + + // Add minimal stubs for common dependencies + addCommonStubs(compilationUnits); + + // Parse-only compilation with relaxed settings + final var options = List.of( + "-proc:none", + "-XDignore.symbol.file", + "-Xlint:none", + "--enable-preview", + "--release", "24" + ); + + final var task = (JavacTask) compiler.getTask( + null, + fileManager, + diagnostics, + options, + null, + compilationUnits + ); + + final var trees = task.parse(); + + // Extract API using visitor + for (final var tree : trees) { + final var fileName = tree.getSourceFile().getName(); + if (fileName.contains(simpleClassName)) { + final var visitor = new ApiExtractorVisitor(); + visitor.scan(tree, null); + return visitor.getExtractedApi(); + } + } + + // If we get here, parsing failed + return JsonObject.of(Map.of( + "error", JsonString.of("Failed to parse source"), + "className", JsonString.of(className) + )); + + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Error parsing upstream source for " + className, e); + return JsonObject.of(Map.of( + "error", JsonString.of("Parse error: " + e.getMessage()), + "className", JsonString.of(className) + )); + } finally { + try { + fileManager.close(); + } catch (IOException e) { + LOGGER.log(Level.FINE, "Error closing file manager", e); + } + } + } + + /// Adds common stub dependencies for JSON API parsing + static void addCommonStubs(List compilationUnits) { + // PreviewFeature annotation stub + compilationUnits.add(new InMemoryJavaFileObject("jdk.internal.javac.PreviewFeature", """ + package jdk.internal.javac; + import java.lang.annotation.*; + @Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + public @interface PreviewFeature { + Feature feature(); + enum Feature { JSON } + } + """)); + + // JsonValue base interface stub + compilationUnits.add(new InMemoryJavaFileObject("java.util.json.JsonValue", """ + package java.util.json; + public sealed interface JsonValue permits JsonObject, JsonArray, JsonString, JsonNumber, JsonBoolean, JsonNull {} + """)); + + // Basic JSON type stubs + final var jsonTypes = List.of("JsonObject", "JsonArray", "JsonString", "JsonNumber", "JsonBoolean", "JsonNull"); + for (final var type : jsonTypes) { + compilationUnits.add(new InMemoryJavaFileObject("java.util.json." + type, + "package java.util.json; public non-sealed interface " + type + " extends JsonValue {}")); + } + + // Internal implementation stubs + compilationUnits.add(new InMemoryJavaFileObject("jdk.internal.util.json.JsonObjectImpl", """ + package jdk.internal.util.json; + import java.util.Map; + import java.util.json.JsonObject; + import java.util.json.JsonValue; + public class JsonObjectImpl implements JsonObject { + public JsonObjectImpl(Map map) {} + public Map members() { return null; } + public boolean equals(Object obj) { return false; } + public int hashCode() { return 0; } + } + """)); + } + + /// In-memory JavaFileObject for creating stub classes + static class InMemoryJavaFileObject extends SimpleJavaFileObject { + private final String content; + + InMemoryJavaFileObject(String className, String content) { + super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); + this.content = content; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return content; + } + } + + /// Visitor to extract API information from AST + static class ApiExtractorVisitor extends TreePathScanner { + private final Map apiMap = new LinkedHashMap<>(); + private final Map methodsMap = new LinkedHashMap<>(); + private final Map fieldsMap = new LinkedHashMap<>(); + private final List constructors = new ArrayList<>(); + + JsonObject getExtractedApi() { + apiMap.put("methods", JsonObject.of(methodsMap)); + apiMap.put("fields", JsonObject.of(fieldsMap)); + apiMap.put("constructors", JsonArray.of(constructors)); + return JsonObject.of(apiMap); + } + + @Override + public Void visitClass(ClassTree node, Void p) { + // Basic class information + apiMap.put("className", JsonString.of(node.getSimpleName().toString())); + apiMap.put("modifiers", extractTreeModifiers(node.getModifiers())); + + // Type information + final var kind = node.getKind(); + apiMap.put("isInterface", JsonBoolean.of(kind == Tree.Kind.INTERFACE)); + apiMap.put("isEnum", JsonBoolean.of(kind == Tree.Kind.ENUM)); + apiMap.put("isRecord", JsonBoolean.of(kind == Tree.Kind.RECORD)); + + // Package name (from compilation unit) + final var compilationUnit = getCurrentPath().getCompilationUnit(); + final var packageTree = compilationUnit.getPackage(); + if (packageTree != null) { + apiMap.put("packageName", JsonString.of(packageTree.getPackageName().toString())); + } else { + apiMap.put("packageName", JsonString.of("")); + } + + // Check if sealed + final var modifiers = node.getModifiers(); + final var isSealed = modifiers.getFlags().stream() + .anyMatch(m -> m.toString().equals("SEALED")); + apiMap.put("isSealed", JsonBoolean.of(isSealed)); + + // Inheritance + final var superTypes = new ArrayList(); + if (node.getExtendsClause() != null) { + superTypes.add(JsonString.of(extractSimpleName(node.getExtendsClause().toString()))); + } + node.getImplementsClause().stream() + .map(tree -> JsonString.of(extractSimpleName(tree.toString()))) + .forEach(superTypes::add); + apiMap.put("extends", JsonArray.of(superTypes)); + + // Permitted subclasses (approximation - would need full symbol resolution) + if (isSealed) { + apiMap.put("permits", JsonArray.of(List.of())); + } + + return super.visitClass(node, p); + } + + @Override + public Void visitMethod(MethodTree node, Void p) { + // Check if public + final var isPublic = isPublicMember(node.getModifiers()); + + if (isPublic) { + final var methodInfo = new LinkedHashMap(); + methodInfo.put("modifiers", extractTreeModifiers(node.getModifiers())); + methodInfo.put("returnType", JsonString.of(extractSimpleName( + node.getReturnType() != null ? node.getReturnType().toString() : "void"))); + methodInfo.put("genericReturnType", JsonString.of( + node.getReturnType() != null ? node.getReturnType().toString() : "void")); + + final var params = node.getParameters().stream() + .map(param -> JsonString.of(extractSimpleName(param.getType().toString()) + " " + param.getName())) + .collect(Collectors.toList()); + methodInfo.put("parameters", JsonArray.of(params)); + + final var exceptions = node.getThrows().stream() + .map(ex -> JsonString.of(extractSimpleName(ex.toString()))) + .collect(Collectors.toList()); + methodInfo.put("throws", JsonArray.of(exceptions)); + + // Handle constructors separately + if (node.getName().toString().equals("")) { + constructors.add(JsonObject.of(methodInfo)); + } else { + methodsMap.put(node.getName().toString(), JsonObject.of(methodInfo)); + } + } + + return super.visitMethod(node, p); + } + + @Override + public Void visitVariable(VariableTree node, Void p) { + // Only process fields (not method parameters or local variables) + if (getCurrentPath().getParentPath().getLeaf().getKind() == Tree.Kind.CLASS) { + final var isPublic = isPublicMember(node.getModifiers()); + + if (isPublic) { + final var fieldInfo = new LinkedHashMap(); + fieldInfo.put("modifiers", extractTreeModifiers(node.getModifiers())); + fieldInfo.put("type", JsonString.of(extractSimpleName(node.getType().toString()))); + fieldInfo.put("genericType", JsonString.of(node.getType().toString())); + + fieldsMap.put(node.getName().toString(), JsonObject.of(fieldInfo)); + } + } + + return super.visitVariable(node, p); + } + + private JsonArray extractTreeModifiers(ModifiersTree modifiers) { + final var modList = modifiers.getFlags().stream() + .map(m -> JsonString.of(m.toString().toLowerCase())) + .collect(Collectors.toList()); + return JsonArray.of(modList); + } + + private boolean isPublicMember(ModifiersTree modifiers) { + // In interfaces, methods without private/default are implicitly public + final var parent = getCurrentPath().getParentPath(); + if (parent != null && parent.getLeaf().getKind() == Tree.Kind.INTERFACE) { + return !modifiers.getFlags().contains(javax.lang.model.element.Modifier.PRIVATE) && + !modifiers.getFlags().contains(javax.lang.model.element.Modifier.DEFAULT); + } + return modifiers.getFlags().contains(javax.lang.model.element.Modifier.PUBLIC); + } + + private String extractSimpleName(String typeName) { + // Remove generic parameters and package prefixes + var name = typeName; + final var genericIndex = name.indexOf('<'); + if (genericIndex >= 0) { + name = name.substring(0, genericIndex); + } + final var lastDot = name.lastIndexOf('.'); + if (lastDot >= 0) { + name = name.substring(lastDot + 1); + } + return name; + } + } + + /// Compares local and upstream APIs to identify differences + /// @param local the local API structure + /// @param upstream the upstream API structure + /// @return JSON object describing the differences + static JsonObject compareApis(JsonObject local, JsonObject upstream) { + Objects.requireNonNull(local, "local must not be null"); + Objects.requireNonNull(upstream, "upstream must not be null"); + + final var diffMap = new LinkedHashMap(); + final var className = ((JsonString) local.members().get("className")).value(); + + diffMap.put("className", JsonString.of(className)); + + // Check for upstream errors + if (upstream.members().containsKey("error")) { + diffMap.put("status", JsonString.of("UPSTREAM_ERROR")); + diffMap.put("error", upstream.members().get("error")); + return JsonObject.of(diffMap); + } + + // Check if status is NOT_IMPLEMENTED (from parsing) + if (upstream.members().containsKey("status")) { + final var status = ((JsonString) upstream.members().get("status")).value(); + if ("NOT_IMPLEMENTED".equals(status)) { + diffMap.put("status", JsonString.of("PARSE_NOT_IMPLEMENTED")); + return JsonObject.of(diffMap); + } + } + + // Perform detailed comparison + final var differences = new ArrayList(); + var hasChanges = false; + + // Compare basic class attributes + hasChanges |= compareAttribute("isInterface", local, upstream, differences); + hasChanges |= compareAttribute("isEnum", local, upstream, differences); + hasChanges |= compareAttribute("isRecord", local, upstream, differences); + hasChanges |= compareAttribute("isSealed", local, upstream, differences); + + // Compare modifiers + hasChanges |= compareModifiers(local, upstream, differences); + + // Compare inheritance + hasChanges |= compareInheritance(local, upstream, differences); + + // Compare methods + hasChanges |= compareMethods(local, upstream, differences); + + // Compare fields + hasChanges |= compareFields(local, upstream, differences); + + // Compare constructors + hasChanges |= compareConstructors(local, upstream, differences); + + // Set status based on findings + if (!hasChanges) { + diffMap.put("status", JsonString.of("MATCHING")); + } else { + diffMap.put("status", JsonString.of("DIFFERENT")); + diffMap.put("differences", JsonArray.of(differences)); + } + + return JsonObject.of(diffMap); + } + + /// Compares a simple boolean attribute + static boolean compareAttribute(String attrName, JsonObject local, JsonObject upstream, List differences) { + final var localValue = local.members().get(attrName); + final var upstreamValue = upstream.members().get(attrName); + + if (!Objects.equals(localValue, upstreamValue)) { + differences.add(JsonObject.of(Map.of( + "type", JsonString.of("attributeChanged"), + "attribute", JsonString.of(attrName), + "local", localValue != null ? localValue : JsonBoolean.of(false), + "upstream", upstreamValue != null ? upstreamValue : JsonBoolean.of(false) + ))); + return true; + } + return false; + } + + /// Compares class modifiers + static boolean compareModifiers(JsonObject local, JsonObject upstream, List differences) { + final var localMods = (JsonArray) local.members().get("modifiers"); + final var upstreamMods = (JsonArray) upstream.members().get("modifiers"); + + if (localMods == null || upstreamMods == null) { + return false; + } + + final var localSet = localMods.values().stream() + .map(v -> ((JsonString) v).value()) + .collect(Collectors.toSet()); + final var upstreamSet = upstreamMods.values().stream() + .map(v -> ((JsonString) v).value()) + .collect(Collectors.toSet()); + + if (!localSet.equals(upstreamSet)) { + differences.add(JsonObject.of(Map.of( + "type", JsonString.of("modifiersChanged"), + "local", localMods, + "upstream", upstreamMods + ))); + return true; + } + return false; + } + + /// Compares inheritance hierarchy + static boolean compareInheritance(JsonObject local, JsonObject upstream, List differences) { + final var localExtends = (JsonArray) local.members().get("extends"); + final var upstreamExtends = (JsonArray) upstream.members().get("extends"); + + if (localExtends == null || upstreamExtends == null) { + return false; + } + + final var localTypes = localExtends.values().stream() + .map(v -> normalizeTypeName(((JsonString) v).value())) + .collect(Collectors.toSet()); + final var upstreamTypes = upstreamExtends.values().stream() + .map(v -> normalizeTypeName(((JsonString) v).value())) + .collect(Collectors.toSet()); + + if (!localTypes.equals(upstreamTypes)) { + differences.add(JsonObject.of(Map.of( + "type", JsonString.of("inheritanceChanged"), + "local", localExtends, + "upstream", upstreamExtends + ))); + return true; + } + return false; + } + + /// Compares methods between local and upstream + static boolean compareMethods(JsonObject local, JsonObject upstream, List differences) { + final var localMethods = (JsonObject) local.members().get("methods"); + final var upstreamMethods = (JsonObject) upstream.members().get("methods"); + + if (localMethods == null || upstreamMethods == null) { + return false; + } + + var hasChanges = false; + + // Check for removed methods (in local but not upstream) + for (final var entry : localMethods.members().entrySet()) { + if (!upstreamMethods.members().containsKey(entry.getKey())) { + differences.add(JsonObject.of(Map.of( + "type", JsonString.of("methodRemoved"), + "method", JsonString.of(entry.getKey()), + "details", entry.getValue() + ))); + hasChanges = true; + } + } + + // Check for added methods (in upstream but not local) + for (final var entry : upstreamMethods.members().entrySet()) { + if (!localMethods.members().containsKey(entry.getKey())) { + differences.add(JsonObject.of(Map.of( + "type", JsonString.of("methodAdded"), + "method", JsonString.of(entry.getKey()), + "details", entry.getValue() + ))); + hasChanges = true; + } + } + + // Check for changed methods + for (final var entry : localMethods.members().entrySet()) { + final var methodName = entry.getKey(); + if (upstreamMethods.members().containsKey(methodName)) { + final var localMethod = (JsonObject) entry.getValue(); + final var upstreamMethod = (JsonObject) upstreamMethods.members().get(methodName); + + if (!compareMethodSignature(localMethod, upstreamMethod)) { + differences.add(JsonObject.of(Map.of( + "type", JsonString.of("methodChanged"), + "method", JsonString.of(methodName), + "local", localMethod, + "upstream", upstreamMethod + ))); + hasChanges = true; + } + } + } + + return hasChanges; + } + + /// Compares method signatures + static boolean compareMethodSignature(JsonObject localMethod, JsonObject upstreamMethod) { + // Compare return types + final var localReturn = normalizeTypeName(((JsonString) localMethod.members().get("returnType")).value()); + final var upstreamReturn = normalizeTypeName(((JsonString) upstreamMethod.members().get("returnType")).value()); + if (!localReturn.equals(upstreamReturn)) { + return false; + } + + // Compare parameters + final var localParams = (JsonArray) localMethod.members().get("parameters"); + final var upstreamParams = (JsonArray) upstreamMethod.members().get("parameters"); + + if (localParams.values().size() != upstreamParams.values().size()) { + return false; + } + + // Compare each parameter + for (int i = 0; i < localParams.values().size(); i++) { + final var localParam = normalizeTypeName(((JsonString) localParams.values().get(i)).value()); + final var upstreamParam = normalizeTypeName(((JsonString) upstreamParams.values().get(i)).value()); + if (!localParam.equals(upstreamParam)) { + return false; + } + } + + return true; + } + + /// Compares fields between local and upstream + static boolean compareFields(JsonObject local, JsonObject upstream, List differences) { + final var localFields = (JsonObject) local.members().get("fields"); + final var upstreamFields = (JsonObject) upstream.members().get("fields"); + + if (localFields == null || upstreamFields == null) { + return false; + } + + var hasChanges = false; + + // Check for field differences + final var localFieldNames = localFields.members().keySet(); + final var upstreamFieldNames = upstreamFields.members().keySet(); + + if (!localFieldNames.equals(upstreamFieldNames)) { + differences.add(JsonObject.of(Map.of( + "type", JsonString.of("fieldsChanged"), + "local", JsonArray.of(localFieldNames.stream().map(JsonString::of).collect(Collectors.toList())), + "upstream", JsonArray.of(upstreamFieldNames.stream().map(JsonString::of).collect(Collectors.toList())) + ))); + hasChanges = true; + } + + return hasChanges; + } + + /// Compares constructors between local and upstream + static boolean compareConstructors(JsonObject local, JsonObject upstream, List differences) { + final var localCtors = (JsonArray) local.members().get("constructors"); + final var upstreamCtors = (JsonArray) upstream.members().get("constructors"); + + if (localCtors == null || upstreamCtors == null) { + return false; + } + + if (localCtors.values().size() != upstreamCtors.values().size()) { + differences.add(JsonObject.of(Map.of( + "type", JsonString.of("constructorsChanged"), + "localCount", JsonNumber.of(localCtors.values().size()), + "upstreamCount", JsonNumber.of(upstreamCtors.values().size()) + ))); + return true; + } + + return false; + } + + /// Normalizes type names by removing package prefixes + static String normalizeTypeName(String typeName) { + // Handle generic types + var normalized = typeName; + + // Replace jdk.sandbox.* with standard packages + normalized = normalized.replace("jdk.sandbox.java.util.json", "java.util.json"); + normalized = normalized.replace("jdk.sandbox.internal.util.json", "jdk.internal.util.json"); + + // Remove any remaining package prefixes for comparison + if (normalized.contains(".")) { + final var parts = normalized.split("\\."); + normalized = parts[parts.length - 1]; + } + + return normalized; + } + + /// Runs a full comparison of local vs upstream APIs + /// @return complete comparison report as JSON + static JsonObject runFullComparison() { + LOGGER.info("Starting full API comparison"); + final var startTime = Instant.now(); + + final var reportMap = new LinkedHashMap(); + reportMap.put("timestamp", JsonString.of(startTime.toString())); + reportMap.put("localPackage", JsonString.of("jdk.sandbox.java.util.json")); + reportMap.put("upstreamPackage", JsonString.of("java.util.json")); + + // Discover local classes + final var localClasses = discoverLocalJsonClasses(); + LOGGER.info(() -> "Found " + localClasses.size() + " local classes"); + + // Fetch upstream sources + final var upstreamSources = fetchUpstreamSources(localClasses); + + // Extract and compare APIs + final var differences = new ArrayList(); + var matchingCount = 0; + var missingUpstream = 0; + var differentApi = 0; + + for (final var clazz : localClasses) { + final var localApi = extractLocalApi(clazz); + final var upstreamSource = upstreamSources.get(clazz.getName()); + final var upstreamApi = extractUpstreamApi(upstreamSource, clazz.getName()); + + final var diff = compareApis(localApi, upstreamApi); + differences.add(diff); + + // Count statistics + final var status = ((JsonString) diff.members().get("status")).value(); + switch (status) { + case "MATCHING" -> matchingCount++; + case "UPSTREAM_ERROR" -> missingUpstream++; + case "DIFFERENT" -> differentApi++; + } + } + + // Build summary + final var summary = JsonObject.of(Map.of( + "totalClasses", JsonNumber.of(localClasses.size()), + "matchingClasses", JsonNumber.of(matchingCount), + "missingUpstream", JsonNumber.of(missingUpstream), + "differentApi", JsonNumber.of(differentApi) + )); + + reportMap.put("summary", summary); + reportMap.put("differences", JsonArray.of(differences)); + + final var duration = Duration.between(startTime, Instant.now()); + reportMap.put("durationMs", JsonNumber.of(duration.toMillis())); + + LOGGER.info(() -> "Comparison completed in " + duration.toMillis() + "ms"); + + return JsonObject.of(reportMap); + } +} \ No newline at end of file diff --git a/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerRunner.java b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerRunner.java new file mode 100644 index 0000000..49f1427 --- /dev/null +++ b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerRunner.java @@ -0,0 +1,51 @@ +package io.github.simbo1905.tracker; + +import jdk.sandbox.java.util.json.Json; +import java.util.logging.ConsoleHandler; +import java.util.logging.Level; +import java.util.logging.Logger; + +/// Command-line runner for the API Tracker +/// +/// Usage: java io.github.simbo1905.tracker.ApiTrackerRunner [loglevel] +/// where loglevel is one of: SEVERE, WARNING, INFO, FINE, FINER, FINEST +public class ApiTrackerRunner { + + public static void main(String[] args) { + // Configure logging based on command line argument + final var logLevel = args.length > 0 ? Level.parse(args[0].toUpperCase()) : Level.INFO; + configureLogging(logLevel); + + System.out.println("=== JSON API Tracker ==="); + System.out.println("Comparing local jdk.sandbox.java.util.json with upstream java.util.json"); + System.out.println("Log level: " + logLevel); + System.out.println(); + + try { + // Run the full comparison + final var report = ApiTracker.runFullComparison(); + + // Pretty print the report + System.out.println("=== Comparison Report ==="); + System.out.println(Json.toDisplayString(report, 2)); + + } catch (Exception e) { + System.err.println("Error during comparison: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } + + private static void configureLogging(Level level) { + // Get root logger + final var rootLogger = Logger.getLogger(""); + rootLogger.setLevel(level); + + // Configure console handler + for (var handler : rootLogger.getHandlers()) { + if (handler instanceof ConsoleHandler) { + handler.setLevel(level); + } + } + } +} \ No newline at end of file diff --git a/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java b/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java new file mode 100644 index 0000000..bbba3ce --- /dev/null +++ b/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java @@ -0,0 +1,230 @@ +package io.github.simbo1905.tracker; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Nested; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jdk.sandbox.java.util.json.JsonBoolean; +import jdk.sandbox.java.util.json.JsonArray; +import jdk.sandbox.java.util.json.JsonObject; +import jdk.sandbox.java.util.json.JsonString; +import jdk.sandbox.java.util.json.JsonValue; + +import java.util.Set; +import java.util.Map; +import java.util.logging.Logger; +import java.util.logging.Level; + +public class ApiTrackerTest { + private static final Logger LOGGER = Logger.getLogger(ApiTrackerTest.class.getName()); + + @BeforeAll + static void setupLogging() { + LoggingControl.setupCleanLogging(); + } + + @Nested + @DisplayName("Local Class Discovery") + class LocalDiscoveryTests { + + @Test + @DisplayName("Should discover JSON API classes") + void testDiscoverLocalJsonClasses() { + final var classes = ApiTracker.discoverLocalJsonClasses(); + + assertThat(classes).isNotNull(); + assertThat(classes).isNotEmpty(); + + // Should find core JSON interfaces + assertThat(classes.stream().map(Class::getName)) + .contains( + "jdk.sandbox.java.util.json.JsonValue", + "jdk.sandbox.java.util.json.JsonObject", + "jdk.sandbox.java.util.json.JsonArray", + "jdk.sandbox.java.util.json.JsonString", + "jdk.sandbox.java.util.json.JsonNumber", + "jdk.sandbox.java.util.json.JsonBoolean", + "jdk.sandbox.java.util.json.JsonNull" + ); + + // Should also find internal implementation classes + assertThat(classes.stream().anyMatch(c -> c.getName().startsWith("jdk.sandbox.internal.util.json"))) + .as("Should find internal implementation classes") + .isTrue(); + + // Should be sorted + final var names = classes.stream().map(Class::getName).toList(); + final var sortedNames = names.stream().sorted().toList(); + assertThat(names).isEqualTo(sortedNames); + } + } + + @Nested + @DisplayName("Local API Extraction") + class LocalApiExtractionTests { + + @Test + @DisplayName("Should extract API from JsonObject interface") + void testExtractLocalApiJsonObject() throws ClassNotFoundException { + final var clazz = Class.forName("jdk.sandbox.java.util.json.JsonObject"); + final var api = ApiTracker.extractLocalApi(clazz); + + assertThat(api).isNotNull(); + assertThat(api.members()).containsKey("className"); + assertThat(((JsonString) api.members().get("className")).value()).isEqualTo("JsonObject"); + + assertThat(api.members()).containsKey("packageName"); + assertThat(((JsonString) api.members().get("packageName")).value()).isEqualTo("jdk.sandbox.java.util.json"); + + assertThat(api.members()).containsKey("isInterface"); + assertThat(((JsonValue) api.members().get("isInterface"))).isEqualTo(JsonBoolean.of(true)); + + assertThat(api.members()).containsKey("methods"); + final var methods = (JsonObject) api.members().get("methods"); + assertThat(methods.members()).containsKeys("members", "of"); + } + + @Test + @DisplayName("Should extract API from JsonValue sealed interface") + void testExtractLocalApiJsonValue() throws ClassNotFoundException { + final var clazz = Class.forName("jdk.sandbox.java.util.json.JsonValue"); + final var api = ApiTracker.extractLocalApi(clazz); + + assertThat(api.members()).containsKey("isSealed"); + assertThat(((JsonValue) api.members().get("isSealed"))).isEqualTo(JsonBoolean.of(true)); + + assertThat(api.members()).containsKey("permits"); + final var permits = (JsonArray) api.members().get("permits"); + assertThat(permits.values()).isNotEmpty(); + } + + @Test + @DisplayName("Should handle null class parameter") + void testExtractLocalApiNull() { + assertThatThrownBy(() -> ApiTracker.extractLocalApi(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("clazz must not be null"); + } + } + + @Nested + @DisplayName("Upstream Source Fetching") + class UpstreamFetchingTests { + + @Test + @DisplayName("Should map local class names to upstream paths") + void testMapToUpstreamPath() { + assertThat(ApiTracker.mapToUpstreamPath("jdk.sandbox.java.util.json.JsonObject")) + .isEqualTo("java/util/json/JsonObject.java"); + + assertThat(ApiTracker.mapToUpstreamPath("jdk.sandbox.internal.util.json.JsonObjectImpl")) + .isEqualTo("jdk/internal/util/json/JsonObjectImpl.java"); + } + + @Test + @DisplayName("Should handle null parameter in fetchUpstreamSources") + void testFetchUpstreamSourcesNull() { + assertThatThrownBy(() -> ApiTracker.fetchUpstreamSources(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("localClasses must not be null"); + } + + @Test + @DisplayName("Should return empty map for empty input") + void testFetchUpstreamSourcesEmpty() { + final var result = ApiTracker.fetchUpstreamSources(Set.of()); + assertThat(result).isEmpty(); + } + } + + @Nested + @DisplayName("API Comparison") + class ApiComparisonTests { + + @Test + @DisplayName("Should handle null parameters in compareApis") + void testCompareApisNull() { + final var dummyApi = JsonObject.of(Map.of("className", JsonString.of("Test"))); + + assertThatThrownBy(() -> ApiTracker.compareApis(null, dummyApi)) + .isInstanceOf(NullPointerException.class) + .hasMessage("local must not be null"); + + assertThatThrownBy(() -> ApiTracker.compareApis(dummyApi, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("upstream must not be null"); + } + + @Test + @DisplayName("Should handle upstream errors in comparison") + void testCompareApisUpstreamError() { + final var local = JsonObject.of(Map.of("className", JsonString.of("TestClass"))); + final var upstream = JsonObject.of(Map.of( + "error", JsonString.of("NOT_FOUND: File not found"), + "className", JsonString.of("TestClass") + )); + + final var result = ApiTracker.compareApis(local, upstream); + + assertThat(result.members()).containsKey("status"); + assertThat(((JsonString) result.members().get("status")).value()).isEqualTo("UPSTREAM_ERROR"); + assertThat(result.members()).containsKey("error"); + } + } + + @Nested + @DisplayName("Full Comparison Orchestration") + class FullComparisonTests { + + @Test + @DisplayName("Should run full comparison and return report structure") + void testRunFullComparison() { + final var report = ApiTracker.runFullComparison(); + + assertThat(report).isNotNull(); + assertThat(report.members()).containsKeys( + "timestamp", + "localPackage", + "upstreamPackage", + "summary", + "differences", + "durationMs" + ); + + final var summary = (JsonObject) report.members().get("summary"); + assertThat(summary.members()).containsKeys( + "totalClasses", + "matchingClasses", + "missingUpstream", + "differentApi" + ); + + // Total classes should be greater than 0 + final var totalClasses = ((JsonValue) summary.members().get("totalClasses")); + assertThat(totalClasses).isNotNull(); + } + } + + @Nested + @DisplayName("Modifier Extraction") + class ModifierExtractionTests { + + @Test + @DisplayName("Should extract modifiers correctly") + void testExtractModifiers() { + // Test public static final + final var modifiers = java.lang.reflect.Modifier.PUBLIC | + java.lang.reflect.Modifier.STATIC | + java.lang.reflect.Modifier.FINAL; + + final var result = ApiTracker.extractModifiers(modifiers); + + assertThat(result.values()).hasSize(3); + assertThat(result.values().stream().map(v -> ((JsonString) v).value())) + .containsExactlyInAnyOrder("public", "static", "final"); + } + } +} \ No newline at end of file diff --git a/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/CompilerApiLearningTest.java b/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/CompilerApiLearningTest.java new file mode 100644 index 0000000..76da6ea --- /dev/null +++ b/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/CompilerApiLearningTest.java @@ -0,0 +1,297 @@ +package io.github.simbo1905.tracker; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + +import javax.tools.JavaCompiler; +import javax.tools.ToolProvider; +import javax.tools.DiagnosticCollector; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.SimpleJavaFileObject; + +import com.sun.source.tree.*; +import com.sun.source.util.*; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import jdk.sandbox.java.util.json.Json; +import jdk.sandbox.java.util.json.JsonArray; +import jdk.sandbox.java.util.json.JsonObject; +import jdk.sandbox.java.util.json.JsonString; +import jdk.sandbox.java.util.json.JsonValue; +import javax.lang.model.element.Modifier; +import java.util.logging.Logger; +import java.util.logging.Level; + +public class CompilerApiLearningTest { + + /// In-memory JavaFileObject for creating stub classes + static class InMemoryJavaFileObject extends SimpleJavaFileObject { + private final String content; + + InMemoryJavaFileObject(String className, String content) { + super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); + this.content = content; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return content; + } + } + private static final Logger LOGGER = Logger.getLogger(CompilerApiLearningTest.class.getName()); + private static final String JSON_OBJECT_SOURCE_PATH = "src/test/resources/JsonObject.java"; + + @BeforeAll + static void setupLogging() { + LoggingControl.setupCleanLogging(); + } + + @Test + @DisplayName("Test 1: Source-Level Analysis with JavaParser API (Parse-Only)") + void testSourceLevelAnalysisWithJavaParser() throws IOException { + Instant start = Instant.now(); + LOGGER.info("\n--- Running Test 1: Source-Level Analysis with JavaParser API (Parse-Only) ---"); + + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + assertThat(compiler).as("JavaCompiler should be available").isNotNull(); + + DiagnosticCollector diagnostics = new DiagnosticCollector<>(); + StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, StandardCharsets.UTF_8); + + File jsonObjectFile = new File(JSON_OBJECT_SOURCE_PATH); + LOGGER.fine("JsonObject file path: " + jsonObjectFile.getAbsolutePath()); + LOGGER.fine("JsonObject file exists: " + jsonObjectFile.exists()); + LOGGER.fine("JsonObject file canonical path: " + jsonObjectFile.getCanonicalPath()); + + Iterable compilationUnits = fileManager.getJavaFileObjectsFromFiles(Collections.singletonList(jsonObjectFile)); + + // Create stub sources for internal dependencies + List allCompilationUnits = new ArrayList<>((List) compilationUnits); + + // Add stub for PreviewFeature annotation + String previewFeatureStub = """ + package jdk.internal.javac; + import java.lang.annotation.*; + @Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + public @interface PreviewFeature { + Feature feature(); + enum Feature { JSON } + } + """; + allCompilationUnits.add(new InMemoryJavaFileObject("jdk.internal.javac.PreviewFeature", previewFeatureStub)); + + // Add stub for JsonObjectImpl + String jsonObjectImplStub = """ + package jdk.internal.util.json; + import java.util.Map; + import java.util.json.JsonObject; + import java.util.json.JsonValue; + public class JsonObjectImpl implements JsonObject { + public JsonObjectImpl(Map map) {} + public Map members() { return null; } + public boolean equals(Object obj) { return false; } + public int hashCode() { return 0; } + } + """; + allCompilationUnits.add(new InMemoryJavaFileObject("jdk.internal.util.json.JsonObjectImpl", jsonObjectImplStub)); + + // Add stub for JsonValue interface (parent of JsonObject) + String jsonValueStub = """ + package java.util.json; + public sealed interface JsonValue permits JsonObject, JsonArray, JsonString, JsonNumber, JsonBoolean, JsonNull {} + """; + allCompilationUnits.add(new InMemoryJavaFileObject("java.util.json.JsonValue", jsonValueStub)); + + // Add minimal stubs for other JSON types referenced in permits clause + String[] jsonTypes = {"JsonArray", "JsonString", "JsonNumber", "JsonBoolean", "JsonNull"}; + for (String type : jsonTypes) { + String stub = "package java.util.json; public non-sealed interface " + type + " extends JsonValue {}"; + allCompilationUnits.add(new InMemoryJavaFileObject("java.util.json." + type, stub)); + } + + // Create a CompilationTask in parse-only mode with relaxed compilation + List options = List.of( + "-proc:none", // Disable annotation processing + "-XDignore.symbol.file", // Ignore internal API restrictions + "-Xlint:none", // Disable all warnings + "--enable-preview", // Enable preview features + "--release", "24" // Target Java 24 + ); + + JavacTask task = (JavacTask) compiler.getTask( + null, // no output writer + fileManager, + diagnostics, + options, + null, // no classes + allCompilationUnits); + + try { + Iterable trees = task.parse(); + assertThat(trees).as("Should parse at least one compilation unit").isNotEmpty(); + + JsonObject extractedApi = null; + + for (CompilationUnitTree tree : trees) { + String fileName = tree.getSourceFile().getName(); + LOGGER.info("Parsed Compilation Unit: " + fileName); + + // Only process the JsonObject.java file, skip stub files + if (!fileName.endsWith("JsonObject.java")) { + continue; + } + + // Visitor to extract API information + ApiExtractorVisitor visitor = new ApiExtractorVisitor(Trees.instance(task)); + visitor.scan(tree, null); + + extractedApi = visitor.getExtractedApi(); + LOGGER.info("Extracted API: " + Json.toDisplayString(extractedApi, 2)); + LOGGER.fine("Raw extracted API map keys: " + extractedApi.members().keySet()); + LOGGER.finer("Full extracted API: " + extractedApi.members()); + } + + assertThat(extractedApi).as("Should have extracted API from JsonObject.java").isNotNull(); + + // Basic assertions for expected content + JsonString className = (JsonString) extractedApi.members().get("className"); + assertThat(className.value()).isEqualTo("JsonObject"); + + JsonArray modifiers = (JsonArray) extractedApi.members().get("modifiers"); + assertThat(modifiers).as("modifiers should be present").isNotNull(); + Set modifierStrings = modifiers.values().stream() + .map(v -> ((JsonString) v).value()) + .collect(Collectors.toSet()); + assertThat(modifierStrings).contains("public"); + + JsonArray extendsList = (JsonArray) extractedApi.members().get("extends"); + assertThat(extendsList).as("extends should be present").isNotNull(); + Set extendsStrings = extendsList.values().stream() + .map(v -> ((JsonString) v).value()) + .collect(Collectors.toSet()); + assertThat(extendsStrings).contains("JsonValue"); + + JsonObject methods = (JsonObject) extractedApi.members().get("methods"); + assertThat(methods).as("methods should be present").isNotNull(); + assertThat(methods.members()).containsKey("members"); + assertThat(methods.members()).containsKey("of"); + assertThat(methods.members()).containsKey("equals"); + assertThat(methods.members()).containsKey("hashCode"); + + // Log diagnostics (errors/warnings from parsing) + diagnostics.getDiagnostics().forEach(d -> LOGGER.warning("Diagnostic: " + d)); + + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error during parsing: " + e.getMessage(), e); + throw e; // Re-throw to fail the test + } finally { + fileManager.close(); + Instant end = Instant.now(); + LOGGER.info("Test 1 finished in: " + Duration.between(start, end).toMillis() + " ms"); + } + } + + // Helper class to extract API information from the AST + static class ApiExtractorVisitor extends TreePathScanner { + private final Map classMap = new LinkedHashMap<>(); + private final Map methodsMap = new LinkedHashMap<>(); + + public ApiExtractorVisitor(Trees trees) { + // Trees parameter kept for future use if needed + } + + public JsonObject getExtractedApi() { + classMap.put("methods", JsonObject.of(methodsMap)); + return JsonObject.of(classMap); + } + + @Override + public Void visitCompilationUnit(CompilationUnitTree node, Void p) { + LOGGER.fine("Visiting compilation unit with " + node.getTypeDecls().size() + " type declarations"); + for (Tree member : node.getTypeDecls()) { + LOGGER.fine("Type declaration kind: " + member.getKind()); + scan(member, p); + } + return super.visitCompilationUnit(node, p); + } + + @Override + public Void visitClass(ClassTree node, Void p) { + LOGGER.fine("Visiting class: " + node.getSimpleName()); + LOGGER.finer("Class kind: " + node.getKind()); + LOGGER.finer("Number of members: " + node.getMembers().size()); + + classMap.put("className", JsonString.of(node.getSimpleName().toString())); + classMap.put("modifiers", JsonArray.of(node.getModifiers().getFlags().stream() + .map(Object::toString) + .map(JsonString::of) + .collect(Collectors.toList()))); + + List extendsList = new ArrayList<>(); + if (node.getExtendsClause() != null) { + extendsList.add(JsonString.of(node.getExtendsClause().toString())); + } + if (!node.getImplementsClause().isEmpty()) { + node.getImplementsClause().forEach(impl -> extendsList.add(JsonString.of(impl.toString()))); + } + classMap.put("extends", JsonArray.of(extendsList)); + + // Log members + for (Tree member : node.getMembers()) { + LOGGER.finer("Member kind: " + member.getKind() + ", toString: " + member.toString().substring(0, Math.min(50, member.toString().length()))); + } + + return super.visitClass(node, p); + } + + @Override + public Void visitMethod(MethodTree node, Void p) { + LOGGER.finer("Visiting method: " + node.getName() + ", modifiers: " + node.getModifiers().getFlags()); + + // In interfaces, methods without private/default modifiers are implicitly public + boolean isPublic = node.getModifiers().getFlags().contains(Modifier.PUBLIC) || + (getCurrentPath().getParentPath() != null && + getCurrentPath().getParentPath().getLeaf().getKind() == Tree.Kind.INTERFACE && + !node.getModifiers().getFlags().contains(Modifier.PRIVATE) && + !node.getModifiers().getFlags().contains(Modifier.DEFAULT)); + + if (isPublic) { + LOGGER.fine("Processing public method: " + node.getName()); + Map methodMap = new LinkedHashMap<>(); + methodMap.put("modifiers", JsonArray.of(node.getModifiers().getFlags().stream() + .map(Object::toString) + .map(JsonString::of) + .collect(Collectors.toList()))); + methodMap.put("returnType", JsonString.of(node.getReturnType() != null ? node.getReturnType().toString() : "void")); + + List parameters = new ArrayList<>(); + node.getParameters().forEach(param -> parameters.add(JsonString.of(param.getType() + " " + param.getName()))); + methodMap.put("parameters", JsonArray.of(parameters)); + + List throwsList = new ArrayList<>(); + node.getThrows().forEach(throwable -> throwsList.add(JsonString.of(throwable.toString()))); + methodMap.put("throws", JsonArray.of(throwsList)); + + methodsMap.put(node.getName().toString(), JsonObject.of(methodMap)); + } + return super.visitMethod(node, p); + } + } +} diff --git a/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/LoggingControl.java b/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/LoggingControl.java new file mode 100644 index 0000000..d54522a --- /dev/null +++ b/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/LoggingControl.java @@ -0,0 +1,52 @@ +package io.github.simbo1905.tracker; + +import java.util.logging.*; + +/// Modern Java companion object pattern for test logging configuration. +/// Uses sealed interface with config record to avoid singleton anti-patterns. +/// Provides clean, compact output instead of JUL's ugly two-line default format. +public sealed interface LoggingControl permits LoggingControl.Config { + + /// Configuration record for logging setup + record Config(Level defaultLevel) implements LoggingControl { + } + + /// Set up clean, compact logging format for tests using functional style. + /// No instances, no singletons - just clean configuration via records and default methods. + static void setupCleanLogging(Config config) { + // Allow CLI override via -Djava.util.logging.ConsoleHandler.level=FINER + String logLevel = System.getProperty("java.util.logging.ConsoleHandler.level"); + Level level = (logLevel != null) ? Level.parse(logLevel) : config.defaultLevel(); + + // Get the root logger to configure globally + Logger rootLogger = Logger.getLogger(""); + + // Remove default handlers to prevent ugly JUL formatting + for (Handler handler : rootLogger.getHandlers()) { + rootLogger.removeHandler(handler); + } + + // Create console handler with clean formatting + ConsoleHandler consoleHandler = new ConsoleHandler(); + consoleHandler.setLevel(level); + + // Custom formatter for compact single-line output (saves tokens and money) + consoleHandler.setFormatter(new Formatter() { + @Override + public String format(LogRecord record) { + return record.getMessage() + "\n"; + } + }); + + rootLogger.addHandler(consoleHandler); + rootLogger.setLevel(level); + } + + /// Convenience method with default WARNING level + static void setupCleanLogging() { + Level level = System.getProperty("java.util.logging.ConsoleHandler.level") != null + ? Level.parse(System.getProperty("java.util.logging.ConsoleHandler.level")) + : Level.WARNING; + setupCleanLogging(new Config(level)); + } +} \ No newline at end of file diff --git a/json-java21-api-tracker/src/test/resources/JsonObject.java b/json-java21-api-tracker/src/test/resources/JsonObject.java new file mode 100644 index 0000000..ad98b40 --- /dev/null +++ b/json-java21-api-tracker/src/test/resources/JsonObject.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.util.json; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import jdk.internal.javac.PreviewFeature; +import jdk.internal.util.json.JsonObjectImpl; + +/** + * The interface that represents JSON object. + * + *

+ * A {@code JsonObject} can be produced by a {@link Json#parse(String)}. + * + * Alternatively, {@link #of(Map)} can be used to obtain a {@code JsonObject}. + * Implementations of {@code JsonObject} cannot be created from sources that + * contain duplicate member names. If duplicate names appear during + * a {@link Json#parse(String)}, a {@code JsonParseException} is thrown. + * + * @since 99 + */ +@PreviewFeature(feature = PreviewFeature.Feature.JSON) +public non-sealed interface JsonObject extends JsonValue { + + /** + * {@return an unmodifiable map of the {@code String} to {@code JsonValue} + * members in this {@code JsonObject}} + */ + Map members(); + + /** + * {@return the {@code JsonObject} created from the given + * map of {@code String} to {@code JsonValue}s} + * + * The {@code JsonObject}'s members occur in the same order as the given + * map's entries. + * + * @param map the map of {@code JsonValue}s. Non-null. + * @throws NullPointerException if {@code map} is {@code null}, contains + * any keys that are {@code null}, or contains any values that are {@code null}. + */ + static JsonObject of(Map map) { + return new JsonObjectImpl(map.entrySet() // Implicit NPE on map + .stream() + .collect(Collectors.toMap( + e -> Objects.requireNonNull(e.getKey()), + Map.Entry::getValue, // Implicit NPE on val + (_, v) -> v, + LinkedHashMap::new))); + } + + /** + * {@return {@code true} if the given object is also a {@code JsonObject} + * and the two {@code JsonObject}s represent the same mappings} Two + * {@code JsonObject}s {@code jo1} and {@code jo2} represent the same + * mappings if {@code jo1.members().equals(jo2.members())}. + * + * @see #members() + */ + @Override + boolean equals(Object obj); + + /** + * {@return the hash code value for this {@code JsonObject}} The hash code value + * of a {@code JsonObject} is derived from the hash code of {@code JsonObject}'s + * {@link #members()}. Thus, for two {@code JsonObject}s {@code jo1} and {@code jo2}, + * {@code jo1.equals(jo2)} implies that {@code jo1.hashCode() == jo2.hashCode()} + * as required by the general contract of {@link Object#hashCode}. + * + * @see #members() + */ + @Override + int hashCode(); +} diff --git a/mvn-test-no-boilerplate.sh b/mvn-test-no-boilerplate.sh new file mode 100755 index 0000000..4142448 --- /dev/null +++ b/mvn-test-no-boilerplate.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +# Strip Maven test boilerplate - show compile errors and test results only +# Usage: ./mvn-test-no-boilerplate.sh [maven test arguments] +# +# Examples: +# ./mvn-test-no-boilerplate.sh -Dtest=RefactorTests +# ./mvn-test-no-boilerplate.sh -Dtest=RefactorTests#testList -Djava.util.logging.ConsoleHandler.level=INFO +# ./mvn-test-no-boilerplate.sh -Dtest=RefactorTests#testList -Djava.util.logging.ConsoleHandler.level=FINER +# +# For running tests in a specific module: +# ./mvn-test-no-boilerplate.sh -pl json-java21-api-tracker -Dtest=CompilerApiLearningTest +# +# The script automatically detects if mvnd is available, otherwise falls back to mvn + +# Detect if mvnd is available, otherwise use mvn +if command -v mvnd &> /dev/null; then + MVN_CMD="mvnd" +else + MVN_CMD="mvn" +fi + +$MVN_CMD test "$@" 2>&1 | awk ' +BEGIN { + scanning_started = 0 + compilation_section = 0 + test_section = 0 +} + +# Skip all WARNING lines before project scanning starts +/INFO.*Scanning for projects/ { + scanning_started = 1 + print + next +} + +# Before scanning starts, skip WARNING lines +!scanning_started && /^WARNING:/ { next } + +# Show compilation errors +/COMPILATION ERROR/ { compilation_section = 1 } +/BUILD FAILURE/ && compilation_section { compilation_section = 0 } + +# Show test section +/INFO.*T E S T S/ { + test_section = 1 + print "-------------------------------------------------------" + print " T E S T S" + print "-------------------------------------------------------" + next +} + +# In compilation error section, show everything +compilation_section { print } + +# In test section, show everything - let user control logging with -D arguments +test_section { + print +} + +# Before test section starts, show important lines only +!test_section && scanning_started { + if (/INFO.*Scanning|INFO.*Building|INFO.*resources|INFO.*compiler|INFO.*surefire|ERROR|FAILURE/) { + print + } + # Show compilation warnings/errors + if (/WARNING.*COMPILATION|ERROR.*/) { + print + } +} +' \ No newline at end of file From ef442f7b49050f44d1b2aa13e2bebf9d9427662a Mon Sep 17 00:00:00 2001 From: Simon Massey Date: Sat, 26 Jul 2025 08:24:36 +0100 Subject: [PATCH 4/6] fix: Address PR review comments - Remove duplicate ApiTrackerMain class (keep only ApiTrackerRunner) - Replace hardcoded class list with dynamic classpath scanning - Change logging from FINE to INFO for key operations - Remove hardcoded version references The API tracker now dynamically discovers classes in the JSON API packages by scanning both directories and JAR files on the classpath. This makes it more robust and eliminates the need to manually update class lists. --- .../github/simbo1905/tracker/ApiTracker.java | 136 ++++++++++---- .../simbo1905/tracker/ApiTrackerMain.java | 167 ------------------ 2 files changed, 101 insertions(+), 202 deletions(-) delete mode 100644 json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerMain.java diff --git a/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java index 8fd6f61..b43362b 100644 --- a/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java +++ b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java @@ -79,45 +79,111 @@ enum Nothing implements ApiTracker {} /// Discovers all classes in the local JSON API packages /// @return sorted set of classes from jdk.sandbox.java.util.json and jdk.sandbox.internal.util.json static Set> discoverLocalJsonClasses() { - LOGGER.fine("Discovering local JSON classes"); + LOGGER.info("Starting class discovery for JSON API packages"); final var classes = new TreeSet>((a, b) -> a.getName().compareTo(b.getName())); - // Known public API classes - final var publicApiClasses = List.of( - "jdk.sandbox.java.util.json.Json", - "jdk.sandbox.java.util.json.JsonValue", - "jdk.sandbox.java.util.json.JsonObject", - "jdk.sandbox.java.util.json.JsonArray", - "jdk.sandbox.java.util.json.JsonString", - "jdk.sandbox.java.util.json.JsonNumber", - "jdk.sandbox.java.util.json.JsonBoolean", - "jdk.sandbox.java.util.json.JsonNull", - "jdk.sandbox.java.util.json.JsonParseException" + // Packages to scan + final var packages = List.of( + "jdk.sandbox.java.util.json", + "jdk.sandbox.internal.util.json" ); - // Known internal implementation classes - final var internalClasses = List.of( - "jdk.sandbox.internal.util.json.JsonParser", - "jdk.sandbox.internal.util.json.JsonObjectImpl", - "jdk.sandbox.internal.util.json.JsonArrayImpl", - "jdk.sandbox.internal.util.json.JsonStringImpl", - "jdk.sandbox.internal.util.json.JsonNumberImpl" - ); + final var classLoader = Thread.currentThread().getContextClassLoader(); + + for (final var packageName : packages) { + try { + final var path = packageName.replace('.', '/'); + final var resources = classLoader.getResources(path); + + while (resources.hasMoreElements()) { + final var url = resources.nextElement(); + LOGGER.fine(() -> "Scanning resource: " + url); + + if ("file".equals(url.getProtocol())) { + // Handle directory scanning + scanDirectory(new java.io.File(url.toURI()), packageName, classes); + } else if ("jar".equals(url.getProtocol())) { + // Handle JAR scanning + scanJar(url, packageName, classes); + } + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Error scanning package: " + packageName, e); + } + } - // Load all known classes - Stream.concat(publicApiClasses.stream(), internalClasses.stream()) - .forEach(className -> { + LOGGER.info("Discovered " + classes.size() + " classes in JSON API packages"); + return Collections.unmodifiableSet(classes); + } + + /// Scans a directory for class files + static void scanDirectory(java.io.File directory, String packageName, Set> classes) { + if (!directory.exists() || !directory.isDirectory()) { + return; + } + + final var files = directory.listFiles(); + if (files == null) { + return; + } + + for (final var file : files) { + if (file.isDirectory()) { + scanDirectory(file, packageName + "." + file.getName(), classes); + } else if (file.getName().endsWith(".class") && !file.getName().contains("$")) { + final var className = packageName + '.' + + file.getName().substring(0, file.getName().length() - 6); try { final var clazz = Class.forName(className); classes.add(clazz); - LOGGER.finer(() -> "Loaded class: " + className); - } catch (ClassNotFoundException e) { - LOGGER.fine(() -> "Class not found (might not exist yet): " + className); + LOGGER.fine(() -> "Found class: " + className); + } catch (ClassNotFoundException | NoClassDefFoundError e) { + LOGGER.fine(() -> "Could not load class: " + className); } - }); - - LOGGER.fine(() -> "Discovered " + classes.size() + " classes"); - return Collections.unmodifiableSet(classes); + } + } + } + + /// Scans a JAR file for classes in the specified package + static void scanJar(java.net.URL jarUrl, String packageName, Set> classes) { + try { + final var jarPath = jarUrl.getPath(); + final var exclamation = jarPath.indexOf('!'); + if (exclamation < 0) { + return; + } + + final var jarFilePath = jarPath.substring(5, exclamation); // Remove "file:" + final var packagePath = packageName.replace('.', '/'); + + try (final var jarFile = new java.util.jar.JarFile(jarFilePath)) { + final var entries = jarFile.entries(); + + while (entries.hasMoreElements()) { + final var entry = entries.nextElement(); + final var entryName = entry.getName(); + + if (entryName.startsWith(packagePath) && + entryName.endsWith(".class") && + !entryName.contains("$")) { + + final var className = entryName + .substring(0, entryName.length() - 6) + .replace('/', '.'); + + try { + final var clazz = Class.forName(className); + classes.add(clazz); + LOGGER.fine(() -> "Found class in JAR: " + className); + } catch (ClassNotFoundException | NoClassDefFoundError e) { + LOGGER.fine(() -> "Could not load class from JAR: " + className); + } + } + } + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Error scanning JAR: " + jarUrl, e); + } } /// Fetches upstream source files from GitHub for the given local classes @@ -125,7 +191,7 @@ static Set> discoverLocalJsonClasses() { /// @return map of className to source code (or error message if fetch failed) static Map fetchUpstreamSources(Set> localClasses) { Objects.requireNonNull(localClasses, "localClasses must not be null"); - LOGGER.fine(() -> "Fetching upstream sources for " + localClasses.size() + " classes"); + LOGGER.info("Fetching upstream sources for " + localClasses.size() + " classes"); final var results = new LinkedHashMap(); final var httpClient = HttpClient.newBuilder() @@ -197,7 +263,7 @@ static String mapToUpstreamPath(String className) { /// @return JSON representation of the class's public API static JsonObject extractLocalApi(Class clazz) { Objects.requireNonNull(clazz, "clazz must not be null"); - LOGGER.fine(() -> "Extracting local API for: " + clazz.getName()); + LOGGER.info("Extracting local API for: " + clazz.getName()); final var apiMap = new LinkedHashMap(); @@ -350,7 +416,7 @@ static JsonObject extractUpstreamApi(String sourceCode, String className) { return JsonObject.of(errorMap); } - LOGGER.fine(() -> "Extracting upstream API for: " + className); + LOGGER.info("Extracting upstream API for: " + className); final var compiler = ToolProvider.getSystemJavaCompiler(); if (compiler == null) { @@ -927,7 +993,7 @@ static JsonObject runFullComparison() { // Discover local classes final var localClasses = discoverLocalJsonClasses(); - LOGGER.info(() -> "Found " + localClasses.size() + " local classes"); + LOGGER.info("Found " + localClasses.size() + " local classes"); // Fetch upstream sources final var upstreamSources = fetchUpstreamSources(localClasses); @@ -969,7 +1035,7 @@ static JsonObject runFullComparison() { final var duration = Duration.between(startTime, Instant.now()); reportMap.put("durationMs", JsonNumber.of(duration.toMillis())); - LOGGER.info(() -> "Comparison completed in " + duration.toMillis() + "ms"); + LOGGER.info("Comparison completed in " + duration.toMillis() + "ms"); return JsonObject.of(reportMap); } diff --git a/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerMain.java b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerMain.java deleted file mode 100644 index c9c02c8..0000000 --- a/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerMain.java +++ /dev/null @@ -1,167 +0,0 @@ -package io.github.simbo1905.tracker; - -import jdk.sandbox.java.util.json.Json; -import jdk.sandbox.java.util.json.JsonArray; -import jdk.sandbox.java.util.json.JsonBoolean; -import jdk.sandbox.java.util.json.JsonObject; -import jdk.sandbox.java.util.json.JsonString; -import jdk.sandbox.java.util.json.JsonValue; -import jdk.sandbox.java.util.json.JsonParseException; - -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.logging.ConsoleHandler; -import java.util.logging.SimpleFormatter; - -/** - * Main entry point for the API Tracker tool. - * - * This tool analyzes Java API structures and tracks changes between - * the OpenJDK sandbox java.util.json implementation and this backport. - */ -public class ApiTrackerMain { - private static final Logger LOGGER = Logger.getLogger(ApiTrackerMain.class.getName()); - - static { - // Configure logging with a clean formatter - Logger rootLogger = Logger.getLogger(""); - rootLogger.setLevel(Level.INFO); - - // Remove default handlers - for (var handler : rootLogger.getHandlers()) { - rootLogger.removeHandler(handler); - } - - // Add console handler with simple formatting - ConsoleHandler consoleHandler = new ConsoleHandler(); - consoleHandler.setLevel(Level.ALL); - consoleHandler.setFormatter(new SimpleFormatter() { - @Override - public synchronized String format(java.util.logging.LogRecord record) { - return String.format("[%s] %s - %s%n", - record.getLevel().getName(), - record.getLoggerName(), - formatMessage(record) - ); - } - }); - rootLogger.addHandler(consoleHandler); - } - - public static void main(String[] args) { - LOGGER.info("Starting API Tracker v0.1-SNAPSHOT"); - - // Validate our JSON parsing works correctly - if (!validateJsonBackport()) { - LOGGER.severe("JSON backport validation failed"); - System.exit(1); - } - - LOGGER.info("JSON backport validation successful"); - - // TODO: Implement API analysis logic - LOGGER.info("API Tracker initialized successfully"); - } - - /** - * Validates that the JSON backport is working correctly. - * Tests various JSON structures to ensure compatibility. - */ - private static boolean validateJsonBackport() { - try { - // Test complex JSON structure - String testJson = """ - { - "apiTracker": { - "version": "0.1-SNAPSHOT", - "modules": { - "core": "java-util-json-java21", - "tracker": "java-util-json-java21-api-tracker" - }, - "features": [ - "API extraction", - "Structural comparison", - "GitHub integration" - ], - "config": { - "autoTrack": true, - "createIssues": true, - "trackInterval": 86400 - } - } - } - """; - - LOGGER.fine("Parsing test JSON structure"); - JsonValue parsedValue = Json.parse(testJson); - - if (!(parsedValue instanceof JsonObject root)) { - LOGGER.severe("Expected JsonObject but got: " + parsedValue.getClass().getName()); - return false; - } - - // Navigate the structure to validate parsing - JsonObject apiTracker = (JsonObject) root.members().get("apiTracker"); - String version = ((JsonString) apiTracker.members().get("version")).value(); - - if (!"0.1-SNAPSHOT".equals(version)) { - LOGGER.severe("Version mismatch: expected 0.1-SNAPSHOT, got " + version); - return false; - } - - // Validate nested objects - JsonObject modules = (JsonObject) apiTracker.members().get("modules"); - LOGGER.fine("Found modules: " + modules.members().size()); - - // Validate array handling - JsonArray features = (JsonArray) apiTracker.members().get("features"); - LOGGER.fine("Found features: " + features.values().size()); - - // Validate boolean handling - JsonObject config = (JsonObject) apiTracker.members().get("config"); - boolean autoTrack = ((JsonBoolean) config.members().get("autoTrack")).value(); - - if (!autoTrack) { - LOGGER.warning("autoTrack is disabled in test configuration"); - } - - // Test edge cases - testEdgeCases(); - - LOGGER.info("All JSON validation tests passed"); - return true; - - } catch (JsonParseException e) { - LOGGER.log(Level.SEVERE, "JSON parsing failed", e); - return false; - } catch (ClassCastException e) { - LOGGER.log(Level.SEVERE, "Unexpected JSON structure", e); - return false; - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Unexpected error during validation", e); - return false; - } - } - - /** - * Tests edge cases for JSON parsing. - */ - private static void testEdgeCases() throws JsonParseException { - // Empty object - Json.parse("{}"); - - // Empty array - Json.parse("[]"); - - // Null value - Json.parse("{\"key\": null}"); - - // Unicode handling - Json.parse("{\"unicode\": \"Hello \\u4e16\\u754c\"}"); - - // Number formats - Json.parse("{\"int\": 42, \"float\": 3.14, \"exp\": 1.23e-4}"); - - LOGGER.fine("Edge case tests completed"); - } -} \ No newline at end of file From 7a3c0fe49b847419112878ad847ced4ba59c9934 Mon Sep 17 00:00:00 2001 From: Simon Massey Date: Sat, 26 Jul 2025 10:57:07 +0100 Subject: [PATCH 5/6] fix: Address PR review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update both GitHub workflows to use Java 24 instead of matrix build - Add comprehensive INFO logging for HTTP operations and class discovery - Remove scaffolding test and fix compilation warnings - Maintain Java 21 compatibility for main library while using Java 24 for tooling 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .github/workflows/daily-api-tracker.yml | 4 +- .github/workflows/maven.yml | 9 +- .../github/simbo1905/tracker/ApiTracker.java | 17 +- .../simbo1905/tracker/ApiTrackerTest.java | 6 +- .../tracker/CompilerApiLearningTest.java | 297 ------------------ 5 files changed, 17 insertions(+), 316 deletions(-) delete mode 100644 json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/CompilerApiLearningTest.java diff --git a/.github/workflows/daily-api-tracker.yml b/.github/workflows/daily-api-tracker.yml index 121f14c..5541a76 100644 --- a/.github/workflows/daily-api-tracker.yml +++ b/.github/workflows/daily-api-tracker.yml @@ -14,10 +14,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Set up JDK 21 + - name: Set up JDK 24 uses: actions/setup-java@v4 with: - java-version: '21' + java-version: '24' distribution: 'temurin' - name: Cache Maven dependencies diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 671b5ba..e7d3ea5 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -10,18 +10,15 @@ on: jobs: build: runs-on: ubuntu-latest - strategy: - matrix: - java: [ '21', '22', '23', '24' ] - name: Build with JDK ${{ matrix.java }} + name: Build with JDK 24 steps: - uses: actions/checkout@v4 - - name: Set up JDK ${{ matrix.java }} + - name: Set up JDK 24 uses: actions/setup-java@v4 with: - java-version: ${{ matrix.java }} + java-version: '24' distribution: 'oracle' - name: Cache Maven dependencies diff --git a/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java index b43362b..594dbce 100644 --- a/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java +++ b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java @@ -112,7 +112,8 @@ static Set> discoverLocalJsonClasses() { } } - LOGGER.info("Discovered " + classes.size() + " classes in JSON API packages"); + LOGGER.info("Discovered " + classes.size() + " classes in JSON API packages: " + + classes.stream().map(Class::getName).sorted().collect(Collectors.joining(", "))); return Collections.unmodifiableSet(classes); } @@ -136,7 +137,7 @@ static void scanDirectory(java.io.File directory, String packageName, Set "Found class: " + className); + LOGGER.info("Found class: " + className); } catch (ClassNotFoundException | NoClassDefFoundError e) { LOGGER.fine(() -> "Could not load class: " + className); } @@ -174,7 +175,7 @@ static void scanJar(java.net.URL jarUrl, String packageName, Set> class try { final var clazz = Class.forName(className); classes.add(clazz); - LOGGER.fine(() -> "Found class in JAR: " + className); + LOGGER.info("Found class in JAR: " + className); } catch (ClassNotFoundException | NoClassDefFoundError e) { LOGGER.fine(() -> "Could not load class from JAR: " + className); } @@ -212,7 +213,7 @@ static Map fetchUpstreamSources(Set> localClasses) { final var upstreamPath = mapToUpstreamPath(className); final var url = GITHUB_BASE_URL + upstreamPath; - LOGGER.fine(() -> "Fetching: " + url); + LOGGER.info("Fetching upstream source: " + url); try { final var request = HttpRequest.newBuilder() @@ -227,20 +228,20 @@ static Map fetchUpstreamSources(Set> localClasses) { final var body = response.body(); FETCH_CACHE.put(className, body); results.put(className, body); - LOGGER.fine(() -> "Successfully fetched: " + className); + LOGGER.info("Successfully fetched " + body.length() + " chars for: " + className); } else if (response.statusCode() == 404) { final var error = "NOT_FOUND: Upstream file not found (possibly deleted or renamed)"; results.put(className, error); - LOGGER.fine(() -> "Not found: " + className); + LOGGER.info("404 Not Found for upstream: " + className + " at " + url); } else { final var error = "HTTP_ERROR: Status " + response.statusCode(); results.put(className, error); - LOGGER.warning(() -> "HTTP error for " + className + ": " + response.statusCode()); + LOGGER.info("HTTP error " + response.statusCode() + " for " + className + " at " + url); } } catch (Exception e) { final var error = "FETCH_ERROR: " + e.getMessage(); results.put(className, error); - LOGGER.log(Level.WARNING, "Error fetching " + className, e); + LOGGER.info("Fetch error for " + className + " at " + url + ": " + e.getMessage()); } } diff --git a/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java b/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java index bbba3ce..30ede7e 100644 --- a/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java +++ b/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java @@ -80,7 +80,7 @@ void testExtractLocalApiJsonObject() throws ClassNotFoundException { assertThat(((JsonString) api.members().get("packageName")).value()).isEqualTo("jdk.sandbox.java.util.json"); assertThat(api.members()).containsKey("isInterface"); - assertThat(((JsonValue) api.members().get("isInterface"))).isEqualTo(JsonBoolean.of(true)); + assertThat(api.members().get("isInterface")).isEqualTo(JsonBoolean.of(true)); assertThat(api.members()).containsKey("methods"); final var methods = (JsonObject) api.members().get("methods"); @@ -94,7 +94,7 @@ void testExtractLocalApiJsonValue() throws ClassNotFoundException { final var api = ApiTracker.extractLocalApi(clazz); assertThat(api.members()).containsKey("isSealed"); - assertThat(((JsonValue) api.members().get("isSealed"))).isEqualTo(JsonBoolean.of(true)); + assertThat(api.members().get("isSealed")).isEqualTo(JsonBoolean.of(true)); assertThat(api.members()).containsKey("permits"); final var permits = (JsonArray) api.members().get("permits"); @@ -203,7 +203,7 @@ void testRunFullComparison() { ); // Total classes should be greater than 0 - final var totalClasses = ((JsonValue) summary.members().get("totalClasses")); + final var totalClasses = summary.members().get("totalClasses"); assertThat(totalClasses).isNotNull(); } } diff --git a/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/CompilerApiLearningTest.java b/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/CompilerApiLearningTest.java deleted file mode 100644 index 76da6ea..0000000 --- a/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/CompilerApiLearningTest.java +++ /dev/null @@ -1,297 +0,0 @@ -package io.github.simbo1905.tracker; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; - -import javax.tools.JavaCompiler; -import javax.tools.ToolProvider; -import javax.tools.DiagnosticCollector; -import javax.tools.JavaFileObject; -import javax.tools.StandardJavaFileManager; -import javax.tools.SimpleJavaFileObject; - -import com.sun.source.tree.*; -import com.sun.source.util.*; - -import java.io.File; -import java.io.IOException; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import jdk.sandbox.java.util.json.Json; -import jdk.sandbox.java.util.json.JsonArray; -import jdk.sandbox.java.util.json.JsonObject; -import jdk.sandbox.java.util.json.JsonString; -import jdk.sandbox.java.util.json.JsonValue; -import javax.lang.model.element.Modifier; -import java.util.logging.Logger; -import java.util.logging.Level; - -public class CompilerApiLearningTest { - - /// In-memory JavaFileObject for creating stub classes - static class InMemoryJavaFileObject extends SimpleJavaFileObject { - private final String content; - - InMemoryJavaFileObject(String className, String content) { - super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); - this.content = content; - } - - @Override - public CharSequence getCharContent(boolean ignoreEncodingErrors) { - return content; - } - } - private static final Logger LOGGER = Logger.getLogger(CompilerApiLearningTest.class.getName()); - private static final String JSON_OBJECT_SOURCE_PATH = "src/test/resources/JsonObject.java"; - - @BeforeAll - static void setupLogging() { - LoggingControl.setupCleanLogging(); - } - - @Test - @DisplayName("Test 1: Source-Level Analysis with JavaParser API (Parse-Only)") - void testSourceLevelAnalysisWithJavaParser() throws IOException { - Instant start = Instant.now(); - LOGGER.info("\n--- Running Test 1: Source-Level Analysis with JavaParser API (Parse-Only) ---"); - - JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); - assertThat(compiler).as("JavaCompiler should be available").isNotNull(); - - DiagnosticCollector diagnostics = new DiagnosticCollector<>(); - StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, StandardCharsets.UTF_8); - - File jsonObjectFile = new File(JSON_OBJECT_SOURCE_PATH); - LOGGER.fine("JsonObject file path: " + jsonObjectFile.getAbsolutePath()); - LOGGER.fine("JsonObject file exists: " + jsonObjectFile.exists()); - LOGGER.fine("JsonObject file canonical path: " + jsonObjectFile.getCanonicalPath()); - - Iterable compilationUnits = fileManager.getJavaFileObjectsFromFiles(Collections.singletonList(jsonObjectFile)); - - // Create stub sources for internal dependencies - List allCompilationUnits = new ArrayList<>((List) compilationUnits); - - // Add stub for PreviewFeature annotation - String previewFeatureStub = """ - package jdk.internal.javac; - import java.lang.annotation.*; - @Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD}) - @Retention(RetentionPolicy.RUNTIME) - public @interface PreviewFeature { - Feature feature(); - enum Feature { JSON } - } - """; - allCompilationUnits.add(new InMemoryJavaFileObject("jdk.internal.javac.PreviewFeature", previewFeatureStub)); - - // Add stub for JsonObjectImpl - String jsonObjectImplStub = """ - package jdk.internal.util.json; - import java.util.Map; - import java.util.json.JsonObject; - import java.util.json.JsonValue; - public class JsonObjectImpl implements JsonObject { - public JsonObjectImpl(Map map) {} - public Map members() { return null; } - public boolean equals(Object obj) { return false; } - public int hashCode() { return 0; } - } - """; - allCompilationUnits.add(new InMemoryJavaFileObject("jdk.internal.util.json.JsonObjectImpl", jsonObjectImplStub)); - - // Add stub for JsonValue interface (parent of JsonObject) - String jsonValueStub = """ - package java.util.json; - public sealed interface JsonValue permits JsonObject, JsonArray, JsonString, JsonNumber, JsonBoolean, JsonNull {} - """; - allCompilationUnits.add(new InMemoryJavaFileObject("java.util.json.JsonValue", jsonValueStub)); - - // Add minimal stubs for other JSON types referenced in permits clause - String[] jsonTypes = {"JsonArray", "JsonString", "JsonNumber", "JsonBoolean", "JsonNull"}; - for (String type : jsonTypes) { - String stub = "package java.util.json; public non-sealed interface " + type + " extends JsonValue {}"; - allCompilationUnits.add(new InMemoryJavaFileObject("java.util.json." + type, stub)); - } - - // Create a CompilationTask in parse-only mode with relaxed compilation - List options = List.of( - "-proc:none", // Disable annotation processing - "-XDignore.symbol.file", // Ignore internal API restrictions - "-Xlint:none", // Disable all warnings - "--enable-preview", // Enable preview features - "--release", "24" // Target Java 24 - ); - - JavacTask task = (JavacTask) compiler.getTask( - null, // no output writer - fileManager, - diagnostics, - options, - null, // no classes - allCompilationUnits); - - try { - Iterable trees = task.parse(); - assertThat(trees).as("Should parse at least one compilation unit").isNotEmpty(); - - JsonObject extractedApi = null; - - for (CompilationUnitTree tree : trees) { - String fileName = tree.getSourceFile().getName(); - LOGGER.info("Parsed Compilation Unit: " + fileName); - - // Only process the JsonObject.java file, skip stub files - if (!fileName.endsWith("JsonObject.java")) { - continue; - } - - // Visitor to extract API information - ApiExtractorVisitor visitor = new ApiExtractorVisitor(Trees.instance(task)); - visitor.scan(tree, null); - - extractedApi = visitor.getExtractedApi(); - LOGGER.info("Extracted API: " + Json.toDisplayString(extractedApi, 2)); - LOGGER.fine("Raw extracted API map keys: " + extractedApi.members().keySet()); - LOGGER.finer("Full extracted API: " + extractedApi.members()); - } - - assertThat(extractedApi).as("Should have extracted API from JsonObject.java").isNotNull(); - - // Basic assertions for expected content - JsonString className = (JsonString) extractedApi.members().get("className"); - assertThat(className.value()).isEqualTo("JsonObject"); - - JsonArray modifiers = (JsonArray) extractedApi.members().get("modifiers"); - assertThat(modifiers).as("modifiers should be present").isNotNull(); - Set modifierStrings = modifiers.values().stream() - .map(v -> ((JsonString) v).value()) - .collect(Collectors.toSet()); - assertThat(modifierStrings).contains("public"); - - JsonArray extendsList = (JsonArray) extractedApi.members().get("extends"); - assertThat(extendsList).as("extends should be present").isNotNull(); - Set extendsStrings = extendsList.values().stream() - .map(v -> ((JsonString) v).value()) - .collect(Collectors.toSet()); - assertThat(extendsStrings).contains("JsonValue"); - - JsonObject methods = (JsonObject) extractedApi.members().get("methods"); - assertThat(methods).as("methods should be present").isNotNull(); - assertThat(methods.members()).containsKey("members"); - assertThat(methods.members()).containsKey("of"); - assertThat(methods.members()).containsKey("equals"); - assertThat(methods.members()).containsKey("hashCode"); - - // Log diagnostics (errors/warnings from parsing) - diagnostics.getDiagnostics().forEach(d -> LOGGER.warning("Diagnostic: " + d)); - - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Error during parsing: " + e.getMessage(), e); - throw e; // Re-throw to fail the test - } finally { - fileManager.close(); - Instant end = Instant.now(); - LOGGER.info("Test 1 finished in: " + Duration.between(start, end).toMillis() + " ms"); - } - } - - // Helper class to extract API information from the AST - static class ApiExtractorVisitor extends TreePathScanner { - private final Map classMap = new LinkedHashMap<>(); - private final Map methodsMap = new LinkedHashMap<>(); - - public ApiExtractorVisitor(Trees trees) { - // Trees parameter kept for future use if needed - } - - public JsonObject getExtractedApi() { - classMap.put("methods", JsonObject.of(methodsMap)); - return JsonObject.of(classMap); - } - - @Override - public Void visitCompilationUnit(CompilationUnitTree node, Void p) { - LOGGER.fine("Visiting compilation unit with " + node.getTypeDecls().size() + " type declarations"); - for (Tree member : node.getTypeDecls()) { - LOGGER.fine("Type declaration kind: " + member.getKind()); - scan(member, p); - } - return super.visitCompilationUnit(node, p); - } - - @Override - public Void visitClass(ClassTree node, Void p) { - LOGGER.fine("Visiting class: " + node.getSimpleName()); - LOGGER.finer("Class kind: " + node.getKind()); - LOGGER.finer("Number of members: " + node.getMembers().size()); - - classMap.put("className", JsonString.of(node.getSimpleName().toString())); - classMap.put("modifiers", JsonArray.of(node.getModifiers().getFlags().stream() - .map(Object::toString) - .map(JsonString::of) - .collect(Collectors.toList()))); - - List extendsList = new ArrayList<>(); - if (node.getExtendsClause() != null) { - extendsList.add(JsonString.of(node.getExtendsClause().toString())); - } - if (!node.getImplementsClause().isEmpty()) { - node.getImplementsClause().forEach(impl -> extendsList.add(JsonString.of(impl.toString()))); - } - classMap.put("extends", JsonArray.of(extendsList)); - - // Log members - for (Tree member : node.getMembers()) { - LOGGER.finer("Member kind: " + member.getKind() + ", toString: " + member.toString().substring(0, Math.min(50, member.toString().length()))); - } - - return super.visitClass(node, p); - } - - @Override - public Void visitMethod(MethodTree node, Void p) { - LOGGER.finer("Visiting method: " + node.getName() + ", modifiers: " + node.getModifiers().getFlags()); - - // In interfaces, methods without private/default modifiers are implicitly public - boolean isPublic = node.getModifiers().getFlags().contains(Modifier.PUBLIC) || - (getCurrentPath().getParentPath() != null && - getCurrentPath().getParentPath().getLeaf().getKind() == Tree.Kind.INTERFACE && - !node.getModifiers().getFlags().contains(Modifier.PRIVATE) && - !node.getModifiers().getFlags().contains(Modifier.DEFAULT)); - - if (isPublic) { - LOGGER.fine("Processing public method: " + node.getName()); - Map methodMap = new LinkedHashMap<>(); - methodMap.put("modifiers", JsonArray.of(node.getModifiers().getFlags().stream() - .map(Object::toString) - .map(JsonString::of) - .collect(Collectors.toList()))); - methodMap.put("returnType", JsonString.of(node.getReturnType() != null ? node.getReturnType().toString() : "void")); - - List parameters = new ArrayList<>(); - node.getParameters().forEach(param -> parameters.add(JsonString.of(param.getType() + " " + param.getName()))); - methodMap.put("parameters", JsonArray.of(parameters)); - - List throwsList = new ArrayList<>(); - node.getThrows().forEach(throwable -> throwsList.add(JsonString.of(throwable.toString()))); - methodMap.put("throws", JsonArray.of(throwsList)); - - methodsMap.put(node.getName().toString(), JsonObject.of(methodMap)); - } - return super.visitMethod(node, p); - } - } -} From 7f2edab6b7d2fd034c2c6b4f121e6ce144035d14 Mon Sep 17 00:00:00 2001 From: Simon Massey Date: Sun, 27 Jul 2025 17:30:10 +0100 Subject: [PATCH 6/6] fix: Focus API tracker on public API only, eliminate false positives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove internal package scanning (jdk.sandbox.internal.util.json) - Track only public API classes (jdk.sandbox.java.util.json) - Eliminate false positive 404 errors for removed internal classes like StableValue - Reduce tracked classes from 19 to 10 (public API only) - Improve performance and focus on meaningful API drift detection - Update tests to verify no internal classes are tracked Results: 0 missing upstream, 10 public API classes, faster execution 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../main/java/io/github/simbo1905/tracker/ApiTracker.java | 5 ++--- .../java/io/github/simbo1905/tracker/ApiTrackerTest.java | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java index 594dbce..5e13d92 100644 --- a/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java +++ b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java @@ -82,10 +82,9 @@ static Set> discoverLocalJsonClasses() { LOGGER.info("Starting class discovery for JSON API packages"); final var classes = new TreeSet>((a, b) -> a.getName().compareTo(b.getName())); - // Packages to scan + // Packages to scan - only public API, not internal implementation final var packages = List.of( - "jdk.sandbox.java.util.json", - "jdk.sandbox.internal.util.json" + "jdk.sandbox.java.util.json" ); final var classLoader = Thread.currentThread().getContextClassLoader(); diff --git a/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java b/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java index 30ede7e..0a2d853 100644 --- a/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java +++ b/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java @@ -50,10 +50,10 @@ void testDiscoverLocalJsonClasses() { "jdk.sandbox.java.util.json.JsonNull" ); - // Should also find internal implementation classes + // Should NOT find internal implementation classes (public API only) assertThat(classes.stream().anyMatch(c -> c.getName().startsWith("jdk.sandbox.internal.util.json"))) - .as("Should find internal implementation classes") - .isTrue(); + .as("Should not find internal implementation classes - public API only") + .isFalse(); // Should be sorted final var names = classes.stream().map(Class::getName).toList();