Skip to content

fix(build): the build follows --project-root-path, and a missing Gradle is a message, not an NPE - #268

Open
rahlk wants to merge 1 commit into
mainfrom
fix/build-root-and-gradle-guard
Open

rahlk wants to merge 1 commit into
mainfrom
fix/build-root-and-gradle-guard

Conversation

@rahlk

@rahlk rahlk commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Two defects in BuildProject, both of which reached the user the same way: as a degraded overlay on a run that still exited 0.

1. gradleBuild dereferenced a null command

mavenBuild guards on isMavenInstalled() before it touches MAVEN_CMD. gradleBuild had no counterpart, so on a machine with neither Gradle on PATH nor a wrapper the command resolves to null and the compile raised:

[WARN] RTA call graph unavailable (NullPointerException: Cannot invoke "String.equals(Object)"
       because "com.ibm.cldk.utils.BuildProject.GRADLE_CMD" is null); emitting declared edges only

The caller catches that and degrades, so the user is told the RTA call graph is unavailable because a private field they have never heard of was null — and the run exits 0. Now:

[ERROR] Gradle is not installed and no Gradle wrapper was found; cannot build <path>

A package-private gradleBuild(String, String) overload takes the command explicitly so the missing-Gradle path is reachable from a test.

2. --project-root-path redirected dependencies but not the build

-f reached downloadLibraryDependencies and stopped there. For an input at the repository root over a module carrying the only pom.xml, dependency resolution therefore succeeded while the compile ran against a directory with no build file at all, fell through to Gradle, and emitted declared edges only.

-i app         -a 2  →  RTA class hierarchy: 25802 total classes, 1 application classes
-i . -f app    -a 2  →  [WARN] RTA call graph unavailable (...); emitting declared edges only

resolveBuildRoot tries the input first:

-i carries a build file? build root change
yes -i unchanged — every existing invocation
no, but -f does -f fixed
neither -i unchanged — previous last resort

Deliberately conservative: only the currently broken case moves. The custom-build-command branch is guarded against a null MAVEN_CMD for the same reason as (1) — String.replace would throw rather than report the missing tool.

Tests

BuildProjectTest was an empty stub; it now carries six cases, written failing first:

  • gradleBuildReportsAMissingGradleInsteadOfThrowing
  • buildRootPrefersTheInputWhenItCarriesItsOwnBuildFile
  • buildRootFallsBackToProjectRootPomWhenTheInputHasNoBuildFile
  • buildRootFallsBackToAGradleProjectRootPom
  • buildRootKeepsTheInputWhenNeitherCarriesABuildFile
  • buildRootToleratesAnAbsentProjectRootPom

Verification

Gate Result
./gradlew test 610 tests, 1 failed, 4 skipped — the failure is the pre-existing CodeAnalyzerIntegrationTest Testcontainers/Docker initializationError, confirmed by re-running on a pristine tree
Schema conformance -a 1/2/3/4 on mvnw-working-test all validate against analysis.v2.schema.json, exit 0, zero warnings
Monotonicity json(-a 1) ⊆ (-a 2) ⊆ (-a 3) ⊆ (-a 4), no violations
Determinism two -a 4 --eager runs byte-identical
Cross-projection --emit neo4j vs JSON at full depth + --external-calls: no JSON id absent from the cypher; the 13 cypher-only ids are ordinal body vertices plus the app root

After the fix, the previously broken invocation builds and projects cleanly:

-i . -f app --app-name sample -a 2
  →  RTA class hierarchy: 25802 total classes, 1 application classes   (no warning)
  →  can://sample/java/app/src/main/java/com/foo/Bar.java

Follow-ons

  • python-sdk pins codeanalyzer-java==3.3.3 exactly (pyproject.toml:46); the pin needs a bump once this releases, or no SDK user receives the fix.
  • -f is undocumented on both docs fronts and in the README beyond the usage synopsis, and it now has a second effect.

Not addressed

gradleCmd.equals("gradlew") is dead — getGradleCommand() returns an absolute wrapper path, never the bare name, so that branch cannot fire. Preserved as-is rather than widened into this fix.

…le is a message, not an NPE

Two defects in BuildProject, both of which reached the user as a degraded
overlay on a run that still exited 0.

gradleBuild dereferenced GRADLE_CMD with no null guard, while mavenBuild
guards on isMavenInstalled(). On a machine with neither Gradle on PATH nor a
wrapper the command resolves to null, so the compile raised a
NullPointerException, the caller caught it and degraded, and the user was
told the RTA call graph was unavailable because a private field they have
never heard of was null. A package-private overload takes the command
explicitly so the missing-Gradle path is reachable from a test.

--project-root-path redirected downloadLibraryDependencies but not the
build. An input at the repository root over a module carrying the only
pom.xml therefore resolved dependencies correctly and then compiled against
a directory with no build file, fell through to Gradle, and emitted
declared edges only. resolveBuildRoot tries the input first, so a module
carrying its own build file builds exactly where it did before and no
existing invocation changes behaviour; the root pom is consulted only when
the input has nothing to build, and when neither has one the input is
returned unchanged.

The custom-build-command branch is guarded against a null MAVEN_CMD for the
same reason: String.replace would throw rather than report the missing tool.
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.

1 participant