Skip to content

build: enable NullAway, scoped to packages that opt in with @NullMarked - #3367

Merged
krmahadevan merged 2 commits into
masterfrom
juherr/enable-nullaway
Aug 16, 2026
Merged

build: enable NullAway, scoped to packages that opt in with @NullMarked#3367
krmahadevan merged 2 commits into
masterfrom
juherr/enable-nullaway

Conversation

@juherr

@juherr juherr commented Aug 15, 2026

Copy link
Copy Markdown
Member

TestNG has no nullness checking, so whether a method may return null is only discoverable by reading it. This wires up NullAway and takes the first package through it.

Supersedes #2941, which used the Checker Framework. That branch is now three years and 336 commits behind, 35 of its 59 files conflict, and testng-asserts has since left the repository. More importantly the Checker Framework analyses every file of the compilation unit regardless of what a package opted into, so the change could not be split — the branch stalled on the size of the diff.

Commit 1 — the wiring

NullAway plugs into the Error Prone pass that already runs on every module, so there is no new Gradle plugin. OnlyNullMarked confines it to code declaring JSpecify @NullMarked, which makes this commit report nothing and touch no Java source.

That scoping is the point: nullness can be adopted one package at a time, each step green on its own, instead of as one sweep over the whole API.

JSpecify ships as a regular api dependency rather than compileOnly. Its annotations are runtime-retained, so hiding them would strip nullness information from the published artifact instead of passing it to consumers. verifyPublishedPomDependencies is updated accordingly.

Commit 2 — the first package

org.testng.log4testng, picked for being small and confined to one module. That last part matters: @NullMarked applies to a package, and 10 of TestNG's 25 main-source packages appear in more than one module, so marking one half marks the other through the compile classpath. org.testng.internal alone spans four modules.

Every Logger method hands its message straight to SLF4J as a {} argument, which renders null as the string "null" and never dereferences it — verified against slf4j-simple 2.0.18. So the parameters are @Nullable. The Throwable overloads keep a non-null throwable.

Verification

Full build green on both commits: 15726 tests in testng-core, 0 failures, including testng-test-osgi, which exercises the bundle with the new dependency.

The wiring was checked to be a real no-op rather than an inert one: a throwaway @NullMarked package with a return null made NullAway fail the compile as expected, and was removed.

Follow-up

Enabling the check on org.testng.internal.reflect surfaced a live NullPointerException, reported as #3361 and fixed separately in #3362.

The follow-on packages are stacked on this branch rather than folded in, so each stays green on its own: #3365 takes org.testng.collections, org.testng.cli and org.testng.cli.jcommander.

Replaces #3363, which was opened from a fork branch. GitHub requires a pull request's base to be a branch of the target repository, so the stack above it could not point at it. Identical commits, same SHAs.

Summary by CodeRabbit

  • Improvements
    • Added null-safety annotations across logging APIs to clarify when messages may be absent.
    • Published nullness annotations for improved compatibility with tools and IDEs.
    • Enabled additional checks to detect potential null-related issues during development.
    • Marked the logging package as non-null by default while preserving nullable logging message parameters.

@juherr
juherr requested a review from krmahadevan as a code owner August 15, 2026 20:15
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d727e130-3672-4953-aeef-a1e2cb0fcaeb

📥 Commits

Reviewing files that changed from the base of the PR and between ffd920b and c7d7263.

📒 Files selected for processing (5)
  • build-logic/code-quality/src/main/kotlin/testng.errorprone.gradle.kts
  • build-logic/jvm/src/main/kotlin/testng.java-library.gradle.kts
  • testng-core-api/src/main/java/org/testng/log4testng/Logger.java
  • testng-core-api/src/main/java/org/testng/log4testng/package-info.java
  • testng/testng-build.gradle.kts

📝 Walkthrough

Walkthrough

The build now publishes JSpecify, enables NullAway for @NullMarked code, and marks the org.testng.log4testng package as null-marked. Logger message parameters are explicitly nullable.

Changes

Nullness tooling and logging contract

Layer / File(s) Summary
Publish JSpecify metadata
build-logic/jvm/src/main/kotlin/testng.java-library.gradle.kts, testng/testng-build.gradle.kts
Adds JSpecify as an API dependency and includes it in the expected published POM.
Enable NullAway checks
build-logic/code-quality/src/main/kotlin/testng.errorprone.gradle.kts
Enables NullAway with OnlyNullMarked for production compilation. Test compilation disables NullAway and SelfAssertion.
Annotate logging API
testng-core-api/src/main/java/org/testng/log4testng/package-info.java, testng-core-api/src/main/java/org/testng/log4testng/Logger.java
Marks the package as null-marked and annotates all logger message parameters as nullable.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to c7d72

The PR introduces package-scoped nullness checking and updates Logger annotations and published dependency metadata; the supplied verification reports successful full builds and tests, so no actionable merge-blocking risk remains beyond normal checks.

Possibly related PRs

Suggested reviewers: krmahadevan

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: enabling NullAway for packages that opt in with @NullMarked.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch juherr/enable-nullaway

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

juherr added 2 commits August 16, 2026 08:56
TestNG has no nullness checking, so whether a method may return null is
only discoverable by reading it. NullAway plugs into the Error Prone pass
that already runs on every module, and OnlyNullMarked confines it to code
declaring JSpecify's @NullMarked. No package declares it yet, so this
commit touches no Java source and reports nothing.

That scoping is the point. Nullness can now be adopted package by package,
each step green on its own, instead of as a single sweep over the whole
API. The earlier attempt in #2941 used the Checker Framework, which
analyses every file of the compilation unit regardless of what the package
opted into, and stalled on the size of the resulting diff.

@NullMarked applies to a package rather than to a source set, and nine test
packages share a name with a main one, so NullAway stays off for test
compilation: marking org.testng or org.testng.internal would otherwise
sweep in their test halves.

JSpecify ships as a regular api dependency rather than compileOnly: its
annotations are runtime-retained, so hiding them would strip nullness
information from the published artifact instead of passing it to consumers.
First package to opt into the nullness checking wired up in the previous
commit. It was picked for being small and confined to one module: most of
TestNG's packages span several, and @NullMarked reaches every module where
the package appears, through package-info.class on the compile classpath.

Every Logger method hands its message straight to SLF4J as a "{}" argument,
which renders null as the string "null" and never dereferences it. So null
is accepted, and the parameters say so. The Throwable overloads keep a
non-null throwable: nothing calls them otherwise.

No behaviour changes -- annotations only.
@krmahadevan
krmahadevan force-pushed the juherr/enable-nullaway branch from c7d7263 to 481f404 Compare August 16, 2026 03:26
@krmahadevan
krmahadevan merged commit 9730f2d into master Aug 16, 2026
9 checks passed
@krmahadevan
krmahadevan deleted the juherr/enable-nullaway branch August 16, 2026 03:27
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