Add JSpecify annotations to language package nodes - redo (12 more)#4257
Add JSpecify annotations to language package nodes - redo (12 more)#4257
Conversation
Test Results 335 files ±0 335 suites ±0 5m 8s ⏱️ +3s Results for commit 1063251. ± Comparison against base commit 50c22c1. This pull request removes 196 and adds 172 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
|
This is a redo of a previous PR that went off the rails |
| - **Do not remove interfaces** from public classes (e.g., if a class implements `NamedNode`, it must continue to do so). | ||
| - **Be extremely careful when changing methods to return `@Nullable`**. If an interface contract (or widespread ecosystem usage) expects a non-null return value, changing it to `@Nullable` is a breaking change that will cause compilation errors or `NullPointerException`s for callers. For example, if a method returned `null` implicitly but its interface requires non-null, you must honor the non-null contract (e.g., returning an empty string or default value instead of `null`). | ||
| - **Do not change the binary signature** of methods or constructors in a way that breaks backwards compatibility. | ||
|
|
There was a problem hiding this comment.
Previous attempt randomly deleted a constructor parameter which caused that PR to go off the rails. Let's fix that here
| * @return the name of this node, or null if this node is anonymous (e.g. an anonymous operation definition) | ||
| */ | ||
| String getName(); | ||
| @Nullable String getName(); |
There was a problem hiding this comment.
This has to be nullable because an operation definition might not have a name
| this.selectionSet = selectionSet; | ||
| } | ||
|
|
||
| public OperationDefinition(String name, |
There was a problem hiding this comment.
These constructors date back to 2018, no longer needed with the builders we have today.
I am removing these lines because these parameters should be not-nullable.
I would usually first deprecate the methods, then remove, but if I keep them here with a deprecated annotation, the nullability annotations will not be correct.
There was a problem hiding this comment.
This is technically a breaking change
| OperationDefinition.Operation op = operationDefinition.getOperation() != null | ||
| ? operationDefinition.getOperation() | ||
| : OperationDefinition.Operation.QUERY; | ||
| switch (op) { |
There was a problem hiding this comment.
Some follow on changes after adding nullability annotations
| def operationDefinition = OperationDefinition.newOperationDefinition() | ||
| .name("q") | ||
| .selectionSet(SelectionSet.newSelectionSet().selection(new Field("f")).build()) | ||
| .build() |
There was a problem hiding this comment.
Minor test change after selection set not nullable
| @Override | ||
| public ObjectField deepCopy() { | ||
| return new ObjectField(name, deepCopy(this.value), getSourceLocation(), getComments(), getIgnoredChars(), getAdditionalData()); | ||
| return new ObjectField(name, assertNotNull(deepCopy(this.value), "value deepCopy should not return null"), getSourceLocation(), getComments(), getIgnoredChars(), getAdditionalData()); |
There was a problem hiding this comment.
I used to think assertion messages matter but they dont. A stacktrace would lead to this code location - so less code can be better code
There was a problem hiding this comment.
This assert is a special one to satisfy the NullAway check
The problem is that deepCopy is a generic utility and I can't guarantee that always returns not-nullable result
| public OperationDefinition(String name) { | ||
| this(name, null, emptyList(), emptyList(), null, null, emptyList(), IgnoredChars.EMPTY, emptyMap()); | ||
| } | ||
|
|
There was a problem hiding this comment.
Fair enough - cruft must be removed at some point
| } | ||
|
|
||
| public Operation getOperation() { | ||
| public @Nullable Operation getOperation() { |
There was a problem hiding this comment.
Ummmmmm can this be null??? if its not specified surely is defaults to QUERY??
There was a problem hiding this comment.
Yep I agree, should have defaulted to query instead
There was a problem hiding this comment.
It does in the parser as well
Per the GraphQL spec and graphql-js reference implementation, the operation type is always known — shorthand queries are QUERY. Default the Builder to Operation.QUERY and remove null checks in callers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Test ReportTest Results
Code Coverage (Java 25)
Changed Class Coverage (4 classes)
|
Summary
Adds JSpecify nullability annotations (
@NullMarked,@NullUnmarked,@Nullable) to the followinggraphql.languageclasses and interfaces, removing each from theJSpecifyAnnotationsCheckexemption list:NodeTraverserNonNullTypeObjectFieldObjectTypeDefinition/ObjectTypeExtensionDefinitionOperationDefinitionOperationTypeDefinitionPrettyAstPrinterSDLDefinition/SDLExtensionDefinitionSDLNamedDefinition/TypeDefinitionKey decisions
Key interesting point -
NamedNode.getName()is@NullableOperationDefinitioncan be anonymous (e.g.{ user { name } }) — the GraphQL spec makes the operation name optional. Pushing@NullableontoNamedNode.getName()is the truthful representation of the interface contract.TypeDefinition.getName()overrides as non-nullTypeDefinitionextends bothSDLNamedDefinition(non-nullgetName()) andNamedNode(@Nullable getName()). SDL type definitions always have a name, soTypeDefinitionexplicitly overridesgetName()to return non-null. This prevents@Nullablefrom cascading into callers likeTypeDefinitionRegistrythat work with concrete schema types.OperationDefinition.selectionSetis non-nullLegacy convenience constructors that passed
nullforselectionSetwere deleted (they were only used in one test). The field, constructor, andgetSelectionSet()are all non-null, matching the GraphQL spec requirement that every operation has a selection set.OperationDefinition.operationis@NullableThe shorthand query form (e.g.
{ user { name } }) omits thequerykeyword, sogetOperation()can legitimately returnnull.Builder inner classes are
@NullUnmarkedAll builder static inner classes are annotated
@NullUnmarkedrather than annotating each field individually.PrettyAstPrinterprivate helpersnode(@Nullable Class startClass),isEmpty(@Nullable ...),nvl(@Nullable ...),block(... @Nullable String separatorSingleLine, @Nullable String whenEmpty), andspaced(@Nullable String... args)/join(String, @Nullable String...)are nullable because null is actually passed at the call sites or because the method bodies already handle null gracefully.Cascading fixes
OperationValidator:getName()is now@NullableonOperationDefinition, so error message construction uses a pre-checked local variable orObjects.toString(x, "")fallback for the anonymous operation case.TypeDefinitionRegistry: twoAssert.assertNotNull(t.getName(), ...)workarounds are removed now thatTypeDefinition.getName()is non-null.🤖 Generated with Claude Code