Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
1b01d2b
Annotate GraphQLAppliedDirective
dondonz Aug 21, 2025
8684b48
Add GraphQLSchema annotation
dondonz Aug 21, 2025
0cdc8de
Remove exemption
dondonz Aug 21, 2025
30d0abe
Annotate GraphQLType and GraphQLEnumType
dondonz Aug 21, 2025
6ee01b8
Add Nullmarked to GraphQLEnumType
dondonz Aug 21, 2025
a45c613
Annotate GraphQLCodeRegistry
dondonz Aug 21, 2025
70336d5
Annotate GraphQLList
dondonz Aug 21, 2025
29f8363
Annotate PropertyDataFetcher
dondonz Aug 21, 2025
083b2ab
Annotate GraphQLUnionType
dondonz Aug 21, 2025
b4761ef
Tidy up
dondonz Aug 21, 2025
9a78fb3
Annotate GraphQLScalarType
dondonz Aug 21, 2025
7bbfd9c
Annotate GraphQLNamedInputType
dondonz Aug 21, 2025
7427f64
Add ErrorType
dondonz Aug 23, 2025
120095c
Annotate Directives
dondonz Aug 23, 2025
012f49c
Annotate ErrorClassification
dondonz Aug 23, 2025
afdd408
Annotate ExceptionWhileDataFetching
dondonz Aug 23, 2025
803c924
Annotate ExecutionResult
dondonz Aug 27, 2025
a322ed0
Annotate GraphQLContext
dondonz Aug 27, 2025
41320d4
Annotate AssertException and GraphQLError
dondonz Aug 28, 2025
9765c21
Annotate GraphQLErrorBuilder
dondonz Aug 28, 2025
0ee713c
Annotate GraphQLErrorException
dondonz Aug 28, 2025
e54fca2
Annotate ParseAndValidate
dondonz Aug 28, 2025
603c00d
Annotate ParseAndValidateResult
dondonz Aug 28, 2025
9fcaea1
Annotate Scalars
dondonz Aug 28, 2025
88b02f3
Annotate SerializationError
dondonz Aug 28, 2025
e65217c
Update GraphQLError with a nullable location list
dondonz Aug 28, 2025
19000a5
Annotate TypeMismatchError
dondonz Aug 28, 2025
313bf4f
Annotate UnresolvedTypeError
dondonz Aug 28, 2025
0846c55
Remove agent as no longer exists
dondonz Aug 30, 2025
d2d87cd
Add JSpecify prompt
dondonz Aug 30, 2025
f4b05bb
Fix NullAway issues with assertions
dondonz Aug 30, 2025
ab1ce8d
Remove null checks
dondonz Aug 30, 2025
241b7a0
Annotate DefaultConnection
dondonz Aug 30, 2025
7ffd1ba
Annotate Relay Connection
dondonz Aug 30, 2025
003958f
Update URL
dondonz Aug 30, 2025
ba3de3e
Annotate DefaultConnectionCursor
dondonz Aug 30, 2025
8de5a75
Annotate ConnectionCursor
dondonz Aug 30, 2025
0bee1f8
Annotate DefaultEdge
dondonz Aug 30, 2025
a6e7382
Annotate DefaultPageInfo
dondonz Aug 30, 2025
e508453
Annotate Edge
dondonz Aug 30, 2025
ee76574
Annotate PageInfo
dondonz Aug 30, 2025
b7a7be6
Annotate SimpleListConnection
dondonz Aug 30, 2025
81beb0d
Annotate Relay
dondonz Aug 30, 2025
9c5e4c8
Annotate ValidationErrorClassification and ValidationErrorType
dondonz Aug 30, 2025
6ec7d82
Annotate ValidationError
dondonz Aug 30, 2025
98ff640
Annotate NamedNode
dondonz Aug 30, 2025
3f58265
Annotate Argument
dondonz Aug 30, 2025
eb98025
Annotate GraphQLTypeUtil
dondonz Aug 30, 2025
97b407d
Fix typo
dondonz Aug 30, 2025
44253f7
Update prompt
dondonz Aug 30, 2025
e8b1670
Annotate Breadcrumb
dondonz Aug 30, 2025
f1b5529
Annotate FieldComplexityCalculator
dondonz Aug 30, 2025
230b757
Annotate FieldComplexityEnvironment
dondonz Aug 30, 2025
ff81ff6
Annotate TypeResolutionEnvironment
dondonz Aug 30, 2025
ed2961f
Annotate MaxQueryComplexityInstrumentation
dondonz Aug 30, 2025
cf724bf
Annotate MaxQueryDepthInstrumentation
dondonz Aug 30, 2025
8a47304
Make ValidationError description nullable as message is nullable
dondonz Aug 31, 2025
de6ad66
Add Nullmarked
dondonz Aug 31, 2025
fd3b4af
Revert to nullable description and type for ValidationError
dondonz Aug 31, 2025
71ef680
Update test for Argument, and make nonnullable field clearer
dondonz Aug 31, 2025
23afaea
Annotate Anonymizer
dondonz Sep 1, 2025
bd2bd8d
Merge branch 'master' into infer-nullity-3
dondonz Dec 4, 2025
09a5236
Use string literal optimisation for assert
dondonz Dec 4, 2025
d1e2e54
Tidy up
dondonz Dec 4, 2025
95e7dc9
Make path length not nullable
dondonz Dec 4, 2025
f8920d8
Use string literal for assert messages
dondonz Dec 4, 2025
c78124c
Change GraphQL error message to be non-nullable
dondonz Dec 7, 2025
5b7104e
Improve prompt
dondonz Dec 7, 2025
de00e30
Add clarification on generics type arguments
dondonz Jan 23, 2026
e27bf54
Merge branch 'master' into infer-nullity-3
dondonz Jan 23, 2026
d5de341
Make TypeResolutionEnvironment value nullable
dondonz Jan 23, 2026
0496a3d
Annotate TypeResolutionParameters
dondonz Jan 23, 2026
d2983da
Add Nullmarked to TypeResolutionParameters
dondonz Jan 23, 2026
92b3ac1
Add prompt to reduce whitespace diff
dondonz Jan 23, 2026
0bf48d3
Prompt updates
dondonz Jan 26, 2026
2931540
Adjulst description of validation error to be nonnull
dondonz Jan 26, 2026
66f3fa0
Adjust tests to be compliant with non-nullable description
dondonz Jan 26, 2026
cd232df
Update error message
dondonz Jan 26, 2026
b8b8a5a
Update tests given classiciation is not nullable
dondonz Jan 26, 2026
91f0f5c
Correctly set local context and context to nullable
dondonz Jan 26, 2026
4a05a72
Add mocks for i18n to accommodate non-nullable validation errors
dondonz Jan 26, 2026
b00dafe
Add another i18n mock
dondonz Jan 26, 2026
85e9854
Merge branch 'master' into infer-nullity-3
dondonz Feb 8, 2026
0a4d5d0
Add more nullable annotations
dondonz Feb 8, 2026
d83f175
Add assert
dondonz Feb 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .claude/commands/jspecify-annotate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
The task is to annotate public API classes (marked with `@PublicAPI`) with JSpecify nullability annotations.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This prompt is checked in so subagents can all share this context

This prompt will be deleted after this series of JSpecify annotations is completed


Note that JSpecify is already used in this repository so it's already imported.

If you see a builder static class, you can label it `@NullUnmarked` and not need to do anymore for this static class in terms of annotations.

Analyze this Java class and add JSpecify annotations based on:
1. Set the class to be `@NullMarked`
2. Remove all the redundant `@NonNull` annotations that IntelliJ added
3. Check Javadoc @param tags mentioning "null", "nullable", "may be null"
4. Check Javadoc @return tags mentioning "null", "optional", "if available"
5. Method implementations that return null or check for null
6. GraphQL specification details (see details below)

## GraphQL Specification Compliance
This is a GraphQL implementation. When determining nullability, consult the GraphQL specification (https://spec.graphql.org/draft/) for the relevant concept. Key principles:

The spec defines which elements are required (non-null) vs optional (nullable). Look for keywords like "MUST" to indicate when an element is required, and conditional words such as "IF" to indicate when an element is optional.

If a class implements or represents a GraphQL specification concept, prioritize the spec's nullability requirements over what IntelliJ inferred.

## How to validate
Finally, please check all this works by running the NullAway compile check.

If you find NullAway errors, try and make the smallest possible change to fix them. If you must, you can use assertNotNull. Make sure to include a message as well.

## Formatting Guidelines

Do not make spacing or formatting changes. Avoid adjusting whitespace, line breaks, or other formatting when editing code. These changes make diffs messy and harder to review. Only make the minimal changes necessary to accomplish the task.

## Cleaning up
Finally, can you remove this class from the JSpecifyAnnotationsCheck as an exemption

You do not need to run the JSpecifyAnnotationsCheck. Removing the completed class is enough.

Remember to delete all unused imports when you're done from the class you've just annotated.

## Generics Annotations

When annotating generic types and methods, follow these JSpecify rules:

### Type Parameter Bounds

The bound on a type parameter determines whether nullable type arguments are allowed:

| Declaration | Allows `@Nullable` type argument? |
|-------------|----------------------------------|
| `<T>` | ❌ No — `Box<@Nullable String>` is illegal |
| `<T extends @Nullable Object>` | ✅ Yes — `Box<@Nullable String>` is legal |

**When to use `<T extends @Nullable Object>`:**
- When callers genuinely need to parameterize with nullable types
- Example: `DataFetcherResult<T extends @Nullable Object>` — data fetchers may return nullable types

**When to keep `<T>`:**
- When the type parameter represents a concrete non-null object
- Even if some methods return `@Nullable T` (meaning "can be null even if T is non-null")
- Example: `Edge<T>` with `@Nullable T getNode()` — node may be null, but T represents the object type

3 changes: 3 additions & 0 deletions src/main/java/graphql/AssertException.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package graphql;


import org.jspecify.annotations.NullMarked;

@PublicApi
@NullMarked
public class AssertException extends GraphQLException {

public AssertException(String message) {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/graphql/Directives.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import graphql.language.DirectiveDefinition;
import graphql.language.StringValue;
import graphql.schema.GraphQLDirective;
import org.jspecify.annotations.NullMarked;

import java.util.Collections;
import java.util.LinkedHashMap;
Expand Down Expand Up @@ -39,6 +40,7 @@
* The directives that are understood by graphql-java
*/
@PublicApi
@NullMarked
public class Directives {

private static final String DEPRECATED = "deprecated";
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/graphql/ErrorClassification.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package graphql;

import org.jspecify.annotations.NullMarked;

/**
* Errors in graphql-java can have a classification to help with the processing
* of errors. Custom {@link graphql.GraphQLError} implementations could use
Expand All @@ -8,6 +10,7 @@
* graphql-java ships with a standard set of error classifications via {@link graphql.ErrorType}
*/
@PublicApi
@NullMarked
public interface ErrorClassification {

/**
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/graphql/ErrorType.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package graphql;


import org.jspecify.annotations.NullMarked;

/**
* All the errors in graphql belong to one of these categories
*/
@PublicApi
@NullMarked
public enum ErrorType implements ErrorClassification {
InvalidSyntax,
ValidationError,
Expand Down
9 changes: 6 additions & 3 deletions src/main/java/graphql/ExceptionWhileDataFetching.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import graphql.execution.ResultPath;
import graphql.language.SourceLocation;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

import java.util.Collections;
import java.util.LinkedHashMap;
Expand All @@ -16,13 +18,14 @@
* This graphql error will be used if a runtime exception is encountered while a data fetcher is invoked
*/
@PublicApi
@NullMarked
public class ExceptionWhileDataFetching implements GraphQLError {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nullability of these fields depends on a combination of the code and the GraphQL specification

Every error must contain an entry with the key "message" with a string description of the error intended for the developer as a guide to understand and correct the error.

  • Path is not nullable, there is an assert not null in the constructor below
  • Exception is not nullable, assert in constructor
  • Locations is at least a list, but not null
  • Extensions can be nullable from https://spec.graphql.org/draft/#sec-Extensions

The "extensions" entry in an execution result or request error result, if set, must have a map as its value.


private final String message;
private final List<Object> path;
private final Throwable exception;
private final List<SourceLocation> locations;
private final Map<String, Object> extensions;
private final @Nullable Map<String, Object> extensions;

public ExceptionWhileDataFetching(ResultPath path, Throwable exception, SourceLocation sourceLocation) {
this.path = assertNotNull(path).toList();
Expand All @@ -41,7 +44,7 @@ private String mkMessage(ResultPath path, Throwable exception) {
* exception into the ExceptionWhileDataFetching error and hence have custom "extension attributes"
* per error message.
*/
private Map<String, Object> mkExtensions(Throwable exception) {
private @Nullable Map<String, Object> mkExtensions(Throwable exception) {
Map<String, Object> extensions = null;
if (exception instanceof GraphQLError) {
Map<String, Object> map = ((GraphQLError) exception).getExtensions();
Expand Down Expand Up @@ -73,7 +76,7 @@ public List<Object> getPath() {
}

@Override
public Map<String, Object> getExtensions() {
public @Nullable Map<String, Object> getExtensions() {
return extensions;
}

Expand Down
4 changes: 2 additions & 2 deletions src/main/java/graphql/ExecutionInput.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
public class ExecutionInput {
private final String query;
private final String operationName;
private final Object context;
private final @Nullable Object context;
private final GraphQLContext graphQLContext;
private final Object localContext;
private final @Nullable Object localContext;
private final Object root;
private final RawVariables rawVariables;
private final Map<String, Object> extensions;
Expand Down
18 changes: 12 additions & 6 deletions src/main/java/graphql/ExecutionResult.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package graphql;


import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;

import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
Expand All @@ -9,6 +13,7 @@
* This simple value class represents the result of performing a graphql query.
*/
@PublicApi
@NullMarked
@SuppressWarnings("TypeParameterUnusedInFormals")
public interface ExecutionResult {

Expand All @@ -22,16 +27,16 @@ public interface ExecutionResult {
*
* @return the data in the result or null if there is none
*/
<T> T getData();
<T> @Nullable T getData();
Copy link
Member Author

@dondonz dondonz Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annotation inferred from the javadoc (and spec too)


/**
* The graphql specification specifies:
*
* <p>
* "If an error was encountered before execution begins, the data entry should not be present in the result.
* If an error was encountered during the execution that prevented a valid response, the data entry in the response should be null."
*
* <p>
* This allows to distinguish between the cases where {@link #getData()} returns null.
*
* <p>
* See : <a href="https://graphql.github.io/graphql-spec/June2018/#sec-Data">https://graphql.github.io/graphql-spec/June2018/#sec-Data</a>
*
* @return <code>true</code> if the entry "data" should be present in the result
Expand All @@ -42,14 +47,14 @@ public interface ExecutionResult {
/**
* @return a map of extensions or null if there are none
*/
Map<Object, Object> getExtensions();
@Nullable Map<Object, Object> getExtensions();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inferred from javadoc (and also spec)



/**
* The graphql specification says that result of a call should be a map that follows certain rules on what items
* should be present. Certain JSON serializers may or may interpret {@link ExecutionResult} to spec, so this method
* is provided to produce a map that strictly follows the specification.
*
* <p>
* See : <a href="https://spec.graphql.org/October2021/#sec-Response-Format">https://spec.graphql.org/October2021/#sec-Response-Format</a>
*
* @return a map of the result that strictly follows the spec
Expand Down Expand Up @@ -88,6 +93,7 @@ static Builder<?> newExecutionResult() {
return ExecutionResultImpl.newExecutionResult();
}

@NullUnmarked
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All builders are NullUnmarked

interface Builder<B extends Builder<B>> {

/**
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/graphql/GraphQL.java
Original file line number Diff line number Diff line change
Expand Up @@ -559,14 +559,14 @@ private PreparsedDocumentEntry parseAndValidate(AtomicReference<ExecutionInput>

ParseAndValidateResult parseResult = parse(executionInput, graphQLSchema, instrumentationState);
if (parseResult.isFailure()) {
return new PreparsedDocumentEntry(parseResult.getSyntaxException().toInvalidSyntaxError());
return new PreparsedDocumentEntry(assertNotNull(parseResult.getSyntaxException(), "Parse result syntax exception cannot be null when failed").toInvalidSyntaxError());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A similar change now has to happen in a bunch of places: now we must be asserting not null on selected lines or else you'll get a NullAway compile warning because the nullity is not clear at this point in the execution

} else {
final Document document = parseResult.getDocument();
// they may have changed the document and the variables via instrumentation so update the reference to it
executionInput = executionInput.transform(builder -> builder.variables(parseResult.getVariables()));
executionInputRef.set(executionInput);

final List<ValidationError> errors = validate(executionInput, document, graphQLSchema, instrumentationState);
final List<ValidationError> errors = validate(executionInput, assertNotNull(document, "Document cannot be null when parse succeeded"), graphQLSchema, instrumentationState);
if (!errors.isEmpty()) {
return new PreparsedDocumentEntry(document, errors);
}
Expand Down Expand Up @@ -599,7 +599,7 @@ private List<ValidationError> validate(ExecutionInput executionInput, Document d
validationCtx.onDispatched();

Predicate<Class<?>> validationRulePredicate = executionInput.getGraphQLContext().getOrDefault(ParseAndValidate.INTERNAL_VALIDATION_PREDICATE_HINT, r -> true);
Locale locale = executionInput.getLocale() != null ? executionInput.getLocale() : Locale.getDefault();
Locale locale = executionInput.getLocale();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And now there are IDE alerts of null checks we can remove. When we are done with all JSpecify annotations on public API classes, we will find many more null checks to remove

List<ValidationError> validationErrors = ParseAndValidate.validate(graphQLSchema, document, validationRulePredicate, locale);

validationCtx.onCompleted(validationErrors, null);
Expand Down
18 changes: 12 additions & 6 deletions src/main/java/graphql/GraphQLContext.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package graphql;

import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;

import java.util.Map;
import java.util.Objects;
import java.util.Optional;
Expand Down Expand Up @@ -32,12 +36,13 @@
* You can set this up via {@link ExecutionInput#getGraphQLContext()}
*
* All keys and values in the context MUST be non null.
*
* <p>
* The class is mutable via a thread safe implementation but it is recommended to try to use this class in an immutable way if you can.
*/
@PublicApi
@ThreadSafe
@SuppressWarnings("unchecked")
@NullMarked
public class GraphQLContext {

private final ConcurrentMap<Object, Object> map;
Expand Down Expand Up @@ -66,7 +71,7 @@ public GraphQLContext delete(Object key) {
*
* @return a value or null
*/
public <T> T get(Object key) {
public <T> @Nullable T get(Object key) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: another case of the nullable generic issue

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update - all values stored in the GraphQLContext are non-null. It's only going to return null if the key is not found. Let's keep it like this.

return (T) map.get(assertNotNull(key));
}

Expand Down Expand Up @@ -210,7 +215,7 @@ public GraphQLContext putAll(Consumer<GraphQLContext.Builder> contextBuilderCons
*
* @return the new value associated with the specified key, or null if none
*/
public <T> T compute(Object key, BiFunction<Object, ? super T, ? extends T> remappingFunction) {
public <T> @Nullable T compute(Object key, BiFunction<Object, ? super T, ? extends T> remappingFunction) {
assertNotNull(remappingFunction);
return (T) map.compute(assertNotNull(key), (k, v) -> remappingFunction.apply(k, (T) v));
}
Expand All @@ -226,7 +231,7 @@ public <T> T compute(Object key, BiFunction<Object, ? super T, ? extends T> rema
* @return the current (existing or computed) value associated with the specified key, or null if the computed value is null
*/

public <T> T computeIfAbsent(Object key, Function<Object, ? extends T> mappingFunction) {
public <T> @Nullable T computeIfAbsent(Object key, Function<Object, ? extends T> mappingFunction) {
return (T) map.computeIfAbsent(assertNotNull(key), assertNotNull(mappingFunction));
}

Expand All @@ -241,7 +246,7 @@ public <T> T computeIfAbsent(Object key, Function<Object, ? extends T> mappingFu
* @return the new value associated with the specified key, or null if none
*/

public <T> T computeIfPresent(Object key, BiFunction<Object, ? super T, ? extends T> remappingFunction) {
public <T> @Nullable T computeIfPresent(Object key, BiFunction<Object, ? super T, ? extends T> remappingFunction) {
assertNotNull(remappingFunction);
return (T) map.computeIfPresent(assertNotNull(key), (k, v) -> remappingFunction.apply(k, (T) v));
}
Expand All @@ -254,7 +259,7 @@ public Stream<Map.Entry<Object, Object>> stream() {
}

@Override
public boolean equals(Object o) {
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
Expand Down Expand Up @@ -315,6 +320,7 @@ public static Builder newContext() {
return new Builder();
}

@NullUnmarked
public static class Builder {
private final ConcurrentMap<Object, Object> map = new ConcurrentHashMap<>();

Expand Down
Loading
Loading