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
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