refactor: declare org.testng.xml null-marked - #3384
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (10)
🚧 Files skipped from review as they are similar to previous changes (8)
Included review availability: Your plan includes up to 8 reviews per rolling hour; 6 remain after this review. 📝 WalkthroughWalkthroughThe pull request applies JSpecify nullability annotations across XML APIs, adds ChangesXML model contracts
Parser and consumer boundaries
Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: ⚪ Minimal · up to The PR adds nullness contracts and localized supporting refactors across the XML suite model, parser, and weaver, with the full build passing and no reported behavior change; no actionable merge-blocking risk remains after normal checks and review. Sequence Diagram(s)sequenceDiagram
participant SuiteXmlParser
participant TestNGContentHandler
participant XmlSuite
SuiteXmlParser->>TestNGContentHandler: parse XML input
TestNGContentHandler->>TestNGContentHandler: validate parser state and attributes
TestNGContentHandler->>XmlSuite: construct and populate suite
TestNGContentHandler-->>SuiteXmlParser: return nullable suite
SuiteXmlParser->>SuiteXmlParser: require a suite result
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
d815bd2 to
e11f78e
Compare
org.testng.xml spans testng-core-api, testng-core and testng-test-kit. The package-info.java goes in testng-core-api and the mark reaches the other two, including testng-test-kit through its compileOnly edge on testng-core. Prefer restructuring to annotating: the lazy <groups>/<run> initialisation in XmlSuite and XmlTest becomes a helper that returns what it guarantees, and the parser's transient element state is read through accessors that assert it. Seven setters take @nullable to match their already-@nullable getter. Kotlin only synthesises a mutable property when both halves agree, so annotating the getter alone would silently turn a `var` into a `val` for Kotlin consumers of this published API. No behaviour change. Where a dereference could not be proved, Objects.requireNonNull keeps the NullPointerException the call site already threw.
e11f78e to
52b9022
Compare
Continues the JSpecify/NullAway stack. Base is
master— #3382 merged while this was beingprepared, so this one stands alone. Rebasing dropped its commit as a duplicate rather than
reapplying it, and the merged version is byte-identical to the commit this was written on.
One package:
org.testng.xml— the suite model, its parser and its weaver. 27 main files, 5112lines, and the first batch where three things are true at once:
testng-core-api, 9 intestng-core, 1 (SuiteDigest)in
testng-test-kit, the last reached only through acompileOnlyedge;XmlSuite,XmlTestandXmlClassare what users buildsuites with programmatically, so a
@Nullablehere is a published contract;shipped code —
org.testng.internal.invokers,xml.internal,reporters.jq,cli,cli.jcommanderand five others.org.testng.xml.internalwas marked in #3374.@NullMarkedneither descends into sub-packages norclimbs out of them, so that green said nothing about the parent.
The coverage was proved in all three modules
testng-coredeclaresapi(projects.testngCoreApi)andtestng-test-kitdeclarescompileOnly(projects.testngCore), so thepackage-info.javagoes intestng-core-api. ThecompileOnlyedge is the new variable: it putstestng-coreand itsapidependencies on thecompile classpath, but no
package-info.classhad ever been asked to travel it.testng-test-kitis also easy to write off as out of scope — it is named "test-kit" and it is neverpublished — but the convention only disables NullAway for tasks whose name contains
Test, andits task is
compileJava. It is under the check.Per the procedure in
AGENTS.md, the negative control runs once per module traversed. A throwawaywent into one file of each. Before the
package-info.javaall three compiled clean, zero NullAway.After it all three failed with
[NullAway] returning @Nullable expression from method with @NonNull return type:testng-core-apipackage-info.javaXmlUtils.java:9testng-coreapi(projects.testngCoreApi)TestNGURLs.java:10testng-test-kitcompileOnly(projects.testngCore)SuiteDigest.java:23All three reverted. The third row is the one that had to be measured rather than assumed, and it is
red — so a second
package-info.javaintestng-test-kitis not needed, and the counts below coverthe whole package.
SuiteDigestthen produced 16 real errors of its own, which is the same factarriving a second way.
What the check reported
testng-core-api, 51testng-core, 16testng-test-kit, 1testng-jcommander)@NullableObjects.requireNonNullEvery annotation is classified by deletion and recompilation — each one stripped on its own and
the five modules force-recompiled, 111 times, then 79 of them a second time after the cleanup pass
below. 110 bring back a named NullAway error; the 7 Kotlin ones bring back a Kotlin error (see the
next section); 1 brings back nothing and is argued below. The files were copied to a separate
directory first, restored from that copy, and checked byte-identical per file afterwards.
Every number above comes from a forced recompile. The fast form —
:testng-core-api:compileJava --rerunand the four other modules by name — was validated once bydeleting an annotation known to be demanded and confirming the error came back.
Restructured rather than annotated
The lazy
<groups>/<run>initialisation was written four times inXmlTestand three times inXmlSuite, each copy followed bym_xmlGroups.getRun().getX(). A privategroupsRun()that returnsthe
XmlRunit just guaranteed replaces all of them and removes seven dereferences with noannotation and no assertion.
The parser is the other half.
TestNGContentHandlerkeeps eighteenm_currentXxxfields set at astart tag and cleared at the matching end tag; seven accessors now read them, so the invariant is
stated once instead of at twenty-five call sites.
The rest:
XmlClassdrops a redundant= nullonm_name(every constructor callsinit, whichassigns it) and its three-argument
initoverload, which had one caller and hid the assignment fromNullAway's initializer analysis;
loadClass()returns the class it resolved sogetSupportClass()needs no assertion;
XmlPackage.getXmlClassesandXmlTest.getInvocationNumberscache through alocal; and
XmlTest.equalsloses a two-armed condition whose second arm re-tested a value the lineabove had already established, so only the first arm could ever fire.
A
@Nullablegetter without its setter is a Kotlin source breakSeven setters here are annotated for a reason that has nothing to do with NullAway, and it is worth
recording because it will recur in every later batch that touches a bean.
Kotlin synthesises a mutable property from a Java getter/setter pair only when the two agree on
nullability. Annotate
getName()@Nullableand leavesetName(String)alone, andXmlPackage.namesilently stops being avarand becomes aval— soXmlPackage().apply { name = p }no longer compiles. That is a source-incompatible change to published API, and this repository has a
Kotlin consumer of exactly that shape:
testng-test-kit/src/main/kotlin/test/SimpleBaseTest.kt.The pairs, all seven of them proved by a throwaway Kotlin file that assigns each property — with the
annotations it compiles, without them it reports 7 errors, one per line:
XmlDefinenameXmlPackagenameXmlGroupsrunXmlMethodSelectorclassName,scriptXmlScriptexpressionXmlTestscriptEach is truthful independently of Kotlin — every backing field is
@Nullable, andXmlTest.setScriptalready had an explicit
else if (script != null)branch, so null was always its contract.The trap worth passing on:
./gradlew builddoes not catch this. Kotlin's incremental compilationtreats a type-use annotation change on the Java classpath as non-ABI, so
:testng-test-kit:compileKotlinruns, reports nothing, and stays green on code that cannot compile from clean. It took
--rerun-tasksto see it. The guard set for a batch that annotates a bean must include:testng-test-kit:compileKotlin --rerun-tasks, not just the Java compiles.The one annotation without a compiler demand
XmlSuite.FailurePolicy.getValidPolicy(@Nullable String policy)— the parameter. Its return isdemanded twice over; the parameter is not, and it would be easy to read that as decoration.
The null is real and routine:
TestNGContentHandler:378calls it withattributes.getValue("configfailurepolicy"), which is null for every suite file that omits theattribute — the normal case. NullAway cannot see it because
org.xml.sax.Attributesis unannotatedand read optimistically. The body has tested
policy == nullon its first line since it was written.Worth recording:
CliConfigurer:170looked like the demanding caller and is not. It guardscli.configFailurePolicy != nullfirst, so NullAway sees a non-null value there.The ripple
Ten merged packages read this one. Four needed something:
org.testng.xml.internal—Parser:154passes no stream when the suite is not afile:URL, soIFileParser.parse's stream parameter is@Nullable.SuiteXmlParserthen had to widen to match:NullAway does check that an implementation does not narrow a
@Nullableinterface parameter. It isreached either through
accept(), which requires afile:scheme and so a stream, or asParser.getParser's fallback for a scheme nothing claims — which has never been readable here, sothe
requireNonNullis the throw that was already happening rather than a new precondition.org.testng.reporters.jq—TestNgXmlPanel.getHeaderreturnsXmlSuite.getFileName(), absent fora suite built in code.
XMLStringBuffer.addOptionalalready declares its value@Nullableandskips null, so the abstract
getHeaderinBaseMultiSuitePanelfollows; the other six overridesstay non-null.
org.testng.cli.jcommander—Converter:70derives an output name fromgetFileName(). Everysuite it sees was read from a file and both readers set the name (
TestNGContentHandlerfor XML,Yamlfor YAML), which therequireNonNullrecords.org.testng.internal.invokersand the six others needed nothing.One item for whoever takes the next batch:
YamlParser(testng-yaml) and the testFakeHttpXmlParserimplementIFileParserwith a non-@Nullablestream parameter. Both are inunmarked packages so nothing checks them today, and both will report the narrowing the moment their
package is marked.
Two latent NullPointerExceptions, recorded and not fixed
An
XmlGroupson anXmlTestcan have noXmlRun—setGroups(new XmlGroups())andaddMetaGroupboth leave one that way — and two methods dereference
getRun()without testing it:XmlTest.addIncludedGroupt.addMetaGroup("m", List.of("a")); t.addIncludedGroup("g")XmlTest.equalst1.addExcludedGroup("x")againstt2.setGroups(new XmlGroups())Both were run against these sources and against the pre-change sources: same
NullPointerExceptioneither way, only the message differs.addExcludedGroup,setIncludedGroupsandsetExcludedGroupsall repair a missing<run>, and everyXmlSuiteequivalent does too, so the asymmetry is
XmlTest-only. Repairing it would turn a long-standingthrow into a silent success, which is a behaviour change and does not belong in this batch —
requireNonNullkeeps the throw and makes the asymmetry compiler-visible. Filed as #3385.Also noted, not reproducible from the suite:
XmlWeaver.getInstance()returned null in test modewhen
-Dtestng.xml.weavernames a third-party class, and the callers dereferenced it. TestNG's owntests only ever select the two bundled weavers. And
XmlSuite.m_testhas no writer anywhere in therepository, so
getTest()has always returned null — annotated truthfully rather than removed, sinceit is published API.
Residue left alone, as agreed for this stack:
== nullguards on values NullAway now provesnon-null, in
XmlClass.equals/hashCodeandXmlInclude.equals/hashCode.Verification
./gradlew build --rerun-tasks: BUILD SUCCESSFUL,16854 completed, 0 failed, 12 skipped— thetwelve are the pre-existing environment-dependent skips, unchanged from #3382.
--rerun-tasksratherthan a plain
buildfor the reason given in the Kotlin section: the plain form went green on a treethat did not compile from clean.
autostyleApplyrun as its own invocation.No behaviour change.
Summary by CodeRabbit
Bug Fixes
Documentation