Skip to content

Fix class closure false positive on platform modules - #5481

Open
shai-almog wants to merge 3 commits into
masterfrom
fix-class-closure-false-positive-framework-packages
Open

Fix class closure false positive on platform modules#5481
shai-almog wants to merge 3 commits into
masterfrom
fix-class-closure-false-positive-framework-packages

Conversation

@shai-almog

@shai-almog shai-almog commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

mvn clean package -Dcodename1.buildTarget=ios-source (and the cloud iOS/Android targets) fails on a fully clean tree with most of com.codename1.ui listed as missing from the build, since #5390:

[ERROR] The compiled application classes are inconsistent. The following classes are
[ERROR] referenced by your code but missing from the build:
[ERROR]  - com.codename1.ui.Button (referenced from ...)
[ERROR]  - com.codename1.ui.Form (referenced from ...)
...

Root cause

The check marks a referenced class missing when it is absent from the staged jar-with-dependencies and lives in a package the project owns. Two things collide on a platform module:

  1. codenameone-core / java-runtime are left out of the staged jar because the build re-supplies them, so no framework class is in the jar. This is true for local targets such as ios-source too - only a local JavaScript build keeps them, because ParparVM then translates on this machine and resolves everything from the staged classes.
  2. verifyApplicationClassClosure built its "this gets re-supplied" set from project.getArtifacts() of the module being built only. Those artifacts are provided scope in the app's common module and provided is not transitive, so an ios/android module that depends on common alone never sees them. The set was always empty there - the same hole the local JavaScript path already documents a few lines below.

The trigger is any application class in a package the framework also owns: a patched framework class, or a helper deliberately placed in a com.codename1 package to reach package private API. That single class pulls the whole framework package into the project package space, and every framework class the app touches then looks missing.

Reproduced on a stock archetype project: it builds clean, and adding one class under common/src/main/java/com/codename1/ui/ makes the iOS build fail with com.codename1.ui.Button/Command/Component/Container/Dialog/Form/Toolbar reported missing.

Fix

  • Resolve codenameone-core / java-runtime from the plugin's own dependencies when the module's classpath does not carry them.
  • Skip the check when core cannot be resolved at all - without its class list the check cannot tell a stale class from a framework class - and warn that stale classes will go undetected.
  • Split the two decisions that were previously one: isStrippedFromStagedJar governs what the assembly leaves out, and the narrower isSuppliedByBuildServer (codenameone-core, java-runtime, kotlin-stdlib) is what may satisfy a dangling reference. A provided scope third party dependency is left out but nobody puts it back, so references into it stay reportable.
  • An artifact with a null scope is no longer stripped from the jar: a missing scope is not a statement that the artifact sits outside the application.

Verification

End to end against a generated app with the patched plugin:

  • App with a class in com.codename1.ui: BUILD SUCCESS (was the reported failure).
  • Genuine stale output (delete a class's source and its .class, leave the stale dependent class file): still fails, reporting only the deleted class.
  • Unit tests cover the strip matrix and the narrower re-supply set: cloud vs local targets, local-javascript keeping the framework, kotlin-stdlib, null scope, ordinary compile scope dependencies.

Note that the check has been inert on iOS/Android modules since it landed (its provided set was always empty there); this is what makes it work on them.

🤖 Generated with Claude Code

The class closure check treated codenameone-core and java-runtime as
server-provided only when they showed up in the platform module's own
artifacts. They never do: they are `provided` scope in the app's common
module and `provided` is not transitive, so an ios/android module that
depends on common alone does not see them -- the same hole the local
JavaScript path already documents.

The framework is also stripped from the staged jar, so with an empty
provided set every framework class referenced from a package the app
shares with the framework -- a patched framework class, or a helper
placed in a com.codename1 package to reach package private API -- was
reported missing. A stock app plus one class in com/codename1/ui made
`mvn clean package -Dcodename1.buildTarget=ios-source` fail with most of
com.codename1.ui listed as missing from the build.

Resolve codenameone-core / java-runtime from the plugin's own
dependencies when the module's classpath does not carry them, and skip
the check entirely when core cannot be resolved at all: without its
class list the check cannot tell a stale class from a framework class.

The decision about what is stripped from the staged jar now lives in one
shared method used by both the jar assembly and the check, so the two
can no longer disagree about what is actually missing. That also covers
`provided` scope third party dependencies, which the check had not
accounted for either.

Verified against a generated app: the failing project builds, a genuine
stale class (deleted source, surviving dependent class file) is still
reported, and only it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 27, 2026 17:37

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d4b4002013

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Codename One Maven plugin’s staging and class-closure verification logic to avoid false-positive “missing framework classes” errors on platform modules (e.g., ios, android) when framework artifacts are intentionally stripped from the staged jar and re-supplied by the build server.

Changes:

  • Introduces shared strip-decision helpers (isLocalJavascriptBuild, isStrippedFromStagedJar) and uses them in both jar assembly and the class-closure verifier to keep behavior consistent.
  • Enhances verifyApplicationClassClosure() to resolve framework jars from the plugin’s own dependencies when they aren’t visible via the platform module’s project.getArtifacts().
  • Adds unit tests covering stripping behavior across build targets/scopes, including local JavaScript and Kotlin stdlib handling.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CN1BuildMojo.java Centralizes and reuses stripping rules between jar staging and class-closure verification; resolves server-supplied framework jars for platform modules.
maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/CN1BuildMojoTest.java Adds unit tests for the new shared stripping logic across representative build targets/scopes.
Comments suppressed due to low confidence (1)

maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CN1BuildMojo.java:598

  • isLocalBuildTarget() calls buildTarget.startsWith(...) without a null guard. isStrippedFromStagedJar(..., buildTarget) calls into this method and is a static helper that can be invoked with a null buildTarget, which would throw a NullPointerException.
    private static boolean isLocalBuildTarget(String buildTarget) {
        // windows-device (BUILD_TARGET_WINDOWS_NATIVE) is a *cloud* build: it sends
        // a "win32" build to the server (see the windows-device target in
        // buildxml-template.xml), mirroring linux-device. Only the explicit
        // local-windows-device cross-compile and the windows-source project
        // generation are local.
        return (buildTarget.startsWith("local-") || BUILD_TARGET_XCODE_PROJECT.equals(buildTarget)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@shai-almog

shai-almog commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 181 screenshots: 181 matched.
✅ JavaScript-port screenshot tests passed.

@shai-almog

shai-almog commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 151 screenshots: 151 matched.

Native Android coverage

  • 📊 Line coverage: 8.37% (7322/87435 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.26% (38206/462263), branch 3.02% (1310/43428), complexity 3.29% (1571/47783), method 4.94% (1275/25820), class 10.04% (349/3475)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/551 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 8.37% (7322/87435 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.26% (38206/462263), branch 3.02% (1310/43428), complexity 3.29% (1571/47783), method 4.94% (1275/25820), class 10.04% (349/3475)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/551 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend scalar fallback (no native SIMD)
SIMD int-add (64K x300) java 152ms / native 256ms = 0.5x speedup
SIMD float-mul (64K x300) java 237ms / native 167ms = 1.4x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path gated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode 89.000 ms
Base64 CN1 decode 85.000 ms
Base64 native encode 385.000 ms
Base64 encode ratio (CN1/native) 0.231x (76.9% faster)
Base64 native decode 272.000 ms
Base64 decode ratio (CN1/native) 0.313x (68.8% faster)
Image encode benchmark status skipped (SIMD unsupported)

@shai-almog

shai-almog commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 144 screenshots: 144 matched.
✅ Native Apple TV (tvOS, Metal) screenshot tests passed.

@shai-almog

shai-almog commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 143 screenshots: 143 matched.
✅ Native iOS screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 258 seconds

Build and Run Timing

Metric Duration
Simulator Boot 75000 ms
Simulator Boot (Run) 2000 ms
App Install 31000 ms
App Launch 2000 ms
Test Execution 863000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 82ms / native 3ms = 27.3x speedup
SIMD float-mul (64K x300) java 159ms / native 3ms = 53.0x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 178.000 ms
Base64 CN1 decode 150.000 ms
Base64 native encode 445.000 ms
Base64 encode ratio (CN1/native) 0.400x (60.0% faster)
Base64 native decode 225.000 ms
Base64 decode ratio (CN1/native) 0.667x (33.3% faster)
Base64 SIMD encode 49.000 ms
Base64 encode ratio (SIMD/CN1) 0.275x (72.5% faster)
Base64 SIMD decode 45.000 ms
Base64 decode ratio (SIMD/CN1) 0.300x (70.0% faster)
Base64 encode ratio (SIMD/native) 0.110x (89.0% faster)
Base64 decode ratio (SIMD/native) 0.200x (80.0% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 10.000 ms
Image createMask (SIMD on) 4.000 ms
Image createMask ratio (SIMD on/off) 0.400x (60.0% faster)
Image applyMask (SIMD off) 42.000 ms
Image applyMask (SIMD on) 38.000 ms
Image applyMask ratio (SIMD on/off) 0.905x (9.5% faster)
Image modifyAlpha (SIMD off) 40.000 ms
Image modifyAlpha (SIMD on) 35.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.875x (12.5% faster)
Image modifyAlpha removeColor (SIMD off) 56.000 ms
Image modifyAlpha removeColor (SIMD on) 45.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.804x (19.6% faster)

@shai-almog

shai-almog commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 217 screenshots: 217 matched.
✅ Native Apple Watch (watchOS, Core Graphics) screenshot tests passed.

@shai-almog

shai-almog commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 148 screenshots: 148 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 206 seconds

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 53ms / native 3ms = 17.6x speedup
SIMD float-mul (64K x300) java 56ms / native 3ms = 18.6x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 164.000 ms
Base64 CN1 decode 124.000 ms
Base64 native encode 674.000 ms
Base64 encode ratio (CN1/native) 0.243x (75.7% faster)
Base64 native decode 223.000 ms
Base64 decode ratio (CN1/native) 0.556x (44.4% faster)
Base64 SIMD encode 51.000 ms
Base64 encode ratio (SIMD/CN1) 0.311x (68.9% faster)
Base64 SIMD decode 46.000 ms
Base64 decode ratio (SIMD/CN1) 0.371x (62.9% faster)
Base64 encode ratio (SIMD/native) 0.076x (92.4% faster)
Base64 decode ratio (SIMD/native) 0.206x (79.4% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 7.000 ms
Image createMask (SIMD on) 1.000 ms
Image createMask ratio (SIMD on/off) 0.143x (85.7% faster)
Image applyMask (SIMD off) 49.000 ms
Image applyMask (SIMD on) 32.000 ms
Image applyMask ratio (SIMD on/off) 0.653x (34.7% faster)
Image modifyAlpha (SIMD off) 36.000 ms
Image modifyAlpha (SIMD on) 38.000 ms
Image modifyAlpha ratio (SIMD on/off) 1.056x (5.6% slower)
Image modifyAlpha removeColor (SIMD off) 61.000 ms
Image modifyAlpha removeColor (SIMD on) 56.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.918x (8.2% faster)

@shai-almog

shai-almog commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 149 screenshots: 149 matched.
✅ Native iOS Metal screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 528 seconds

Build and Run Timing

Metric Duration
Simulator Boot 80000 ms
Simulator Boot (Run) 1000 ms
App Install 23000 ms
App Launch 1000 ms
Test Execution 1092000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 123ms / native 4ms = 30.7x speedup
SIMD float-mul (64K x300) java 102ms / native 3ms = 34.0x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 534.000 ms
Base64 CN1 decode 285.000 ms
Base64 native encode 789.000 ms
Base64 encode ratio (CN1/native) 0.677x (32.3% faster)
Base64 native decode 861.000 ms
Base64 decode ratio (CN1/native) 0.331x (66.9% faster)
Base64 SIMD encode 63.000 ms
Base64 encode ratio (SIMD/CN1) 0.118x (88.2% faster)
Base64 SIMD decode 64.000 ms
Base64 decode ratio (SIMD/CN1) 0.225x (77.5% faster)
Base64 encode ratio (SIMD/native) 0.080x (92.0% faster)
Base64 decode ratio (SIMD/native) 0.074x (92.6% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 38.000 ms
Image createMask (SIMD on) 4.000 ms
Image createMask ratio (SIMD on/off) 0.105x (89.5% faster)
Image applyMask (SIMD off) 197.000 ms
Image applyMask (SIMD on) 110.000 ms
Image applyMask ratio (SIMD on/off) 0.558x (44.2% faster)
Image modifyAlpha (SIMD off) 183.000 ms
Image modifyAlpha (SIMD on) 193.000 ms
Image modifyAlpha ratio (SIMD on/off) 1.055x (5.5% slower)
Image modifyAlpha removeColor (SIMD off) 301.000 ms
Image modifyAlpha removeColor (SIMD on) 192.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.638x (36.2% faster)

Review feedback on the closure check:

Only codenameone-core, java-runtime and kotlin-stdlib are put back after
being left out of the staged jar, so only those may satisfy a dangling
reference. Reusing the assembly exclusion decision for that also excused
references into an arbitrary `provided` or `runtime` scope dependency,
which nobody supplies before the translators run -- an incomplete jar
would have been uploaded instead of reported. The two decisions are now
separate methods, with the narrow one feeding the check.

An artifact with a null scope is no longer stripped from the staged jar:
a missing scope is not a statement that the artifact sits outside the
application, and dropping its classes can only break the build.

Also add the Codename One header the copyright gate requires on the
touched test file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 28, 2026 03:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

More review feedback:

Skipping the check silently at debug level hid the fact that stale
classes would go unreported, so say it at warn level with the build
target and the coordinates that could not be resolved.

Hold the provided jars in a LinkedHashSet rather than de-duplicating a
list by scanning it, and take a Collection in the verifier so the set
can be passed straight through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 28, 2026 04:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment on lines +226 to +233
static boolean isSuppliedByBuildServer(String groupId, String artifactId, String buildTarget) {
if (GROUP_ID.equals(groupId) && contains(artifactId, BUNDLE_ARTIFACT_ID_BLACKLIST)) {
return !isLocalJavascriptBuild(buildTarget);
}
return !isLocalBuildTarget(buildTarget)
&& "org.jetbrains.kotlin".equals(groupId)
&& "kotlin-stdlib".equals(artifactId);
}
Comment on lines +242 to +258
static boolean isStrippedFromStagedJar(String groupId, String artifactId, String scope, String buildTarget) {
if (GROUP_ID.equals(groupId) && contains(artifactId, BUNDLE_ARTIFACT_ID_BLACKLIST)) {
// For local JavaScript builds we need codenameone-core and java-runtime classes
// in the staged jar - the build server normally re-supplies those, but ParparVM's
// ByteCodeTranslator runs locally here and resolves everything from the staged class
// directory.
return !isLocalJavascriptBuild(buildTarget);
}
if (isSuppliedByBuildServer(groupId, artifactId, buildTarget)) {
// When sending to the build server, we'll strip the kotlin-stdlib and the server will
// provide it. For local builds, it's easier to just include it.
return true;
}
// An artifact with no scope was never scoped out of the application, so
// keep its classes rather than dropping them from the jar.
return scope != null && !"compile".equals(scope);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants