From 03f3bc3cf2e3a2e4205cf1420754c0ceff150bbb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 30 May 2026 04:03:15 +0000 Subject: [PATCH 01/30] ci: use GitHub release tag for publish version --- .github/workflows/publish.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9c92f9f..74e67ef 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -25,7 +25,17 @@ jobs: - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 - - name: Publish to GitHub Packages + - name: Publish release to GitHub Packages + if: github.event_name == 'release' + env: + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + ORG_GRADLE_PROJECT_version: ${{ github.event.release.tag_name }} + run: ./gradlew publish + + - name: Publish workflow dispatch to GitHub Packages + if: github.event_name == 'workflow_dispatch' env: GITHUB_ACTOR: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 330563fb656f35d9c527bafa56db83257883eec6 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sat, 30 May 2026 09:28:43 -0500 Subject: [PATCH 02/30] dfdf --- .github/workflows/ci.yml | 81 ++++++++++++++++++++++++++++++++--- .github/workflows/publish.yml | 43 ------------------- GitVersion.yml | 18 ++++++++ 3 files changed, 93 insertions(+), 49 deletions(-) delete mode 100644 .github/workflows/publish.yml create mode 100644 GitVersion.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 473a4bf..6449520 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,28 +1,97 @@ -name: CI +name: java-ref-plugin on: + workflow_dispatch: push: branches: - - "**" + - main + - develop pull_request: + branches: + - main + - develop + release: + types: + - created jobs: build: + name: Build runs-on: ubuntu-latest permissions: contents: read steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 + + - name: Install GitVersion + uses: gittools/actions/gitversion/setup@v4 + with: + versionSpec: 6.x + + - name: Execute GitVersion + uses: gittools/actions/gitversion/execute@v4 - name: Set up Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin - java-version: 17 + java-version: 25 - name: Set up Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v6 - name: Build and test + env: + ORG_GRADLE_PROJECT_version: $(GitVersion_FullSemVer) run: ./gradlew build + + - name: Upload Gradle build output + uses: actions/upload-artifact@v4 + with: + name: gradle-build-output + path: build/ + + publish: + name: Publish + runs-on: ubuntu-latest + needs: build + permissions: + contents: read + packages: write + steps: + - name: Check out repository + uses: actions/checkout@v5 + + - name: Set up Java + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 25 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Download Gradle build output + uses: actions/download-artifact@v4 + with: + name: gradle-build-output + path: . + + - name: Publish release to GitHub Packages + env: + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + ORG_GRADLE_PROJECT_version: $(GitVersion_FullSemVer) + run: > + ./gradlew publish + -x compileJava + -x compileTestJava + -x processResources + -x processTestResources + -x classes + -x testClasses + -x jar + -x sourcesJar + -x test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 74e67ef..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Publish - -on: - release: - types: - - published - workflow_dispatch: - -jobs: - publish: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Set up Java - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: 17 - - - name: Set up Gradle - uses: gradle/actions/setup-gradle@v4 - - - name: Publish release to GitHub Packages - if: github.event_name == 'release' - env: - GITHUB_ACTOR: ${{ github.actor }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_REPOSITORY: ${{ github.repository }} - ORG_GRADLE_PROJECT_version: ${{ github.event.release.tag_name }} - run: ./gradlew publish - - - name: Publish workflow dispatch to GitHub Packages - if: github.event_name == 'workflow_dispatch' - env: - GITHUB_ACTOR: ${{ github.actor }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_REPOSITORY: ${{ github.repository }} - run: ./gradlew publish diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 0000000..ab52792 --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,18 @@ +mode: ContinuousDeployment +assembly-file-versioning-format: '{Major}.{Minor}.{Patch}.{WeightedPreReleaseNumber ?? 0}' +major-version-bump-message: '\+semver:\s?(major)' +minor-version-bump-message: '\+semver:\s?(minor)' +patch-version-bump-message: '\+semver:\s?(patch)' +branches: + main: + mode: ContinuousDelivery + regex: ^main$ + label: pre + increment: Patch + is-release-branch: false + develop: + mode: ContinuousDelivery + regex: ^develop$ + label: dev + increment: Minor + is-release-branch: false From 3320d97347f6934aad49303e70a0bd19c992380d Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sat, 30 May 2026 09:28:51 -0500 Subject: [PATCH 03/30] dfdf --- .idea/.gitignore | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .idea/.gitignore diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml From 80b26d45da0e871876c6daf108867cb72fcbf7fd Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sat, 30 May 2026 09:29:49 -0500 Subject: [PATCH 04/30] Not shallow. --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6449520..c70acc3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,9 @@ jobs: steps: - name: Check out repository uses: actions/checkout@v5 + with: + fetch-depth: 0 + submodules: recursive - name: Install GitVersion uses: gittools/actions/gitversion/setup@v4 From b55898020eeaef6b3ce9d2e046157144d968de63 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sat, 30 May 2026 09:31:40 -0500 Subject: [PATCH 05/30] upd --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c70acc3..7e5b09a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,7 @@ jobs: run: ./gradlew build - name: Upload Gradle build output - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: gradle-build-output path: build/ From b14de21071764b6db780b9cdcb362206a456302e Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sat, 30 May 2026 09:32:07 -0500 Subject: [PATCH 06/30] vers --- .github/workflows/ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e5b09a..90d15d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,6 +66,14 @@ jobs: - name: Check out repository uses: actions/checkout@v5 + - name: Install GitVersion + uses: gittools/actions/gitversion/setup@v4 + with: + versionSpec: 6.x + + - name: Execute GitVersion + uses: gittools/actions/gitversion/execute@v4 + - name: Set up Java uses: actions/setup-java@v5 with: From d08db2ee190f35b020c5616a13ae75011fa81132 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sat, 30 May 2026 09:35:18 -0500 Subject: [PATCH 07/30] Once. --- .github/workflows/ci.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90d15d2..a66ca36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,8 @@ jobs: build: name: Build runs-on: ubuntu-latest + outputs: + gradle_version: ${{ steps.gitversion.outputs.fullSemVer }} permissions: contents: read steps: @@ -33,6 +35,7 @@ jobs: versionSpec: 6.x - name: Execute GitVersion + id: gitversion uses: gittools/actions/gitversion/execute@v4 - name: Set up Java @@ -46,7 +49,7 @@ jobs: - name: Build and test env: - ORG_GRADLE_PROJECT_version: $(GitVersion_FullSemVer) + ORG_GRADLE_PROJECT_version: ${{ steps.gitversion.outputs.fullSemVer }} run: ./gradlew build - name: Upload Gradle build output @@ -65,14 +68,9 @@ jobs: steps: - name: Check out repository uses: actions/checkout@v5 - - - name: Install GitVersion - uses: gittools/actions/gitversion/setup@v4 with: - versionSpec: 6.x - - - name: Execute GitVersion - uses: gittools/actions/gitversion/execute@v4 + fetch-depth: 0 + submodules: recursive - name: Set up Java uses: actions/setup-java@v5 @@ -94,7 +92,7 @@ jobs: GITHUB_ACTOR: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_REPOSITORY: ${{ github.repository }} - ORG_GRADLE_PROJECT_version: $(GitVersion_FullSemVer) + ORG_GRADLE_PROJECT_version: ${{ needs.build.outputs.gradle_version }} run: > ./gradlew publish -x compileJava From a7e82206e643d5c6f451d30ea88730fa68a4e724 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sat, 30 May 2026 09:39:53 -0500 Subject: [PATCH 08/30] Once.f --- .github/workflows/ci.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a66ca36..3905d5a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,6 +57,7 @@ jobs: with: name: gradle-build-output path: build/ + if-no-files-found: error publish: name: Publish @@ -85,7 +86,16 @@ jobs: uses: actions/download-artifact@v4 with: name: gradle-build-output - path: . + path: build/ + + - name: Verify restored Gradle build output + env: + ORG_GRADLE_PROJECT_version: ${{ needs.build.outputs.gradle_version }} + run: | + find build -maxdepth 3 -type f | sort + test -d build/libs + test -f "build/libs/java-ref-plugin-${ORG_GRADLE_PROJECT_version}.jar" + test -f "build/libs/java-ref-plugin-${ORG_GRADLE_PROJECT_version}-sources.jar" - name: Publish release to GitHub Packages env: From c42537a6fc618d69218052faa56bb1cb727513f4 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sat, 30 May 2026 09:47:42 -0500 Subject: [PATCH 09/30] Fix versioning. --- .github/workflows/ci.yml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3905d5a..4922202 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: name: Build runs-on: ubuntu-latest outputs: - gradle_version: ${{ steps.gitversion.outputs.fullSemVer }} + gradle_version: ${{ steps.maven_version.outputs.value }} permissions: contents: read steps: @@ -38,6 +38,25 @@ jobs: id: gitversion uses: gittools/actions/gitversion/execute@v4 + - name: Compute Maven-friendly version + id: maven_version + shell: bash + env: + GITVERSION_MAJOR_MINOR_PATCH: ${{ steps.gitversion.outputs.majorMinorPatch }} + GITVERSION_PRE_RELEASE_LABEL: ${{ steps.gitversion.outputs.preReleaseLabel }} + GITVERSION_PRE_RELEASE_NUMBER: ${{ steps.gitversion.outputs.preReleaseNumber }} + run: | + version="$GITVERSION_MAJOR_MINOR_PATCH" + if [ -n "$GITVERSION_PRE_RELEASE_LABEL" ]; then + version="$version-$GITVERSION_PRE_RELEASE_LABEL" + if [ -n "$GITVERSION_PRE_RELEASE_NUMBER" ]; then + version="$version-$GITVERSION_PRE_RELEASE_NUMBER" + fi + fi + + echo "Computed Maven version: $version" + echo "value=$version" >> "$GITHUB_OUTPUT" + - name: Set up Java uses: actions/setup-java@v5 with: @@ -49,7 +68,7 @@ jobs: - name: Build and test env: - ORG_GRADLE_PROJECT_version: ${{ steps.gitversion.outputs.fullSemVer }} + ORG_GRADLE_PROJECT_version: ${{ steps.maven_version.outputs.value }} run: ./gradlew build - name: Upload Gradle build output From 39bfd32cc68118888adcfae2bdcb6287827a3e0f Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sat, 30 May 2026 09:50:56 -0500 Subject: [PATCH 10/30] Fix versioning. --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4922202..da227ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: contents: read steps: - name: Check out repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 submodules: recursive @@ -72,7 +72,7 @@ jobs: run: ./gradlew build - name: Upload Gradle build output - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 with: name: gradle-build-output path: build/ @@ -87,7 +87,7 @@ jobs: packages: write steps: - name: Check out repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 submodules: recursive @@ -102,7 +102,7 @@ jobs: uses: gradle/actions/setup-gradle@v6 - name: Download Gradle build output - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: gradle-build-output path: build/ From 22db6e471c0a0dd6d2a8c661701e872cf5e6eac1 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sat, 30 May 2026 09:52:40 -0500 Subject: [PATCH 11/30] Upload separate artifacts. --- .github/workflows/ci.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da227ea..17b5c94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,6 +71,20 @@ jobs: ORG_GRADLE_PROJECT_version: ${{ steps.maven_version.outputs.value }} run: ./gradlew build + - name: Upload library JAR + uses: actions/upload-artifact@v7 + with: + name: java-ref-plugin-${{ steps.maven_version.outputs.value }} + path: build/libs/java-ref-plugin-${{ steps.maven_version.outputs.value }}.jar + if-no-files-found: error + + - name: Upload sources JAR + uses: actions/upload-artifact@v7 + with: + name: java-ref-plugin-${{ steps.maven_version.outputs.value }}-sources + path: build/libs/java-ref-plugin-${{ steps.maven_version.outputs.value }}-sources.jar + if-no-files-found: error + - name: Upload Gradle build output uses: actions/upload-artifact@v7 with: @@ -102,7 +116,7 @@ jobs: uses: gradle/actions/setup-gradle@v6 - name: Download Gradle build output - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v4 with: name: gradle-build-output path: build/ From 324127be048f1f75796db940f9c1bb37ad30e4b3 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sat, 30 May 2026 09:56:36 -0500 Subject: [PATCH 12/30] Change to org.ikvm. --- README.md | 31 +++++++++++++++++++ build.gradle.kts | 2 +- gradle.properties | 2 +- .../ikvm/javarefplugin/JavaRefPlugin.java | 3 +- .../javarefplugin/MethodBodyStripper.java | 3 +- .../services/com.sun.source.util.Plugin | 2 +- .../ikvm/javarefplugin/JavaRefPluginTest.java | 3 +- 7 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 README.md rename src/main/java/{net => org}/ikvm/javarefplugin/JavaRefPlugin.java (97%) rename src/main/java/{net => org}/ikvm/javarefplugin/MethodBodyStripper.java (99%) rename src/test/java/{net => org}/ikvm/javarefplugin/JavaRefPluginTest.java (99%) diff --git a/README.md b/README.md new file mode 100644 index 0000000..f502249 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# java-ref-plugin + +`java-ref-plugin` is a `javac` plugin that rewrites concrete method bodies into `NoSuchMethodError` throws for reference-only API artifacts. + +## Maven coordinates + +```text +org.ikvm:java-ref-plugin +``` + +## Plugin identity + +- Plugin name: `JavaRef` +- Plugin class: `org.ikvm.javarefplugin.JavaRefPlugin` +- Automatic module name: `org.ikvm.javarefplugin` + +## Building + +```bash +./gradlew build +``` + +## Publishing + +The project publishes the Maven artifact `org.ikvm:java-ref-plugin`. + +## Source layout + +- Main sources: `src/main/java/org/ikvm/javarefplugin` +- Tests: `src/test/java/org/ikvm/javarefplugin` + diff --git a/build.gradle.kts b/build.gradle.kts index 441623f..3c01248 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -55,7 +55,7 @@ tasks.withType().configureEach { tasks.named("jar") { manifest { - attributes["Automatic-Module-Name"] = "net.ikvm.javarefplugin" + attributes["Automatic-Module-Name"] = "org.ikvm.javarefplugin" } } diff --git a/gradle.properties b/gradle.properties index 112c2e4..b263bbf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.configuration-cache=true -group=net.ikvm +group=org.ikvm version=0.1.0-SNAPSHOT description=Javac plugin that rewrites concrete method bodies into NoSuchMethodError throws for reference-only API artifacts. diff --git a/src/main/java/net/ikvm/javarefplugin/JavaRefPlugin.java b/src/main/java/org/ikvm/javarefplugin/JavaRefPlugin.java similarity index 97% rename from src/main/java/net/ikvm/javarefplugin/JavaRefPlugin.java rename to src/main/java/org/ikvm/javarefplugin/JavaRefPlugin.java index 2961b5d..51242a4 100644 --- a/src/main/java/net/ikvm/javarefplugin/JavaRefPlugin.java +++ b/src/main/java/org/ikvm/javarefplugin/JavaRefPlugin.java @@ -1,4 +1,4 @@ -package net.ikvm.javarefplugin; +package org.ikvm.javarefplugin; import com.sun.source.util.JavacTask; import com.sun.source.util.Plugin; @@ -44,3 +44,4 @@ public void finished(TaskEvent event) { }); } } + diff --git a/src/main/java/net/ikvm/javarefplugin/MethodBodyStripper.java b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java similarity index 99% rename from src/main/java/net/ikvm/javarefplugin/MethodBodyStripper.java rename to src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java index 3cc3e0f..a188d9f 100644 --- a/src/main/java/net/ikvm/javarefplugin/MethodBodyStripper.java +++ b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java @@ -1,4 +1,4 @@ -package net.ikvm.javarefplugin; +package org.ikvm.javarefplugin; import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.tree.JCTree; @@ -126,3 +126,4 @@ private JCTree.JCExpression memberAccess(String qualifiedName) { return expression; } } + diff --git a/src/main/resources/META-INF/services/com.sun.source.util.Plugin b/src/main/resources/META-INF/services/com.sun.source.util.Plugin index e7d8ddf..d177053 100644 --- a/src/main/resources/META-INF/services/com.sun.source.util.Plugin +++ b/src/main/resources/META-INF/services/com.sun.source.util.Plugin @@ -1 +1 @@ -net.ikvm.javarefplugin.JavaRefPlugin +org.ikvm.javarefplugin.JavaRefPlugin diff --git a/src/test/java/net/ikvm/javarefplugin/JavaRefPluginTest.java b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java similarity index 99% rename from src/test/java/net/ikvm/javarefplugin/JavaRefPluginTest.java rename to src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java index 35a5466..ac849dd 100644 --- a/src/test/java/net/ikvm/javarefplugin/JavaRefPluginTest.java +++ b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java @@ -1,4 +1,4 @@ -package net.ikvm.javarefplugin; +package org.ikvm.javarefplugin; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -246,3 +246,4 @@ private URLClassLoader classLoader() { } } } + From 3d25b84adfea1d189dd5955eab0b1cc4ffa1c157 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sat, 30 May 2026 10:00:40 -0500 Subject: [PATCH 13/30] Change to org.ikvm. --- .../javarefplugin/MethodBodyStripper.java | 8 +++--- .../ikvm/javarefplugin/JavaRefPluginTest.java | 26 ++++++++++++++----- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java index a188d9f..c965b1f 100644 --- a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java +++ b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java @@ -12,7 +12,7 @@ final class MethodBodyStripper extends TreeTranslator { - private static final String STRIPPED_MESSAGE = "Method body stripped by JavaRef plugin."; + private static final String STRIPPED_MESSAGE = "Method body stripped from reference-only artifact."; private final TreeMaker maker; private final Names names; @@ -77,7 +77,7 @@ private List replacementStatements(JCTree.JCMethodDecl tree) maker.NewClass( null, List.nil(), - memberAccess("java.lang.NoSuchMethodError"), + unsupportedOperationExceptionType(), List.of(maker.Literal(STRIPPED_MESSAGE)), null ) @@ -117,8 +117,8 @@ private JCTree.JCStatement findConstructorInvocation(List st return null; } - private JCTree.JCExpression memberAccess(String qualifiedName) { - String[] elements = qualifiedName.split("\\."); + private JCTree.JCExpression unsupportedOperationExceptionType() { + String[] elements = "java.lang.UnsupportedOperationException".split("\\."); JCTree.JCExpression expression = maker.Ident(names.fromString(elements[0])); for (int i = 1; i < elements.length; i++) { expression = maker.Select(expression, names.fromString(elements[i])); diff --git a/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java index ac849dd..725afef 100644 --- a/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java +++ b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java @@ -1,6 +1,7 @@ package org.ikvm.javarefplugin; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -63,11 +64,15 @@ public static String greeting() { InvocationTargetException staticFailure = assertThrows(InvocationTargetException.class, () -> sampleClass.getMethod("greeting").invoke(null)); - assertTrue(staticFailure.getCause() instanceof NoSuchMethodError); + UnsupportedOperationException staticCause = + assertInstanceOf(UnsupportedOperationException.class, staticFailure.getCause()); + assertEquals("Method body stripped from reference-only artifact.", staticCause.getMessage()); InvocationTargetException constructorFailure = assertThrows(InvocationTargetException.class, () -> sampleClass.getConstructor().newInstance()); - assertTrue(constructorFailure.getCause() instanceof NoSuchMethodError); + UnsupportedOperationException constructorCause = + assertInstanceOf(UnsupportedOperationException.class, constructorFailure.getCause()); + assertEquals("Method body stripped from reference-only artifact.", constructorCause.getMessage()); } @Test @@ -99,7 +104,9 @@ public Child() { Class childClass = result.loadClass("example.Child"); InvocationTargetException constructorFailure = assertThrows(InvocationTargetException.class, () -> childClass.getConstructor().newInstance()); - assertTrue(constructorFailure.getCause() instanceof NoSuchMethodError); + UnsupportedOperationException constructorCause = + assertInstanceOf(UnsupportedOperationException.class, constructorFailure.getCause()); + assertEquals("Method body stripped from reference-only artifact.", constructorCause.getMessage()); } @Test @@ -127,7 +134,9 @@ static String helper() { InvocationTargetException staticFailure = assertThrows(InvocationTargetException.class, () -> interfaceClass.getMethod("helper").invoke(null)); - assertTrue(staticFailure.getCause() instanceof NoSuchMethodError); + UnsupportedOperationException staticCause = + assertInstanceOf(UnsupportedOperationException.class, staticFailure.getCause()); + assertEquals("Method body stripped from reference-only artifact.", staticCause.getMessage()); Object proxy = java.lang.reflect.Proxy.newProxyInstance( result.classLoader(), @@ -141,8 +150,9 @@ static String helper() { .findSpecial(interfaceClass, "value", MethodType.methodType(String.class), interfaceClass) .bindTo(proxy); - NoSuchMethodError defaultFailure = assertThrows(NoSuchMethodError.class, handle::invokeWithArguments); - assertEquals("Method body stripped by JavaRef plugin.", defaultFailure.getMessage()); + UnsupportedOperationException defaultFailure = + assertThrows(UnsupportedOperationException.class, handle::invokeWithArguments); + assertEquals("Method body stripped from reference-only artifact.", defaultFailure.getMessage()); } @Test @@ -162,7 +172,9 @@ public class ImplicitConstructor { Class type = result.loadClass("example.ImplicitConstructor"); InvocationTargetException constructorFailure = assertThrows(InvocationTargetException.class, () -> type.getConstructor().newInstance()); - assertTrue(constructorFailure.getCause() instanceof NoSuchMethodError); + UnsupportedOperationException constructorCause = + assertInstanceOf(UnsupportedOperationException.class, constructorFailure.getCause()); + assertEquals("Method body stripped from reference-only artifact.", constructorCause.getMessage()); } @Test From dd1b1a1c33b0252c1d6d93875f09573ab0c724c7 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sat, 30 May 2026 10:12:20 -0500 Subject: [PATCH 14/30] Change to org.ikvm. --- .github/workflows/ci.yml | 8 +- build.gradle.kts | 18 +- gradle/libs.versions.toml | 2 +- .../ikvm/javarefplugin/JavaRefPluginTest.java | 221 +++++++++--------- 4 files changed, 131 insertions(+), 118 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17b5c94..d4a28f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,13 @@ jobs: echo "Computed Maven version: $version" echo "value=$version" >> "$GITHUB_OUTPUT" - - name: Set up Java + - name: Set up Java 8 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 8 + + - name: Set up Java 25 uses: actions/setup-java@v5 with: distribution: temurin diff --git a/build.gradle.kts b/build.gradle.kts index 3c01248..5bd2977 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,17 +10,14 @@ repositories { mavenCentral() } -val javacExports = listOf( - "jdk.compiler/com.sun.tools.javac.api", - "jdk.compiler/com.sun.tools.javac.code", - "jdk.compiler/com.sun.tools.javac.tree", - "jdk.compiler/com.sun.tools.javac.util", -) - val java8Compiler = javaToolchains.compilerFor { languageVersion = JavaLanguageVersion.of(8) } +val java8Launcher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(8) +} + dependencies { compileOnly(files(java8Compiler.map { it.metadata.installationPath.file("lib/tools.jar") })) testImplementation(libs.junit.jupiter) @@ -41,13 +38,14 @@ tasks.named("compileJava") { } tasks.named("compileTestJava") { - sourceCompatibility = JavaVersion.VERSION_17.toString() - targetCompatibility = JavaVersion.VERSION_17.toString() + javaCompiler = java8Compiler + sourceCompatibility = JavaVersion.VERSION_1_8.toString() + targetCompatibility = JavaVersion.VERSION_1_8.toString() } tasks.withType().configureEach { useJUnitPlatform() - jvmArgs(javacExports.map { export -> "--add-exports=$export=ALL-UNNAMED" }) + javaLauncher = java8Launcher testLogging { exceptionFormat = TestExceptionFormat.FULL } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6b19d51..253e0a7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -junit-jupiter = "6.0.1" +junit-jupiter = "5.10.3" [libraries] junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" } diff --git a/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java index 725afef..aa79d0f 100644 --- a/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java +++ b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java @@ -1,23 +1,21 @@ package org.ikvm.javarefplugin; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.io.InputStream; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -32,30 +30,34 @@ class JavaRefPluginTest { + private static final String STRIPPED_MESSAGE = "Method body stripped from reference-only artifact."; + @TempDir Path tempDir; @Test void stripsStaticMethodsAndConstructorsWhileKeepingFields() throws Exception { - CompilationResult result = compile( - Map.of( - "example/Sample.java", - """ - package example; - - public class Sample { - public static final String CONSTANT = "hello"; - - public Sample() { - } - - public static String greeting() { - return CONSTANT; - } - } - """ + Map sources = new LinkedHashMap<>(); + sources.put( + "example/Sample.java", + joinLines( + "package example;", + "", + "public class Sample {", + " public static final String CONSTANT = \"hello\";", + "", + " public Sample() {", + " }", + "", + " public static String greeting() {", + " return CONSTANT;", + " }", + "}" ) ); + CompilationResult result = compile( + sources + ); Class sampleClass = result.loadClass("example.Sample"); @@ -64,117 +66,121 @@ public static String greeting() { InvocationTargetException staticFailure = assertThrows(InvocationTargetException.class, () -> sampleClass.getMethod("greeting").invoke(null)); - UnsupportedOperationException staticCause = - assertInstanceOf(UnsupportedOperationException.class, staticFailure.getCause()); - assertEquals("Method body stripped from reference-only artifact.", staticCause.getMessage()); + assertTrue(staticFailure.getCause() instanceof UnsupportedOperationException); + assertEquals(STRIPPED_MESSAGE, staticFailure.getCause().getMessage()); InvocationTargetException constructorFailure = assertThrows(InvocationTargetException.class, () -> sampleClass.getConstructor().newInstance()); - UnsupportedOperationException constructorCause = - assertInstanceOf(UnsupportedOperationException.class, constructorFailure.getCause()); - assertEquals("Method body stripped from reference-only artifact.", constructorCause.getMessage()); + assertTrue(constructorFailure.getCause() instanceof UnsupportedOperationException); + assertEquals(STRIPPED_MESSAGE, constructorFailure.getCause().getMessage()); } @Test void keepsExplicitConstructorChainingValid() throws Exception { - CompilationResult result = compile( - Map.of( - "example/Base.java", - """ - package example; - - public class Base { - public Base(String name) { - } - } - """, - "example/Child.java", - """ - package example; - - public class Child extends Base { - public Child() { - super("parent"); - } - } - """ + Map sources = new LinkedHashMap<>(); + sources.put( + "example/Base.java", + joinLines( + "package example;", + "", + "public class Base {", + " public Base(String name) {", + " }", + "}" + ) + ); + sources.put( + "example/Child.java", + joinLines( + "package example;", + "", + "public class Child extends Base {", + " public Child() {", + " super(\"parent\");", + " }", + "}" ) ); + CompilationResult result = compile( + sources + ); Class childClass = result.loadClass("example.Child"); InvocationTargetException constructorFailure = assertThrows(InvocationTargetException.class, () -> childClass.getConstructor().newInstance()); - UnsupportedOperationException constructorCause = - assertInstanceOf(UnsupportedOperationException.class, constructorFailure.getCause()); - assertEquals("Method body stripped from reference-only artifact.", constructorCause.getMessage()); + assertTrue(constructorFailure.getCause() instanceof UnsupportedOperationException); + assertEquals(STRIPPED_MESSAGE, constructorFailure.getCause().getMessage()); } @Test void stripsInterfaceDefaultAndStaticMethods() throws Exception { - CompilationResult result = compile( - Map.of( - "example/SampleInterface.java", - """ - package example; - - public interface SampleInterface { - default String value() { - return "value"; - } - - static String helper() { - return "helper"; - } - } - """ + Map sources = new LinkedHashMap<>(); + sources.put( + "example/SampleInterface.java", + joinLines( + "package example;", + "", + "public interface SampleInterface {", + " default String value() {", + " return \"value\";", + " }", + "", + " static String helper() {", + " return \"helper\";", + " }", + "}" + ) + ); + sources.put( + "example/SampleInterfaceImpl.java", + joinLines( + "package example;", + "", + "public class SampleInterfaceImpl implements SampleInterface {", + "}" ) ); + CompilationResult result = compile( + sources + ); Class interfaceClass = result.loadClass("example.SampleInterface"); + Class implementationClass = result.loadClass("example.SampleInterfaceImpl"); InvocationTargetException staticFailure = assertThrows(InvocationTargetException.class, () -> interfaceClass.getMethod("helper").invoke(null)); - UnsupportedOperationException staticCause = - assertInstanceOf(UnsupportedOperationException.class, staticFailure.getCause()); - assertEquals("Method body stripped from reference-only artifact.", staticCause.getMessage()); - - Object proxy = java.lang.reflect.Proxy.newProxyInstance( - result.classLoader(), - new Class[] { interfaceClass }, - (instance, method, arguments) -> { - throw new UnsupportedOperationException(method.getName()); - } - ); - - MethodHandle handle = MethodHandles.privateLookupIn(interfaceClass, MethodHandles.lookup()) - .findSpecial(interfaceClass, "value", MethodType.methodType(String.class), interfaceClass) - .bindTo(proxy); - - UnsupportedOperationException defaultFailure = - assertThrows(UnsupportedOperationException.class, handle::invokeWithArguments); - assertEquals("Method body stripped from reference-only artifact.", defaultFailure.getMessage()); + assertTrue(staticFailure.getCause() instanceof UnsupportedOperationException); + assertEquals(STRIPPED_MESSAGE, staticFailure.getCause().getMessage()); + + Object instance = implementationClass.getConstructor().newInstance(); + Method defaultMethod = implementationClass.getMethod("value"); + InvocationTargetException defaultFailure = + assertThrows(InvocationTargetException.class, () -> defaultMethod.invoke(instance)); + assertTrue(defaultFailure.getCause() instanceof UnsupportedOperationException); + assertEquals(STRIPPED_MESSAGE, defaultFailure.getCause().getMessage()); } @Test void stripsImplicitDefaultConstructors() throws Exception { - CompilationResult result = compile( - Map.of( - "example/ImplicitConstructor.java", - """ - package example; - - public class ImplicitConstructor { - } - """ + Map sources = new LinkedHashMap<>(); + sources.put( + "example/ImplicitConstructor.java", + joinLines( + "package example;", + "", + "public class ImplicitConstructor {", + "}" ) ); + CompilationResult result = compile( + sources + ); Class type = result.loadClass("example.ImplicitConstructor"); InvocationTargetException constructorFailure = assertThrows(InvocationTargetException.class, () -> type.getConstructor().newInstance()); - UnsupportedOperationException constructorCause = - assertInstanceOf(UnsupportedOperationException.class, constructorFailure.getCause()); - assertEquals("Method body stripped from reference-only artifact.", constructorCause.getMessage()); + assertTrue(constructorFailure.getCause() instanceof UnsupportedOperationException); + assertEquals(STRIPPED_MESSAGE, constructorFailure.getCause().getMessage()); } @Test @@ -199,14 +205,13 @@ private CompilationResult compile(Map sources) throws IOExceptio try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, StandardCharsets.UTF_8)) { Iterable units = fileManager.getJavaFileObjectsFromFiles(sourceFiles); - List options = List.of( - "-proc:none", - "-classpath", - System.getProperty("java.class.path"), - "-d", - classesDirectory.toString(), - "-Xplugin:" + JavaRefPlugin.NAME - ); + List options = new ArrayList(); + options.add("-proc:none"); + options.add("-classpath"); + options.add(System.getProperty("java.class.path")); + options.add("-d"); + options.add(classesDirectory.toString()); + options.add("-Xplugin:" + JavaRefPlugin.NAME); Boolean success = compiler.getTask(null, fileManager, diagnostics, options, null, units).call(); assertTrue(Boolean.TRUE.equals(success), () -> diagnostics.getDiagnostics() @@ -223,6 +228,10 @@ private static String formatDiagnostic(Diagnostic diag return source + ":" + diagnostic.getLineNumber() + ": " + diagnostic.getMessage(null); } + private static String joinLines(String... lines) { + return String.join(System.lineSeparator(), lines) + System.lineSeparator(); + } + private static int classFileMajorVersion(Class type) throws IOException { try (InputStream input = type.getResourceAsStream(type.getSimpleName() + ".class")) { assertNotNull(input); From 6a6e1c3c91ae4c242c199225403b329955377a21 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sat, 30 May 2026 10:27:31 -0500 Subject: [PATCH 15/30] Try to change to JDK 8 --- build.gradle.kts | 11 ++-- .../ikvm/javarefplugin/JavaRefPluginTest.java | 56 +++++++++++++------ 2 files changed, 44 insertions(+), 23 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 5bd2977..9ce3e15 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,19 +14,19 @@ val java8Compiler = javaToolchains.compilerFor { languageVersion = JavaLanguageVersion.of(8) } -val java8Launcher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(8) -} +val java8ToolsJar = files(java8Compiler.map { it.metadata.installationPath.file("lib/tools.jar") }) dependencies { - compileOnly(files(java8Compiler.map { it.metadata.installationPath.file("lib/tools.jar") })) + compileOnly(java8ToolsJar) + testCompileOnly(java8ToolsJar) + testRuntimeOnly(java8ToolsJar) testImplementation(libs.junit.jupiter) testRuntimeOnly("org.junit.platform:junit-platform-launcher") } java { toolchain { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(8) } withSourcesJar() } @@ -45,7 +45,6 @@ tasks.named("compileTestJava") { tasks.withType().configureEach { useJUnitPlatform() - javaLauncher = java8Launcher testLogging { exceptionFormat = TestExceptionFormat.FULL } diff --git a/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java index aa79d0f..4a05370 100644 --- a/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java +++ b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; @@ -114,8 +115,9 @@ void keepsExplicitConstructorChainingValid() throws Exception { @Test void stripsInterfaceDefaultAndStaticMethods() throws Exception { - Map sources = new LinkedHashMap<>(); - sources.put( + // Compile interface WITH plugin → static and default method bodies stripped + Map interfaceSources = new LinkedHashMap<>(); + interfaceSources.put( "example/SampleInterface.java", joinLines( "package example;", @@ -131,7 +133,11 @@ void stripsInterfaceDefaultAndStaticMethods() throws Exception { "}" ) ); - sources.put( + CompilationResult interfaceResult = compileImpl(interfaceSources, "", true); + + // Compile implementation WITHOUT plugin so its constructor is usable + Map implSources = new LinkedHashMap<>(); + implSources.put( "example/SampleInterfaceImpl.java", joinLines( "package example;", @@ -140,18 +146,25 @@ void stripsInterfaceDefaultAndStaticMethods() throws Exception { "}" ) ); - CompilationResult result = compile( - sources - ); + CompilationResult implResult = compileImpl(implSources, interfaceResult.classesDirectory.toString(), false); + + // Load both sets of classes together + URL[] urls = new URL[] { + interfaceResult.classesDirectory.toUri().toURL(), + implResult.classesDirectory.toUri().toURL() + }; + URLClassLoader classLoader = new URLClassLoader(urls, JavaRefPluginTest.class.getClassLoader()); - Class interfaceClass = result.loadClass("example.SampleInterface"); - Class implementationClass = result.loadClass("example.SampleInterfaceImpl"); + Class interfaceClass = Class.forName("example.SampleInterface", true, classLoader); + Class implementationClass = Class.forName("example.SampleInterfaceImpl", true, classLoader); + // Static method should throw InvocationTargetException staticFailure = assertThrows(InvocationTargetException.class, () -> interfaceClass.getMethod("helper").invoke(null)); assertTrue(staticFailure.getCause() instanceof UnsupportedOperationException); assertEquals(STRIPPED_MESSAGE, staticFailure.getCause().getMessage()); + // Default method should throw (dispatches to stripped default from the interface) Object instance = implementationClass.getConstructor().newInstance(); Method defaultMethod = implementationClass.getMethod("value"); InvocationTargetException defaultFailure = @@ -190,28 +203,39 @@ void pluginClassesTargetJava8Bytecode() throws Exception { } private CompilationResult compile(Map sources) throws IOException { + return compileImpl(sources, "", true); + } + + private CompilationResult compileImpl(Map sources, String extraClasspath, boolean withPlugin) throws IOException { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); DiagnosticCollector diagnostics = new DiagnosticCollector<>(); - Path sourceDirectory = Files.createDirectories(tempDir.resolve("src")); - Path classesDirectory = Files.createDirectories(tempDir.resolve("classes")); + Path sourceDirectory = Files.createDirectories(tempDir.resolve("src-" + System.nanoTime())); + Path classesDirectory = Files.createDirectories(tempDir.resolve("classes-" + System.nanoTime())); List sourceFiles = new ArrayList<>(); for (Map.Entry source : sources.entrySet()) { Path file = sourceDirectory.resolve(source.getKey()); Files.createDirectories(file.getParent()); - Files.writeString(file, source.getValue(), StandardCharsets.UTF_8); + Files.write(file, source.getValue().getBytes(StandardCharsets.UTF_8)); sourceFiles.add(file.toFile()); } + String classpath = System.getProperty("java.class.path"); + if (extraClasspath != null && !extraClasspath.isEmpty()) { + classpath = classpath + File.pathSeparator + extraClasspath; + } + try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, StandardCharsets.UTF_8)) { Iterable units = fileManager.getJavaFileObjectsFromFiles(sourceFiles); List options = new ArrayList(); options.add("-proc:none"); options.add("-classpath"); - options.add(System.getProperty("java.class.path")); + options.add(classpath); options.add("-d"); options.add(classesDirectory.toString()); - options.add("-Xplugin:" + JavaRefPlugin.NAME); + if (withPlugin) { + options.add("-Xplugin:" + JavaRefPlugin.NAME); + } Boolean success = compiler.getTask(null, fileManager, diagnostics, options, null, units).call(); assertTrue(Boolean.TRUE.equals(success), () -> diagnostics.getDiagnostics() @@ -251,9 +275,11 @@ private static int classFileMajorVersion(Class type) throws IOException { } private static final class CompilationResult { + private final Path classesDirectory; private final URLClassLoader classLoader; private CompilationResult(Path classesDirectory) throws IOException { + this.classesDirectory = classesDirectory; URL url = classesDirectory.toUri().toURL(); this.classLoader = new URLClassLoader(new URL[] { url }, JavaRefPluginTest.class.getClassLoader()); } @@ -261,10 +287,6 @@ private CompilationResult(Path classesDirectory) throws IOException { private Class loadClass(String name) throws Exception { return Class.forName(name, true, classLoader); } - - private URLClassLoader classLoader() { - return classLoader; - } } } From 3df8f91975ab93801e79ad446788ed48cafb7896 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sun, 31 May 2026 12:12:45 -0500 Subject: [PATCH 16/30] Added ignorePackage argument. Strip static ctors as well. Change to replacing method bodies with throw null, which causes NPE, but has less code. --- README.md | 14 +- build.gradle.kts | 5 + .../org/ikvm/javarefplugin/JavaRefPlugin.java | 38 ++++- .../javarefplugin/MethodBodyStripper.java | 31 ++-- .../ikvm/javarefplugin/JavaRefPluginTest.java | 133 +++++++++++++++--- 5 files changed, 183 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index f502249..ecfed3f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # java-ref-plugin -`java-ref-plugin` is a `javac` plugin that rewrites concrete method bodies into `NoSuchMethodError` throws for reference-only API artifacts. +`java-ref-plugin` is a `javac` plugin that rewrites concrete method bodies into failing stubs for reference-only API artifacts. ## Maven coordinates @@ -14,12 +14,24 @@ org.ikvm:java-ref-plugin - Plugin class: `org.ikvm.javarefplugin.JavaRefPlugin` - Automatic module name: `org.ikvm.javarefplugin` +### Plugin arguments + +- `ignorePackage=`: skips rewriting for classes in the package and its subpackages. Repeat the argument to ignore multiple package roots. + ## Building ```bash ./gradlew build ``` +## JVM setup + +- Gradle daemon: Java 17+ +- Java compilation target: Java 8 (toolchain) +- Test runtime: Java 8 (toolchain) + +If your shell defaults to Java 8, point `JAVA_HOME` at a Java 17 JDK before running Gradle. + ## Publishing The project publishes the Maven artifact `org.ikvm:java-ref-plugin`. diff --git a/build.gradle.kts b/build.gradle.kts index 9ce3e15..99812de 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,6 +14,10 @@ val java8Compiler = javaToolchains.compilerFor { languageVersion = JavaLanguageVersion.of(8) } +val java8Launcher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(8) +} + val java8ToolsJar = files(java8Compiler.map { it.metadata.installationPath.file("lib/tools.jar") }) dependencies { @@ -45,6 +49,7 @@ tasks.named("compileTestJava") { tasks.withType().configureEach { useJUnitPlatform() + javaLauncher = java8Launcher testLogging { exceptionFormat = TestExceptionFormat.FULL } diff --git a/src/main/java/org/ikvm/javarefplugin/JavaRefPlugin.java b/src/main/java/org/ikvm/javarefplugin/JavaRefPlugin.java index 51242a4..084132e 100644 --- a/src/main/java/org/ikvm/javarefplugin/JavaRefPlugin.java +++ b/src/main/java/org/ikvm/javarefplugin/JavaRefPlugin.java @@ -5,7 +5,9 @@ import com.sun.source.util.TaskEvent; import com.sun.source.util.TaskListener; import com.sun.tools.javac.api.BasicJavacTask; +import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.util.Context; +import java.util.LinkedHashSet; import java.util.Collections; import java.util.IdentityHashMap; import java.util.Set; @@ -21,6 +23,7 @@ public String getName() { @Override public void init(JavacTask task, String... args) { + Set ignoredPackages = parseIgnoredPackages(args); Context context = ((BasicJavacTask) task).getContext(); MethodBodyStripper stripper = new MethodBodyStripper(context); Set processedUnits = Collections.newSetFromMap(new IdentityHashMap<>()); @@ -37,11 +40,44 @@ public void finished(TaskEvent event) { } Object unit = event.getCompilationUnit(); - if (unit != null && processedUnits.add(unit)) { + if (unit != null && processedUnits.add(unit) && !shouldIgnoreUnit(unit, ignoredPackages)) { stripper.strip(unit); } } }); } + + private static Set parseIgnoredPackages(String... args) { + Set ignoredPackages = new LinkedHashSet<>(); + for (String arg : args) { + if (arg.startsWith("ignorePackage=")) { + String value = arg.substring("ignorePackage=".length()).trim(); + if (value.isEmpty()) { + throw new IllegalArgumentException("ignorePackage argument must not be empty."); + } + + ignoredPackages.add(value); + } + } + + return ignoredPackages; + } + + private static boolean shouldIgnoreUnit(Object unit, Set ignoredPackages) { + if (ignoredPackages.isEmpty() || !(unit instanceof JCTree.JCCompilationUnit)) { + return false; + } + + JCTree.JCCompilationUnit compilationUnit = (JCTree.JCCompilationUnit) unit; + JCTree.JCExpression packageNameExpression = compilationUnit.getPackageName(); + String packageName = packageNameExpression == null ? "" : packageNameExpression.toString(); + for (String ignoredPackage : ignoredPackages) { + if (packageName.equals(ignoredPackage) || packageName.startsWith(ignoredPackage + ".")) { + return true; + } + } + + return false; + } } diff --git a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java index c965b1f..14760bc 100644 --- a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java +++ b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java @@ -1,6 +1,7 @@ package org.ikvm.javarefplugin; import com.sun.tools.javac.code.Flags; +import com.sun.tools.javac.code.TypeTag; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.TreeMaker; import com.sun.tools.javac.tree.TreeTranslator; @@ -12,8 +13,6 @@ final class MethodBodyStripper extends TreeTranslator { - private static final String STRIPPED_MESSAGE = "Method body stripped from reference-only artifact."; - private final TreeMaker maker; private final Names names; private boolean inAnnotationType; @@ -55,13 +54,21 @@ private boolean shouldKeepBody(JCTree.JCMethodDecl tree) { return true; } - return (flags & Flags.SYNTHETIC) != 0 && !isConstructor(tree); + return (flags & Flags.SYNTHETIC) != 0 && !isInitializer(tree); } private boolean isConstructor(JCTree.JCMethodDecl tree) { return tree.name == names.init; } + private boolean isClassInitializer(JCTree.JCMethodDecl tree) { + return tree.name == names.clinit; + } + + private boolean isInitializer(JCTree.JCMethodDecl tree) { + return isConstructor(tree) || isClassInitializer(tree); + } + private List replacementStatements(JCTree.JCMethodDecl tree) { ListBuffer statements = new ListBuffer<>(); @@ -73,15 +80,7 @@ private List replacementStatements(JCTree.JCMethodDecl tree) } statements.append( - maker.Throw( - maker.NewClass( - null, - List.nil(), - unsupportedOperationExceptionType(), - List.of(maker.Literal(STRIPPED_MESSAGE)), - null - ) - ) + maker.Throw(maker.Literal(TypeTag.BOT, null)) ); return statements.toList(); @@ -117,13 +116,5 @@ private JCTree.JCStatement findConstructorInvocation(List st return null; } - private JCTree.JCExpression unsupportedOperationExceptionType() { - String[] elements = "java.lang.UnsupportedOperationException".split("\\."); - JCTree.JCExpression expression = maker.Ident(names.fromString(elements[0])); - for (int i = 1; i < elements.length; i++) { - expression = maker.Select(expression, names.fromString(elements[i])); - } - return expression; - } } diff --git a/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java index 4a05370..7097d75 100644 --- a/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java +++ b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java @@ -31,8 +31,6 @@ class JavaRefPluginTest { - private static final String STRIPPED_MESSAGE = "Method body stripped from reference-only artifact."; - @TempDir Path tempDir; @@ -67,13 +65,11 @@ void stripsStaticMethodsAndConstructorsWhileKeepingFields() throws Exception { InvocationTargetException staticFailure = assertThrows(InvocationTargetException.class, () -> sampleClass.getMethod("greeting").invoke(null)); - assertTrue(staticFailure.getCause() instanceof UnsupportedOperationException); - assertEquals(STRIPPED_MESSAGE, staticFailure.getCause().getMessage()); + assertTrue(staticFailure.getCause() instanceof NullPointerException); InvocationTargetException constructorFailure = assertThrows(InvocationTargetException.class, () -> sampleClass.getConstructor().newInstance()); - assertTrue(constructorFailure.getCause() instanceof UnsupportedOperationException); - assertEquals(STRIPPED_MESSAGE, constructorFailure.getCause().getMessage()); + assertTrue(constructorFailure.getCause() instanceof NullPointerException); } @Test @@ -109,8 +105,7 @@ void keepsExplicitConstructorChainingValid() throws Exception { Class childClass = result.loadClass("example.Child"); InvocationTargetException constructorFailure = assertThrows(InvocationTargetException.class, () -> childClass.getConstructor().newInstance()); - assertTrue(constructorFailure.getCause() instanceof UnsupportedOperationException); - assertEquals(STRIPPED_MESSAGE, constructorFailure.getCause().getMessage()); + assertTrue(constructorFailure.getCause() instanceof NullPointerException); } @Test @@ -161,16 +156,14 @@ void stripsInterfaceDefaultAndStaticMethods() throws Exception { // Static method should throw InvocationTargetException staticFailure = assertThrows(InvocationTargetException.class, () -> interfaceClass.getMethod("helper").invoke(null)); - assertTrue(staticFailure.getCause() instanceof UnsupportedOperationException); - assertEquals(STRIPPED_MESSAGE, staticFailure.getCause().getMessage()); + assertTrue(staticFailure.getCause() instanceof NullPointerException); // Default method should throw (dispatches to stripped default from the interface) Object instance = implementationClass.getConstructor().newInstance(); Method defaultMethod = implementationClass.getMethod("value"); InvocationTargetException defaultFailure = assertThrows(InvocationTargetException.class, () -> defaultMethod.invoke(instance)); - assertTrue(defaultFailure.getCause() instanceof UnsupportedOperationException); - assertEquals(STRIPPED_MESSAGE, defaultFailure.getCause().getMessage()); + assertTrue(defaultFailure.getCause() instanceof NullPointerException); } @Test @@ -192,8 +185,104 @@ void stripsImplicitDefaultConstructors() throws Exception { Class type = result.loadClass("example.ImplicitConstructor"); InvocationTargetException constructorFailure = assertThrows(InvocationTargetException.class, () -> type.getConstructor().newInstance()); - assertTrue(constructorFailure.getCause() instanceof UnsupportedOperationException); - assertEquals(STRIPPED_MESSAGE, constructorFailure.getCause().getMessage()); + assertTrue(constructorFailure.getCause() instanceof NullPointerException); + } + + @Test + void ignoresConfiguredPackage() throws Exception { + Map sources = new LinkedHashMap<>(); + sources.put( + "example/keep/Kept.java", + joinLines( + "package example.keep;", + "", + "public class Kept {", + " public static String value() {", + " return \"kept\";", + " }", + "}" + ) + ); + sources.put( + "example/strip/Stripped.java", + joinLines( + "package example.strip;", + "", + "public class Stripped {", + " public static String value() {", + " return \"stripped\";", + " }", + "}" + ) + ); + + CompilationResult result = compile(sources, "ignorePackage=example.keep"); + + Class keptClass = result.loadClass("example.keep.Kept"); + assertEquals("kept", keptClass.getMethod("value").invoke(null)); + + Class strippedClass = result.loadClass("example.strip.Stripped"); + InvocationTargetException strippedFailure = + assertThrows(InvocationTargetException.class, () -> strippedClass.getMethod("value").invoke(null)); + assertTrue(strippedFailure.getCause() instanceof NullPointerException); + } + + @Test + void ignoresMultipleConfiguredPackages() throws Exception { + Map sources = new LinkedHashMap<>(); + sources.put( + "example/keep/one/KeptOne.java", + joinLines( + "package example.keep.one;", + "", + "public class KeptOne {", + " public static String value() {", + " return \"one\";", + " }", + "}" + ) + ); + sources.put( + "example/keep/two/KeptTwo.java", + joinLines( + "package example.keep.two;", + "", + "public class KeptTwo {", + " public static String value() {", + " return \"two\";", + " }", + "}" + ) + ); + sources.put( + "example/strip/Stripped.java", + joinLines( + "package example.strip;", + "", + "public class Stripped {", + " public static String value() {", + " return \"strip\";", + " }", + "}" + ) + ); + + CompilationResult result = compile( + sources, + "ignorePackage=example.keep.one", + "ignorePackage=example.keep.two" + ); + + Class keptOneClass = result.loadClass("example.keep.one.KeptOne"); + assertEquals("one", keptOneClass.getMethod("value").invoke(null)); + + Class keptTwoClass = result.loadClass("example.keep.two.KeptTwo"); + assertEquals("two", keptTwoClass.getMethod("value").invoke(null)); + + Class strippedClass = result.loadClass("example.strip.Stripped"); + InvocationTargetException strippedFailure = + assertThrows(InvocationTargetException.class, () -> strippedClass.getMethod("value").invoke(null)); + assertTrue(strippedFailure.getCause() instanceof NullPointerException); } @Test @@ -206,7 +295,11 @@ private CompilationResult compile(Map sources) throws IOExceptio return compileImpl(sources, "", true); } - private CompilationResult compileImpl(Map sources, String extraClasspath, boolean withPlugin) throws IOException { + private CompilationResult compile(Map sources, String... pluginArgs) throws IOException { + return compileImpl(sources, "", true, pluginArgs); + } + + private CompilationResult compileImpl(Map sources, String extraClasspath, boolean withPlugin, String... pluginArgs) throws IOException { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); DiagnosticCollector diagnostics = new DiagnosticCollector<>(); Path sourceDirectory = Files.createDirectories(tempDir.resolve("src-" + System.nanoTime())); @@ -234,7 +327,7 @@ private CompilationResult compileImpl(Map sources, String extraC options.add("-d"); options.add(classesDirectory.toString()); if (withPlugin) { - options.add("-Xplugin:" + JavaRefPlugin.NAME); + options.add(pluginOption(pluginArgs)); } Boolean success = compiler.getTask(null, fileManager, diagnostics, options, null, units).call(); @@ -247,6 +340,14 @@ private CompilationResult compileImpl(Map sources, String extraC return new CompilationResult(classesDirectory); } + private static String pluginOption(String... pluginArgs) { + if (pluginArgs == null || pluginArgs.length == 0) { + return "-Xplugin:" + JavaRefPlugin.NAME; + } + + return "-Xplugin:" + JavaRefPlugin.NAME + " " + String.join(" ", pluginArgs); + } + private static String formatDiagnostic(Diagnostic diagnostic) { String source = diagnostic.getSource() == null ? "" : diagnostic.getSource().getName(); return source + ":" + diagnostic.getLineNumber() + ": " + diagnostic.getMessage(null); From 89ee466aa8dd82b871dadd529cc1b1aa9aeade27 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sun, 31 May 2026 13:03:03 -0500 Subject: [PATCH 17/30] Log ignored classes. --- README.md | 2 +- .../org/ikvm/javarefplugin/JavaRefPlugin.java | 65 ++++++++++++++++--- .../ikvm/javarefplugin/JavaRefPluginTest.java | 48 ++++++++++++++ 3 files changed, 105 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ecfed3f..f65e38d 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ org.ikvm:java-ref-plugin ### Plugin arguments -- `ignorePackage=`: skips rewriting for classes in the package and its subpackages. Repeat the argument to ignore multiple package roots. +- `ignorePackage=`: skips rewriting for classes in the package and its subpackages. Repeat the argument to ignore multiple package roots. Ignored classes are logged as `javac` notices during compilation. ## Building diff --git a/src/main/java/org/ikvm/javarefplugin/JavaRefPlugin.java b/src/main/java/org/ikvm/javarefplugin/JavaRefPlugin.java index 084132e..258594c 100644 --- a/src/main/java/org/ikvm/javarefplugin/JavaRefPlugin.java +++ b/src/main/java/org/ikvm/javarefplugin/JavaRefPlugin.java @@ -7,9 +7,10 @@ import com.sun.tools.javac.api.BasicJavacTask; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.util.Context; -import java.util.LinkedHashSet; +import com.sun.tools.javac.util.Log; import java.util.Collections; import java.util.IdentityHashMap; +import java.util.LinkedHashSet; import java.util.Set; public final class JavaRefPlugin implements Plugin { @@ -25,6 +26,7 @@ public String getName() { public void init(JavacTask task, String... args) { Set ignoredPackages = parseIgnoredPackages(args); Context context = ((BasicJavacTask) task).getContext(); + Log log = Log.instance(context); MethodBodyStripper stripper = new MethodBodyStripper(context); Set processedUnits = Collections.newSetFromMap(new IdentityHashMap<>()); @@ -40,9 +42,17 @@ public void finished(TaskEvent event) { } Object unit = event.getCompilationUnit(); - if (unit != null && processedUnits.add(unit) && !shouldIgnoreUnit(unit, ignoredPackages)) { - stripper.strip(unit); + if (unit == null || !processedUnits.add(unit)) { + return; + } + + String ignoredPackage = findIgnoredPackage(unit, ignoredPackages); + if (ignoredPackage != null) { + logIgnoredUnit(log, unit, ignoredPackage); + return; } + + stripper.strip(unit); } }); } @@ -63,21 +73,58 @@ private static Set parseIgnoredPackages(String... args) { return ignoredPackages; } - private static boolean shouldIgnoreUnit(Object unit, Set ignoredPackages) { + private static String findIgnoredPackage(Object unit, Set ignoredPackages) { if (ignoredPackages.isEmpty() || !(unit instanceof JCTree.JCCompilationUnit)) { - return false; + return null; } JCTree.JCCompilationUnit compilationUnit = (JCTree.JCCompilationUnit) unit; - JCTree.JCExpression packageNameExpression = compilationUnit.getPackageName(); - String packageName = packageNameExpression == null ? "" : packageNameExpression.toString(); + String packageName = packageName(compilationUnit); for (String ignoredPackage : ignoredPackages) { if (packageName.equals(ignoredPackage) || packageName.startsWith(ignoredPackage + ".")) { - return true; + return ignoredPackage; + } + } + + return null; + } + + private static void logIgnoredUnit(Log log, Object unit, String ignoredPackage) { + if (!(unit instanceof JCTree.JCCompilationUnit)) { + return; + } + + JCTree.JCCompilationUnit compilationUnit = (JCTree.JCCompilationUnit) unit; + String packageName = packageName(compilationUnit); + boolean loggedType = false; + for (JCTree definition : compilationUnit.defs) { + if (definition instanceof JCTree.JCClassDecl) { + JCTree.JCClassDecl classDecl = (JCTree.JCClassDecl) definition; + log.printRawLines( + Log.WriterKind.NOTICE, + NAME + ": ignoring " + qualifiedName(packageName, classDecl.name.toString()) + + " due to ignorePackage=" + ignoredPackage + ); + loggedType = true; } } - return false; + if (!loggedType) { + String sourceName = compilationUnit.getSourceFile() == null ? "" : compilationUnit.getSourceFile().getName(); + log.printRawLines( + Log.WriterKind.NOTICE, + NAME + ": ignoring compilation unit " + sourceName + " due to ignorePackage=" + ignoredPackage + ); + } + } + + private static String packageName(JCTree.JCCompilationUnit compilationUnit) { + JCTree.JCExpression packageNameExpression = compilationUnit.getPackageName(); + return packageNameExpression == null ? "" : packageNameExpression.toString(); + } + + private static String qualifiedName(String packageName, String simpleName) { + return packageName.isEmpty() ? simpleName : packageName + "." + simpleName; } } diff --git a/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java index 7097d75..1211f76 100644 --- a/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java +++ b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java @@ -5,9 +5,11 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.io.PrintStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; @@ -285,6 +287,30 @@ void ignoresMultipleConfiguredPackages() throws Exception { assertTrue(strippedFailure.getCause() instanceof NullPointerException); } + @Test + void logsIgnoredClasses() throws Exception { + Map sources = new LinkedHashMap<>(); + sources.put( + "example/keep/Kept.java", + joinLines( + "package example.keep;", + "", + "public class Kept {", + " public static String value() {", + " return \"kept\";", + " }", + "}" + ) + ); + + CompilationInvocation invocation = compileCapturingOutput(sources, "ignorePackage=example.keep"); + + assertTrue(invocation.output.contains("JavaRef: ignoring example.keep.Kept due to ignorePackage=example.keep")); + + Class keptClass = invocation.result.loadClass("example.keep.Kept"); + assertEquals("kept", keptClass.getMethod("value").invoke(null)); + } + @Test void pluginClassesTargetJava8Bytecode() throws Exception { assertEquals(52, classFileMajorVersion(JavaRefPlugin.class)); @@ -299,6 +325,18 @@ private CompilationResult compile(Map sources, String... pluginA return compileImpl(sources, "", true, pluginArgs); } + private CompilationInvocation compileCapturingOutput(Map sources, String... pluginArgs) throws IOException { + PrintStream originalErr = System.err; + ByteArrayOutputStream capturedErr = new ByteArrayOutputStream(); + try { + System.setErr(new PrintStream(capturedErr, true, StandardCharsets.UTF_8.name())); + CompilationResult result = compileImpl(sources, "", true, pluginArgs); + return new CompilationInvocation(result, capturedErr.toString(StandardCharsets.UTF_8.name())); + } finally { + System.setErr(originalErr); + } + } + private CompilationResult compileImpl(Map sources, String extraClasspath, boolean withPlugin, String... pluginArgs) throws IOException { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); DiagnosticCollector diagnostics = new DiagnosticCollector<>(); @@ -389,5 +427,15 @@ private Class loadClass(String name) throws Exception { return Class.forName(name, true, classLoader); } } + + private static final class CompilationInvocation { + private final CompilationResult result; + private final String output; + + private CompilationInvocation(CompilationResult result, String output) { + this.result = result; + this.output = output; + } + } } From 61c9203199369ac3eb09a6cbdaf4deb0432f6ae1 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sun, 31 May 2026 13:22:31 -0500 Subject: [PATCH 18/30] Remove detecting for certain method types. --- .../org/ikvm/javarefplugin/MethodBodyStripper.java | 13 +------------ .../org/ikvm/javarefplugin/JavaRefPluginTest.java | 1 + 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java index 14760bc..096ea32 100644 --- a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java +++ b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java @@ -50,24 +50,13 @@ public void visitMethodDef(JCTree.JCMethodDecl tree) { private boolean shouldKeepBody(JCTree.JCMethodDecl tree) { long flags = tree.mods.flags; - if ((flags & (Flags.ABSTRACT | Flags.NATIVE)) != 0) { - return true; - } - - return (flags & Flags.SYNTHETIC) != 0 && !isInitializer(tree); + return (flags & (Flags.ABSTRACT | Flags.NATIVE)) != 0; } private boolean isConstructor(JCTree.JCMethodDecl tree) { return tree.name == names.init; } - private boolean isClassInitializer(JCTree.JCMethodDecl tree) { - return tree.name == names.clinit; - } - - private boolean isInitializer(JCTree.JCMethodDecl tree) { - return isConstructor(tree) || isClassInitializer(tree); - } private List replacementStatements(JCTree.JCMethodDecl tree) { ListBuffer statements = new ListBuffer<>(); diff --git a/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java index 1211f76..86b0762 100644 --- a/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java +++ b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java @@ -413,6 +413,7 @@ private static int classFileMajorVersion(Class type) throws IOException { } } + private static final class CompilationResult { private final Path classesDirectory; private final URLClassLoader classLoader; From acbeb99ab5c384b69e870842bdc7c09a18a31a6d Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sun, 31 May 2026 13:43:09 -0500 Subject: [PATCH 19/30] Rewrite static ctors to set all fields to default value. --- README.md | 10 +++ .../javarefplugin/MethodBodyStripper.java | 61 ++++++++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f65e38d..6e0ef1e 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,16 @@ org.ikvm:java-ref-plugin - Plugin class: `org.ikvm.javarefplugin.JavaRefPlugin` - Automatic module name: `org.ikvm.javarefplugin` +## Plugin behavior + +The plugin rewrites method bodies to throw `NullPointerException` on invocation: + +- **Instance methods**: replaced with `throw null`. +- **Constructors**: preserved `this()/super()` chaining, then `throw null`. +- **Static initializers**: replaced with minimal field assignments (sets static fields to type-appropriate defaults: `0`, `false`, or `null`). This drastically reduces bytecode size while ensuring the class loads correctly. +- **Abstract/native methods**: bodies left unchanged (they have no bodies). +- **Synthetic bridge methods**: rewritten like regular methods (bridges still exist as method symbols). + ### Plugin arguments - `ignorePackage=`: skips rewriting for classes in the package and its subpackages. Repeat the argument to ignore multiple package roots. Ignored classes are logged as `javac` notices during compilation. diff --git a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java index 096ea32..01830a4 100644 --- a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java +++ b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java @@ -30,11 +30,67 @@ void strip(Object compilationUnit) { public void visitClassDef(JCTree.JCClassDecl tree) { boolean previousInAnnotationType = inAnnotationType; inAnnotationType = (tree.mods.flags & Flags.ANNOTATION) != 0; + super.visitClassDef(tree); + + // Rewrite static initializers to set fields to default values + if (tree.defs != null && !inAnnotationType) { + for (JCTree def : tree.defs) { + if (def instanceof JCTree.JCBlock) { + JCTree.JCBlock block = (JCTree.JCBlock) def; + if ((block.flags & Flags.STATIC) != 0) { + // Replace with minimal field assignments + block.stats = generateDefaultFieldAssignments(tree); + } + } + } + } + inAnnotationType = previousInAnnotationType; result = tree; } + private List generateDefaultFieldAssignments(JCTree.JCClassDecl classTree) { + ListBuffer statements = new ListBuffer<>(); + + if (classTree.defs == null) { + return statements.toList(); + } + + // Collect all static fields + for (JCTree def : classTree.defs) { + if (def instanceof JCTree.JCVariableDecl) { + JCTree.JCVariableDecl varDecl = (JCTree.JCVariableDecl) def; + if ((varDecl.mods.flags & Flags.STATIC) != 0) { + // Generate assignment: field = defaultValue; + JCTree.JCExpression defaultValue = generateDefaultValue(varDecl.vartype); + JCTree.JCAssign assignment = maker.Assign( + maker.Ident(varDecl.name), + defaultValue + ); + statements.append(maker.Exec(assignment)); + } + } + } + + return statements.toList(); + } + + private JCTree.JCExpression generateDefaultValue(JCTree.JCExpression typeExpr) { + // For simplicity: 0 for numeric types, false for boolean, null for everything else + String typeStr = typeExpr.toString(); + if ("int".equals(typeStr) || "byte".equals(typeStr) || "short".equals(typeStr) || "long".equals(typeStr) + || "float".equals(typeStr) || "double".equals(typeStr)) { + return maker.Literal(TypeTag.INT, 0); + } else if ("boolean".equals(typeStr)) { + return maker.Literal(TypeTag.BOOLEAN, Boolean.FALSE); + } else if ("char".equals(typeStr)) { + return maker.Literal(TypeTag.CHAR, 0); + } + // Reference type or unknown: null + return maker.Literal(TypeTag.BOT, null); + } + @Override public void visitMethodDef(JCTree.JCMethodDecl tree) { super.visitMethodDef(tree); @@ -57,6 +113,10 @@ private boolean isConstructor(JCTree.JCMethodDecl tree) { return tree.name == names.init; } + private boolean isClassInitializer(JCTree.JCMethodDecl tree) { + return tree.name == names.clinit; + } + private List replacementStatements(JCTree.JCMethodDecl tree) { ListBuffer statements = new ListBuffer<>(); @@ -106,4 +166,3 @@ private JCTree.JCStatement findConstructorInvocation(List st } } - From 877c8e93eeccc632490661702afeeee7b2556a09 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sun, 31 May 2026 13:53:28 -0500 Subject: [PATCH 20/30] Ignore setting values with CONSTANTS. --- .../javarefplugin/MethodBodyStripper.java | 10 ++++- .../ikvm/javarefplugin/JavaRefPluginTest.java | 44 ++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java index 01830a4..b5eeb55 100644 --- a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java +++ b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java @@ -57,11 +57,19 @@ private List generateDefaultFieldAssignments(JCTree.JCClassD return statements.toList(); } - // Collect all static fields + // Collect all static fields that can be safely reassigned + // (skip final fields that have an initializer - they have compile-time constant values) for (JCTree def : classTree.defs) { if (def instanceof JCTree.JCVariableDecl) { JCTree.JCVariableDecl varDecl = (JCTree.JCVariableDecl) def; if ((varDecl.mods.flags & Flags.STATIC) != 0) { + // Skip if final AND has an initializer (compile-time constant) + boolean isFinal = (varDecl.mods.flags & Flags.FINAL) != 0; + boolean hasInitializer = varDecl.init != null; + if (isFinal && hasInitializer) { + continue; // Skip final fields with initializers + } + // Generate assignment: field = defaultValue; JCTree.JCExpression defaultValue = generateDefaultValue(varDecl.vartype); JCTree.JCAssign assignment = maker.Assign( diff --git a/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java index 86b0762..806f06c 100644 --- a/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java +++ b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java @@ -438,5 +438,47 @@ private CompilationInvocation(CompilationResult result, String output) { this.output = output; } } -} + @Test + void staticInitializerOptimizationHandlesAllFieldTypes() throws Exception { + Map sources = new LinkedHashMap<>(); + sources.put( + "example/StaticFieldTypes.java", + joinLines( + "package example;", + "", + "public class StaticFieldTypes {", + " static final long CONSTANT = 42L;", + " static final String uninit;", + " static int mutable;", + " static {", + " uninit = \"initialized\";", + " mutable = 100;", + " }", + " public static long getConstant() { return CONSTANT; }", + " public static String getUninit() { return uninit; }", + " public static int getMutable() { return mutable; }", + "}" + ) + ); + + // Just verify the plugin can compile this without errors + // (the key test is that final fields with initializers don't get reassigned) + CompilationResult result = compile(sources); + Class type = result.loadClass("example.StaticFieldTypes"); + + // All methods should be stripped and throw NullPointerException + InvocationTargetException constantFailure = + assertThrows(InvocationTargetException.class, () -> type.getMethod("getConstant").invoke(null)); + assertTrue(constantFailure.getCause() instanceof NullPointerException); + + InvocationTargetException uninitFailure = + assertThrows(InvocationTargetException.class, () -> type.getMethod("getUninit").invoke(null)); + assertTrue(uninitFailure.getCause() instanceof NullPointerException); + + InvocationTargetException mutableFailure = + assertThrows(InvocationTargetException.class, () -> type.getMethod("getMutable").invoke(null)); + assertTrue(mutableFailure.getCause() instanceof NullPointerException); + } + +} From 494074bd655adc4878753c75e0c490a76c4674b2 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sun, 31 May 2026 13:54:46 -0500 Subject: [PATCH 21/30] Ignore .idea settings. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5c4ac6d..ed5a25a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ build # Ignore Kotlin plugin data .kotlin +/.idea/ From df59c96344871fbaba8c784534c5aa6d6547d0eb Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sun, 31 May 2026 14:01:23 -0500 Subject: [PATCH 22/30] Don't assign non static. --- .../javarefplugin/MethodBodyStripper.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java index b5eeb55..333551b 100644 --- a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java +++ b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java @@ -57,20 +57,24 @@ private List generateDefaultFieldAssignments(JCTree.JCClassD return statements.toList(); } - // Collect all static fields that can be safely reassigned - // (skip final fields that have an initializer - they have compile-time constant values) + // Assign default values to: + // 1. All static non-final fields + // 2. static final fields WITHOUT field initializers (must assign in static {} to avoid "not initialized" error) + // Do NOT assign to: static final fields WITH field initializers (they already have values) for (JCTree def : classTree.defs) { if (def instanceof JCTree.JCVariableDecl) { JCTree.JCVariableDecl varDecl = (JCTree.JCVariableDecl) def; - if ((varDecl.mods.flags & Flags.STATIC) != 0) { - // Skip if final AND has an initializer (compile-time constant) - boolean isFinal = (varDecl.mods.flags & Flags.FINAL) != 0; - boolean hasInitializer = varDecl.init != null; - if (isFinal && hasInitializer) { - continue; // Skip final fields with initializers - } + boolean isStatic = (varDecl.mods.flags & Flags.STATIC) != 0; + boolean isFinal = (varDecl.mods.flags & Flags.FINAL) != 0; + boolean hasInitializer = varDecl.init != null; + + // Skip final fields that already have initializers + if (isFinal && hasInitializer) { + continue; + } - // Generate assignment: field = defaultValue; + // Assign all other static fields + if (isStatic) { JCTree.JCExpression defaultValue = generateDefaultValue(varDecl.vartype); JCTree.JCAssign assignment = maker.Assign( maker.Ident(varDecl.name), From c82780b3a08c424c803d158f2c79c1ce7aa8db01 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sun, 31 May 2026 14:04:02 -0500 Subject: [PATCH 23/30] Change order. --- .../javarefplugin/MethodBodyStripper.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java index 333551b..ede8a7b 100644 --- a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java +++ b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java @@ -40,7 +40,7 @@ public void visitClassDef(JCTree.JCClassDecl tree) { JCTree.JCBlock block = (JCTree.JCBlock) def; if ((block.flags & Flags.STATIC) != 0) { // Replace with minimal field assignments - block.stats = generateDefaultFieldAssignments(tree); + block.stats = generateDefaultStaticFieldAssignments(tree); } } } @@ -50,7 +50,7 @@ public void visitClassDef(JCTree.JCClassDecl tree) { result = tree; } - private List generateDefaultFieldAssignments(JCTree.JCClassDecl classTree) { + private List generateDefaultStaticFieldAssignments(JCTree.JCClassDecl classTree) { ListBuffer statements = new ListBuffer<>(); if (classTree.defs == null) { @@ -65,6 +65,12 @@ private List generateDefaultFieldAssignments(JCTree.JCClassD if (def instanceof JCTree.JCVariableDecl) { JCTree.JCVariableDecl varDecl = (JCTree.JCVariableDecl) def; boolean isStatic = (varDecl.mods.flags & Flags.STATIC) != 0; + + // Only process static fields + if (!isStatic) { + continue; + } + boolean isFinal = (varDecl.mods.flags & Flags.FINAL) != 0; boolean hasInitializer = varDecl.init != null; @@ -74,14 +80,12 @@ private List generateDefaultFieldAssignments(JCTree.JCClassD } // Assign all other static fields - if (isStatic) { - JCTree.JCExpression defaultValue = generateDefaultValue(varDecl.vartype); - JCTree.JCAssign assignment = maker.Assign( - maker.Ident(varDecl.name), - defaultValue - ); - statements.append(maker.Exec(assignment)); - } + JCTree.JCExpression defaultValue = generateDefaultValue(varDecl.vartype); + JCTree.JCAssign assignment = maker.Assign( + maker.Ident(varDecl.name), + defaultValue + ); + statements.append(maker.Exec(assignment)); } } From 5abd15badf05c61741180ed213c579acd79c4de1 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sun, 31 May 2026 14:05:13 -0500 Subject: [PATCH 24/30] Not with initializer. --- .../ikvm/javarefplugin/MethodBodyStripper.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java index ede8a7b..73f4f32 100644 --- a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java +++ b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java @@ -57,10 +57,10 @@ private List generateDefaultStaticFieldAssignments(JCTree.JC return statements.toList(); } - // Assign default values to: - // 1. All static non-final fields - // 2. static final fields WITHOUT field initializers (must assign in static {} to avoid "not initialized" error) - // Do NOT assign to: static final fields WITH field initializers (they already have values) + // Assign default values to static fields WITHOUT initializers: + // 1. static non-final fields without initializers + // 2. static final fields without initializers (must assign in static {} block) + // Do NOT assign to any static fields that already have initializers for (JCTree def : classTree.defs) { if (def instanceof JCTree.JCVariableDecl) { JCTree.JCVariableDecl varDecl = (JCTree.JCVariableDecl) def; @@ -71,15 +71,14 @@ private List generateDefaultStaticFieldAssignments(JCTree.JC continue; } - boolean isFinal = (varDecl.mods.flags & Flags.FINAL) != 0; boolean hasInitializer = varDecl.init != null; - // Skip final fields that already have initializers - if (isFinal && hasInitializer) { + // Skip any field that already has an initializer + if (hasInitializer) { continue; } - // Assign all other static fields + // Assign default to static fields without initializers JCTree.JCExpression defaultValue = generateDefaultValue(varDecl.vartype); JCTree.JCAssign assignment = maker.Assign( maker.Ident(varDecl.name), From dc16680a93232ebd5ff1184211b56e60ec6981a1 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sun, 31 May 2026 14:24:21 -0500 Subject: [PATCH 25/30] Booleans should be zero. --- .../javarefplugin/MethodBodyStripper.java | 12 +-- .../ikvm/javarefplugin/JavaRefPluginTest.java | 74 +++++++++++++++++++ 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java index 73f4f32..5b1fceb 100644 --- a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java +++ b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java @@ -57,28 +57,23 @@ private List generateDefaultStaticFieldAssignments(JCTree.JC return statements.toList(); } - // Assign default values to static fields WITHOUT initializers: - // 1. static non-final fields without initializers - // 2. static final fields without initializers (must assign in static {} block) - // Do NOT assign to any static fields that already have initializers + // Assign defaults to static fields without initializers. + // Skip fields that have initializers - they are already assigned and shouldn't be reassigned. for (JCTree def : classTree.defs) { if (def instanceof JCTree.JCVariableDecl) { JCTree.JCVariableDecl varDecl = (JCTree.JCVariableDecl) def; boolean isStatic = (varDecl.mods.flags & Flags.STATIC) != 0; - // Only process static fields if (!isStatic) { continue; } boolean hasInitializer = varDecl.init != null; - // Skip any field that already has an initializer if (hasInitializer) { continue; } - // Assign default to static fields without initializers JCTree.JCExpression defaultValue = generateDefaultValue(varDecl.vartype); JCTree.JCAssign assignment = maker.Assign( maker.Ident(varDecl.name), @@ -98,7 +93,8 @@ private JCTree.JCExpression generateDefaultValue(JCTree.JCExpression typeExpr) { || "float".equals(typeStr) || "double".equals(typeStr)) { return maker.Literal(TypeTag.INT, 0); } else if ("boolean".equals(typeStr)) { - return maker.Literal(TypeTag.BOOLEAN, Boolean.FALSE); + // javac expects boolean literals as numeric payload (0/1) for TypeTag.BOOLEAN + return maker.Literal(TypeTag.BOOLEAN, 0); } else if ("char".equals(typeStr)) { return maker.Literal(TypeTag.CHAR, 0); } diff --git a/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java index 806f06c..d833511 100644 --- a/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java +++ b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java @@ -1,6 +1,7 @@ package org.ikvm.javarefplugin; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -481,4 +482,77 @@ void staticInitializerOptimizationHandlesAllFieldTypes() throws Exception { assertTrue(mutableFailure.getCause() instanceof NullPointerException); } + @Test + void staticFieldsAllCombinations() throws Exception { + Map sources = new LinkedHashMap<>(); + sources.put( + "example/AllStaticFields.java", + joinLines( + "package example;", + "", + "public class AllStaticFields {", + " // static final WITH initializer - should NOT be reassigned", + " static final long WITH_INIT_FINAL = 42L;", + "", + " // static final WITHOUT initializer - MUST be assigned", + " static final String WITHOUT_INIT_FINAL;", + "", + " // static non-final WITH initializer - should NOT be reassigned?", + " static int WITH_INIT_MUTABLE = 100;", + "", + " // static non-final WITHOUT initializer - should be assigned to 0", + " static int WITHOUT_INIT_MUTABLE;", + "", + " static {", + " WITHOUT_INIT_FINAL = \"initialized\";", + " WITHOUT_INIT_MUTABLE = 200;", + " }", + "", + " public static void dummy() { }", + "}" + ) + ); + + // Just verify the plugin can compile this without errors + CompilationResult result = compile(sources); + assertNotNull(result.loadClass("example.AllStaticFields")); + } + + @Test + void staticBooleanFieldsCombinationsAreHandled() throws Exception { + Map sources = new LinkedHashMap<>(); + sources.put( + "example/StaticBooleanFields.java", + joinLines( + "package example;", + "", + "public class StaticBooleanFields {", + " static final boolean WITH_INIT_FINAL = true;", + " static final boolean WITHOUT_INIT_FINAL;", + " static boolean WITH_INIT_MUTABLE = true;", + " static boolean WITHOUT_INIT_MUTABLE;", + "", + " static {", + " WITHOUT_INIT_FINAL = true;", + " WITHOUT_INIT_MUTABLE = true;", + " }", + "}" + ) + ); + + CompilationResult result = compile(sources); + Class type = result.loadClass("example.StaticBooleanFields"); + + assertTrue(getStaticBoolean(type, "WITH_INIT_FINAL")); + assertFalse(getStaticBoolean(type, "WITHOUT_INIT_FINAL")); + assertTrue(getStaticBoolean(type, "WITH_INIT_MUTABLE")); + assertFalse(getStaticBoolean(type, "WITHOUT_INIT_MUTABLE")); + } + + private static boolean getStaticBoolean(Class type, String fieldName) throws Exception { + java.lang.reflect.Field field = type.getDeclaredField(fieldName); + field.setAccessible(true); + return field.getBoolean(null); + } + } From a241183fe1c7d86bec03f6e17cd7c5f2199593f4 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sun, 31 May 2026 14:39:00 -0500 Subject: [PATCH 26/30] Java can have multiple static{} blocks. We just strip them all and regenerate one if needed. --- .../javarefplugin/MethodBodyStripper.java | 23 +++++++++------- .../ikvm/javarefplugin/JavaRefPluginTest.java | 26 +++++++++++++++++++ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java index 5b1fceb..e96afc8 100644 --- a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java +++ b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java @@ -33,17 +33,23 @@ public void visitClassDef(JCTree.JCClassDecl tree) { super.visitClassDef(tree); - // Rewrite static initializers to set fields to default values + // Strip all original static initializers and synthesize a single minimal one when needed. if (tree.defs != null && !inAnnotationType) { + List rewrittenAssignments = generateDefaultStaticFieldAssignments(tree); + ListBuffer rewrittenDefs = new ListBuffer<>(); + for (JCTree def : tree.defs) { - if (def instanceof JCTree.JCBlock) { - JCTree.JCBlock block = (JCTree.JCBlock) def; - if ((block.flags & Flags.STATIC) != 0) { - // Replace with minimal field assignments - block.stats = generateDefaultStaticFieldAssignments(tree); - } + if (def instanceof JCTree.JCBlock && ((((JCTree.JCBlock) def).flags & Flags.STATIC) != 0)) { + continue; } + rewrittenDefs.append(def); + } + + if (!rewrittenAssignments.isEmpty()) { + rewrittenDefs.append(maker.Block(Flags.STATIC, rewrittenAssignments)); } + + tree.defs = rewrittenDefs.toList(); } inAnnotationType = previousInAnnotationType; @@ -62,14 +68,13 @@ private List generateDefaultStaticFieldAssignments(JCTree.JC for (JCTree def : classTree.defs) { if (def instanceof JCTree.JCVariableDecl) { JCTree.JCVariableDecl varDecl = (JCTree.JCVariableDecl) def; - boolean isStatic = (varDecl.mods.flags & Flags.STATIC) != 0; + boolean isStatic = (varDecl.mods.flags & Flags.STATIC) != 0; if (!isStatic) { continue; } boolean hasInitializer = varDecl.init != null; - if (hasInitializer) { continue; } diff --git a/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java index d833511..dd76a02 100644 --- a/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java +++ b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java @@ -549,6 +549,32 @@ void staticBooleanFieldsCombinationsAreHandled() throws Exception { assertFalse(getStaticBoolean(type, "WITHOUT_INIT_MUTABLE")); } + @Test + void staticFinalWithoutInitializerWithMultipleStaticBlocksAssignedOnce() throws Exception { + Map sources = new LinkedHashMap<>(); + sources.put( + "example/MultipleStaticBlocks.java", + joinLines( + "package example;", + "", + "public class MultipleStaticBlocks {", + " static final boolean FLAG;", + " static {", + " System.out.println(\"first\");", + " }", + " static {", + " System.out.println(\"second\");", + " }", + "}" + ) + ); + + CompilationResult result = compile(sources); + Class type = result.loadClass("example.MultipleStaticBlocks"); + assertNotNull(type); + assertFalse(getStaticBoolean(type, "FLAG")); + } + private static boolean getStaticBoolean(Class type, String fieldName) throws Exception { java.lang.reflect.Field field = type.getDeclaredField(fieldName); field.setAccessible(true); From 5c6ceda96171ca56fae273fd22c8ab08c2f89efe Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sun, 31 May 2026 14:42:06 -0500 Subject: [PATCH 27/30] Add test with two static blcks. --- .../ikvm/javarefplugin/JavaRefPluginTest.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java index dd76a02..01cea4a 100644 --- a/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java +++ b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java @@ -575,10 +575,47 @@ void staticFinalWithoutInitializerWithMultipleStaticBlocksAssignedOnce() throws assertFalse(getStaticBoolean(type, "FLAG")); } + @Test + void stripsClassWithTwoStaticBlocks() throws Exception { + Map sources = new LinkedHashMap<>(); + sources.put( + "example/TwoStaticBlocks.java", + joinLines( + "package example;", + "", + "public class TwoStaticBlocks {", + " static final int REQUIRED;", + " static int MUTABLE;", + "", + " static {", + " MUTABLE = 123;", + " }", + "", + " static {", + " REQUIRED = 7;", + " }", + "}", + "" + ) + ); + + CompilationResult result = compile(sources); + Class type = result.loadClass("example.TwoStaticBlocks"); + assertNotNull(type); + assertEquals(0, getStaticInt(type, "REQUIRED")); + assertEquals(0, getStaticInt(type, "MUTABLE")); + } + private static boolean getStaticBoolean(Class type, String fieldName) throws Exception { java.lang.reflect.Field field = type.getDeclaredField(fieldName); field.setAccessible(true); return field.getBoolean(null); } + private static int getStaticInt(Class type, String fieldName) throws Exception { + java.lang.reflect.Field field = type.getDeclaredField(fieldName); + field.setAccessible(true); + return field.getInt(null); + } + } From 205644137293f59cba040e48212eaa25520ba808 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sun, 31 May 2026 15:08:30 -0500 Subject: [PATCH 28/30] Revert. Just preserve static ctors. Too complicated right now. --- .../javarefplugin/MethodBodyStripper.java | 74 ------------------- .../ikvm/javarefplugin/JavaRefPluginTest.java | 42 +++++++++-- 2 files changed, 36 insertions(+), 80 deletions(-) diff --git a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java index e96afc8..57bcd8d 100644 --- a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java +++ b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java @@ -33,79 +33,10 @@ public void visitClassDef(JCTree.JCClassDecl tree) { super.visitClassDef(tree); - // Strip all original static initializers and synthesize a single minimal one when needed. - if (tree.defs != null && !inAnnotationType) { - List rewrittenAssignments = generateDefaultStaticFieldAssignments(tree); - ListBuffer rewrittenDefs = new ListBuffer<>(); - - for (JCTree def : tree.defs) { - if (def instanceof JCTree.JCBlock && ((((JCTree.JCBlock) def).flags & Flags.STATIC) != 0)) { - continue; - } - rewrittenDefs.append(def); - } - - if (!rewrittenAssignments.isEmpty()) { - rewrittenDefs.append(maker.Block(Flags.STATIC, rewrittenAssignments)); - } - - tree.defs = rewrittenDefs.toList(); - } - inAnnotationType = previousInAnnotationType; result = tree; } - private List generateDefaultStaticFieldAssignments(JCTree.JCClassDecl classTree) { - ListBuffer statements = new ListBuffer<>(); - - if (classTree.defs == null) { - return statements.toList(); - } - - // Assign defaults to static fields without initializers. - // Skip fields that have initializers - they are already assigned and shouldn't be reassigned. - for (JCTree def : classTree.defs) { - if (def instanceof JCTree.JCVariableDecl) { - JCTree.JCVariableDecl varDecl = (JCTree.JCVariableDecl) def; - - boolean isStatic = (varDecl.mods.flags & Flags.STATIC) != 0; - if (!isStatic) { - continue; - } - - boolean hasInitializer = varDecl.init != null; - if (hasInitializer) { - continue; - } - - JCTree.JCExpression defaultValue = generateDefaultValue(varDecl.vartype); - JCTree.JCAssign assignment = maker.Assign( - maker.Ident(varDecl.name), - defaultValue - ); - statements.append(maker.Exec(assignment)); - } - } - - return statements.toList(); - } - - private JCTree.JCExpression generateDefaultValue(JCTree.JCExpression typeExpr) { - // For simplicity: 0 for numeric types, false for boolean, null for everything else - String typeStr = typeExpr.toString(); - if ("int".equals(typeStr) || "byte".equals(typeStr) || "short".equals(typeStr) || "long".equals(typeStr) - || "float".equals(typeStr) || "double".equals(typeStr)) { - return maker.Literal(TypeTag.INT, 0); - } else if ("boolean".equals(typeStr)) { - // javac expects boolean literals as numeric payload (0/1) for TypeTag.BOOLEAN - return maker.Literal(TypeTag.BOOLEAN, 0); - } else if ("char".equals(typeStr)) { - return maker.Literal(TypeTag.CHAR, 0); - } - // Reference type or unknown: null - return maker.Literal(TypeTag.BOT, null); - } @Override public void visitMethodDef(JCTree.JCMethodDecl tree) { @@ -129,11 +60,6 @@ private boolean isConstructor(JCTree.JCMethodDecl tree) { return tree.name == names.init; } - private boolean isClassInitializer(JCTree.JCMethodDecl tree) { - return tree.name == names.clinit; - } - - private List replacementStatements(JCTree.JCMethodDecl tree) { ListBuffer statements = new ListBuffer<>(); diff --git a/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java index 01cea4a..5a7cf58 100644 --- a/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java +++ b/src/test/java/org/ikvm/javarefplugin/JavaRefPluginTest.java @@ -544,9 +544,9 @@ void staticBooleanFieldsCombinationsAreHandled() throws Exception { Class type = result.loadClass("example.StaticBooleanFields"); assertTrue(getStaticBoolean(type, "WITH_INIT_FINAL")); - assertFalse(getStaticBoolean(type, "WITHOUT_INIT_FINAL")); + assertTrue(getStaticBoolean(type, "WITHOUT_INIT_FINAL")); assertTrue(getStaticBoolean(type, "WITH_INIT_MUTABLE")); - assertFalse(getStaticBoolean(type, "WITHOUT_INIT_MUTABLE")); + assertTrue(getStaticBoolean(type, "WITHOUT_INIT_MUTABLE")); } @Test @@ -560,7 +560,7 @@ void staticFinalWithoutInitializerWithMultipleStaticBlocksAssignedOnce() throws "public class MultipleStaticBlocks {", " static final boolean FLAG;", " static {", - " System.out.println(\"first\");", + " FLAG = true;", " }", " static {", " System.out.println(\"second\");", @@ -572,7 +572,7 @@ void staticFinalWithoutInitializerWithMultipleStaticBlocksAssignedOnce() throws CompilationResult result = compile(sources); Class type = result.loadClass("example.MultipleStaticBlocks"); assertNotNull(type); - assertFalse(getStaticBoolean(type, "FLAG")); + assertTrue(getStaticBoolean(type, "FLAG")); } @Test @@ -602,8 +602,32 @@ void stripsClassWithTwoStaticBlocks() throws Exception { CompilationResult result = compile(sources); Class type = result.loadClass("example.TwoStaticBlocks"); assertNotNull(type); - assertEquals(0, getStaticInt(type, "REQUIRED")); - assertEquals(0, getStaticInt(type, "MUTABLE")); + assertEquals(7, getStaticInt(type, "REQUIRED")); + assertEquals(123, getStaticInt(type, "MUTABLE")); + } + + @Test + void preservesStaticInitializerPositionForBlankFinalRead() throws Exception { + Map sources = new LinkedHashMap<>(); + sources.put( + "example/StaticOrder.java", + joinLines( + "package example;", + "", + "public class StaticOrder {", + " static final Object REQUIRED;", + " static {", + " REQUIRED = new Object();", + " }", + " static Object ALIAS = REQUIRED;", + "}" + ) + ); + + CompilationResult result = compile(sources); + Class type = result.loadClass("example.StaticOrder"); + assertNotNull(type); + assertNotNull(getStaticObject(type, "ALIAS")); } private static boolean getStaticBoolean(Class type, String fieldName) throws Exception { @@ -618,4 +642,10 @@ private static int getStaticInt(Class type, String fieldName) throws Exceptio return field.getInt(null); } + private static Object getStaticObject(Class type, String fieldName) throws Exception { + java.lang.reflect.Field field = type.getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(null); + } + } From bb17e15991eca2ef47c53223c04793843b2259c7 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sun, 31 May 2026 15:51:40 -0500 Subject: [PATCH 29/30] Space. --- src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java index 57bcd8d..154b0a8 100644 --- a/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java +++ b/src/main/java/org/ikvm/javarefplugin/MethodBodyStripper.java @@ -37,7 +37,6 @@ public void visitClassDef(JCTree.JCClassDecl tree) { result = tree; } - @Override public void visitMethodDef(JCTree.JCMethodDecl tree) { super.visitMethodDef(tree); From 2e51340326b0afdd7e197b84a0f2fd9b656c023a Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sun, 31 May 2026 15:55:29 -0500 Subject: [PATCH 30/30] README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6e0ef1e..b8bd0d5 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,9 @@ The plugin rewrites method bodies to throw `NullPointerException` on invocation: - **Instance methods**: replaced with `throw null`. - **Constructors**: preserved `this()/super()` chaining, then `throw null`. -- **Static initializers**: replaced with minimal field assignments (sets static fields to type-appropriate defaults: `0`, `false`, or `null`). This drastically reduces bytecode size while ensuring the class loads correctly. +- **Static initializers**: preserved intact. - **Abstract/native methods**: bodies left unchanged (they have no bodies). -- **Synthetic bridge methods**: rewritten like regular methods (bridges still exist as method symbols). +- **Synthetic bridge methods**: not specifically handled; the plugin rewrites the concrete method bodies it sees during its `ENTER`-time traversal. ### Plugin arguments