Skip to content

Fix class closure false positive on platform modules - #5481

Merged
shai-almog merged 4 commits into
masterfrom
fix-class-closure-false-positive-framework-packages
Jul 28, 2026
Merged

Fix class closure false positive on platform modules#5481
shai-almog merged 4 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 259ms / native 99ms = 2.6x speedup
SIMD float-mul (64K x300) java 147ms / native 96ms = 1.5x 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 77.000 ms
Base64 CN1 decode 91.000 ms
Base64 native encode 366.000 ms
Base64 encode ratio (CN1/native) 0.210x (79.0% faster)
Base64 native decode 304.000 ms
Base64 decode ratio (CN1/native) 0.299x (70.1% 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: 510 seconds

Build and Run Timing

Metric Duration
Simulator Boot 86000 ms
Simulator Boot (Run) 1000 ms
App Install 13000 ms
App Launch 6000 ms
Test Execution 1138000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 63ms / native 3ms = 21.0x speedup
SIMD float-mul (64K x300) java 127ms / native 5ms = 25.4x 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 270.000 ms
Base64 CN1 decode 454.000 ms
Base64 native encode 922.000 ms
Base64 encode ratio (CN1/native) 0.293x (70.7% faster)
Base64 native decode 419.000 ms
Base64 decode ratio (CN1/native) 1.084x (8.4% slower)
Base64 SIMD encode 51.000 ms
Base64 encode ratio (SIMD/CN1) 0.189x (81.1% faster)
Base64 SIMD decode 59.000 ms
Base64 decode ratio (SIMD/CN1) 0.130x (87.0% faster)
Base64 encode ratio (SIMD/native) 0.055x (94.5% faster)
Base64 decode ratio (SIMD/native) 0.141x (85.9% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 11.000 ms
Image createMask (SIMD on) 2.000 ms
Image createMask ratio (SIMD on/off) 0.182x (81.8% faster)
Image applyMask (SIMD off) 170.000 ms
Image applyMask (SIMD on) 183.000 ms
Image applyMask ratio (SIMD on/off) 1.076x (7.6% slower)
Image modifyAlpha (SIMD off) 110.000 ms
Image modifyAlpha (SIMD on) 44.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.400x (60.0% faster)
Image modifyAlpha removeColor (SIMD off) 262.000 ms
Image modifyAlpha removeColor (SIMD on) 303.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 1.156x (15.6% slower)

@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: 225 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 55ms / native 3ms = 18.3x 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 204.000 ms
Base64 CN1 decode 184.000 ms
Base64 native encode 852.000 ms
Base64 encode ratio (CN1/native) 0.239x (76.1% faster)
Base64 native decode 405.000 ms
Base64 decode ratio (CN1/native) 0.454x (54.6% faster)
Base64 SIMD encode 70.000 ms
Base64 encode ratio (SIMD/CN1) 0.343x (65.7% faster)
Base64 SIMD decode 58.000 ms
Base64 decode ratio (SIMD/CN1) 0.315x (68.5% faster)
Base64 encode ratio (SIMD/native) 0.082x (91.8% faster)
Base64 decode ratio (SIMD/native) 0.143x (85.7% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 11.000 ms
Image createMask (SIMD on) 4.000 ms
Image createMask ratio (SIMD on/off) 0.364x (63.6% faster)
Image applyMask (SIMD off) 68.000 ms
Image applyMask (SIMD on) 63.000 ms
Image applyMask ratio (SIMD on/off) 0.926x (7.4% faster)
Image modifyAlpha (SIMD off) 78.000 ms
Image modifyAlpha (SIMD on) 62.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.795x (20.5% faster)
Image modifyAlpha removeColor (SIMD off) 66.000 ms
Image modifyAlpha removeColor (SIMD on) 54.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.818x (18.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: 625 seconds

Build and Run Timing

Metric Duration
Simulator Boot 77000 ms
Simulator Boot (Run) 0 ms
App Install 14000 ms
App Launch 1000 ms
Test Execution 745000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 56ms / native 5ms = 11.2x speedup
SIMD float-mul (64K x300) java 53ms / native 4ms = 13.2x 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 227.000 ms
Base64 CN1 decode 121.000 ms
Base64 native encode 300.000 ms
Base64 encode ratio (CN1/native) 0.757x (24.3% faster)
Base64 native decode 476.000 ms
Base64 decode ratio (CN1/native) 0.254x (74.6% faster)
Base64 SIMD encode 49.000 ms
Base64 encode ratio (SIMD/CN1) 0.216x (78.4% faster)
Base64 SIMD decode 87.000 ms
Base64 decode ratio (SIMD/CN1) 0.719x (28.1% faster)
Base64 encode ratio (SIMD/native) 0.163x (83.7% faster)
Base64 decode ratio (SIMD/native) 0.183x (81.7% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 7.000 ms
Image createMask (SIMD on) 2.000 ms
Image createMask ratio (SIMD on/off) 0.286x (71.4% faster)
Image applyMask (SIMD off) 58.000 ms
Image applyMask (SIMD on) 40.000 ms
Image applyMask ratio (SIMD on/off) 0.690x (31.0% faster)
Image modifyAlpha (SIMD off) 42.000 ms
Image modifyAlpha (SIMD on) 121.000 ms
Image modifyAlpha ratio (SIMD on/off) 2.881x (188.1% slower)
Image modifyAlpha removeColor (SIMD off) 76.000 ms
Image modifyAlpha removeColor (SIMD on) 49.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.645x (35.5% 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.

The mojo's build target parameter is required, so these never see null
from a build, but they are static and package private and one of them
already guarded for it. Make the guard consistent: an absent target is
treated as not local, which keeps the server supplied artifacts out of
the staged jar rather than throwing.

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

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 no new comments.

Comments suppressed due to low confidence (1)

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

  • verifyApplicationClassClosure() now always resolves and adds codenameone-core/java-runtime jars to providedJars (for all non-local-javascript targets). ClassClosureVerifier.findMissingProjectReferences() currently scans every providedJar to build providedClasses even when the staged jar is consistent, so this change can introduce an unconditional full scan of these large framework jars on every build. Consider deferring the providedJars scan in ClassClosureVerifier until after you’ve found at least one candidate missing reference (i.e., referenced + in project package space + not present in staged jar), so the common success path doesn’t pay this cost.
        if (!localJavascriptBuild) {
            // codenameone-core and java-runtime are `provided` scope in the app's common
            // module and `provided` is not transitive, so a platform module (ios, android,
            // ...) that only depends on common does not see them among its own artifacts.
            // They still have to count as provided: without them every reference into a

@shai-almog
shai-almog merged commit 7803bef into master Jul 28, 2026
36 checks passed
@shai-almog
shai-almog deleted the fix-class-closure-false-positive-framework-packages branch July 28, 2026 17:37
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