Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
70 changes: 68 additions & 2 deletions .claude/commands/jspecify-annotate.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ 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.

## Batch Size and Prioritization

Annotate approximately 10 classes per batch for optimal context management. Start with interface/simple classes first, then tackle complex classes with builders. This helps identify patterns early.

## Exploration Phase

Before annotating, use `grep` to search for how each class is instantiated (e.g., `grep -r "new Comment"`) to understand which parameters can be null. Check constructor calls, method returns, and field assignments to inform your nullability decisions.

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
Expand All @@ -12,14 +20,73 @@ Analyze this Java class and add JSpecify annotations based on:
5. Method implementations that return null or check for null
6. GraphQL specification details (see details below)

## Pattern Examples

Here are concrete examples of common annotation patterns:

**Interface:**
```java
@PublicApi
@NullMarked
public interface MyInterface {
// Methods inherit @NullMarked context
}
```

**Class with nullable field:**
```java
@PublicApi
@NullMarked
public class Comment {
private final String content;
private final @Nullable SourceLocation sourceLocation;

public Comment(String content, @Nullable SourceLocation sourceLocation) {
this.content = content;
this.sourceLocation = sourceLocation;
}

public @Nullable SourceLocation getSourceLocation() {
return sourceLocation;
}
}
```

**Class with nullable return type:**
```java
@PublicApi
@NullMarked
public class Container {
public @Nullable Node getChildOrNull(String key) {
// May return null
return children.get(key);
}
}
```

**Builder with @NullUnmarked:**
```java
@PublicApi
@NullMarked
public class MyClass {
@NullUnmarked
public static class Builder {
// No further annotations needed in builder
}
}
```

## 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
## Validation Strategy

Run `./gradlew compileJava` after every 3-5 classes annotated, not just at the end. This catches issues early and makes debugging easier.

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.
Expand Down Expand Up @@ -56,4 +123,3 @@ The bound on a type parameter determines whether nullable type arguments are all
- 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

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import graphql.Internal;
import graphql.PublicApi;
import graphql.execution.incremental.AlternativeCallContext;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import org.jspecify.annotations.NullUnmarked;

import java.util.function.Consumer;

Expand All @@ -13,6 +15,7 @@
* The parameters that are passed to execution strategies
*/
@PublicApi
@NullMarked
public class ExecutionStrategyParameters {
private final ExecutionStepInfo executionStepInfo;
private final Object source;
Expand Down Expand Up @@ -212,6 +215,7 @@ public static Builder newParameters(ExecutionStrategyParameters oldParameters) {
return new Builder(oldParameters);
}

@NullUnmarked
public static class Builder {
ExecutionStepInfo executionStepInfo;
Object source;
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/graphql/execution/FetchedValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import graphql.GraphQLError;
import graphql.PublicApi;
import graphql.execution.instrumentation.parameters.InstrumentationFieldCompleteParameters;
import org.jspecify.annotations.NullMarked;

import java.util.List;

Expand All @@ -12,6 +13,7 @@
* and therefore part of the public despite never used in a method signature.
*/
@PublicApi
@NullMarked
public class FetchedValue {
private final Object fetchedValue;
private final Object localContext;
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/graphql/execution/FieldValueInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.google.common.collect.ImmutableList;
import graphql.PublicApi;
import org.jspecify.annotations.NullMarked;

import java.util.List;
import java.util.concurrent.CompletableFuture;
Expand All @@ -19,6 +20,7 @@
* values might need a call to a database or other systems will tend to be {@link CompletableFuture} promises.
*/
@PublicApi
@NullMarked
public class FieldValueInfo {

public enum CompleteValueType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import graphql.language.SourceLocation;
import graphql.schema.GraphQLType;
import graphql.schema.GraphQLTypeUtil;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

import java.util.List;

Expand All @@ -16,14 +18,15 @@
* - This unordered map should not contain any entries with names not defined by a field of this input object type, otherwise an error should be thrown.
*/
@PublicApi
@NullMarked
public class InputMapDefinesTooManyFieldsException extends GraphQLException implements GraphQLError {

public InputMapDefinesTooManyFieldsException(GraphQLType graphQLType, String fieldName) {
super(String.format("The variables input contains a field name '%s' that is not defined for input object type '%s' ", fieldName, GraphQLTypeUtil.simplePrint(graphQLType)));
}

@Override
public List<SourceLocation> getLocations() {
public @Nullable List<SourceLocation> getLocations() {
return null;
}

Expand Down
7 changes: 6 additions & 1 deletion src/main/java/graphql/execution/MergedSelectionSet.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import graphql.PublicApi;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import org.jspecify.annotations.NullUnmarked;

import java.util.List;
import java.util.Map;
import java.util.Set;


@PublicApi
@NullMarked
public class MergedSelectionSet {

private final Map<String, MergedField> subFields;
Expand All @@ -36,7 +40,7 @@ public Set<String> keySet() {
return subFields.keySet();
}

public MergedField getSubField(String key) {
public @Nullable MergedField getSubField(String key) {
return subFields.get(key);
}

Expand All @@ -52,6 +56,7 @@ public static Builder newMergedSelectionSet() {
return new Builder();
}

@NullUnmarked
public static class Builder {

private Map<String, MergedField> subFields;
Expand Down
9 changes: 6 additions & 3 deletions src/main/java/graphql/execution/MissingRootTypeException.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,24 @@
import graphql.GraphQLException;
import graphql.PublicApi;
import graphql.language.SourceLocation;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

/**
* This is thrown if a query is attempting to perform an operation not defined in the GraphQL schema
*/
@PublicApi
@NullMarked
public class MissingRootTypeException extends GraphQLException implements GraphQLError {
private List<SourceLocation> sourceLocations;
private @Nullable List<SourceLocation> sourceLocations;

public MissingRootTypeException(String message, SourceLocation sourceLocation) {
public MissingRootTypeException(String message, @Nullable SourceLocation sourceLocation) {
super(message);
this.sourceLocations = sourceLocation == null ? null : Collections.singletonList(sourceLocation);
}

@Override
public List<SourceLocation> getLocations() {
public @Nullable List<SourceLocation> getLocations() {
return sourceLocations;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import graphql.schema.GraphQLInputObjectField;
import graphql.schema.GraphQLType;
import graphql.schema.GraphQLTypeUtil;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

import java.util.Collections;
import java.util.List;
Expand All @@ -20,9 +22,10 @@
* This is thrown if a non nullable value is coerced to a null value
*/
@PublicApi
@NullMarked
public class NonNullableValueCoercedAsNullException extends GraphQLException implements GraphQLError {
private List<SourceLocation> sourceLocations;
private List<Object> path;
private @Nullable List<SourceLocation> sourceLocations;
private @Nullable List<Object> path;

public NonNullableValueCoercedAsNullException(VariableDefinition variableDefinition, GraphQLType graphQLType) {
super(format("Variable '%s' has coerced Null value for NonNull type '%s'",
Expand Down Expand Up @@ -74,12 +77,12 @@ public NonNullableValueCoercedAsNullException(GraphQLArgument graphQLArgument) {
}

@Override
public List<SourceLocation> getLocations() {
public @Nullable List<SourceLocation> getLocations() {
return sourceLocations;
}

@Override
public List<Object> getPath() {
public @Nullable List<Object> getPath() {
return path;
}

Expand Down
5 changes: 4 additions & 1 deletion src/main/java/graphql/execution/NormalizedVariables.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
import graphql.collect.ImmutableKit;
import graphql.collect.ImmutableMapWithNullValues;
import graphql.normalized.NormalizedInputValue;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

import java.util.Map;

/**
* Holds coerced variables, that is their values are now in a normalized {@link graphql.normalized.NormalizedInputValue} form.
*/
@PublicApi
@NullMarked
public class NormalizedVariables {
private final ImmutableMapWithNullValues<String, NormalizedInputValue> normalisedVariables;

Expand All @@ -26,7 +29,7 @@ public boolean containsKey(String key) {
return normalisedVariables.containsKey(key);
}

public Object get(String key) {
public @Nullable Object get(String key) {
return normalisedVariables.get(key);
}

Expand Down
5 changes: 4 additions & 1 deletion src/main/java/graphql/execution/OneOfNullValueException.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,24 @@
import graphql.GraphQLException;
import graphql.PublicApi;
import graphql.language.SourceLocation;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

import java.util.List;

/**
* The input map to One Of Input Types MUST only have 1 entry with a non null value
*/
@PublicApi
@NullMarked
public class OneOfNullValueException extends GraphQLException implements GraphQLError {

public OneOfNullValueException(String message) {
super(message);
}

@Override
public List<SourceLocation> getLocations() {
public @Nullable List<SourceLocation> getLocations() {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,24 @@
import graphql.GraphQLException;
import graphql.PublicApi;
import graphql.language.SourceLocation;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

import java.util.List;

/**
* The input map to One Of Input Types MUST only have 1 entry
*/
@PublicApi
@NullMarked
public class OneOfTooManyKeysException extends GraphQLException implements GraphQLError {

public OneOfTooManyKeysException(String message) {
super(message);
}

@Override
public List<SourceLocation> getLocations() {
public @Nullable List<SourceLocation> getLocations() {
return null;
}

Expand Down
9 changes: 6 additions & 3 deletions src/main/java/graphql/language/Comment.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
package graphql.language;

import graphql.PublicApi;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

import java.io.Serializable;

/**
* A single-line comment. These are comments that start with a {@code #} in source documents.
*/
@PublicApi
@NullMarked
public class Comment implements Serializable {
public final String content;
public final SourceLocation sourceLocation;
public final @Nullable SourceLocation sourceLocation;

public Comment(String content, SourceLocation sourceLocation) {
public Comment(String content, @Nullable SourceLocation sourceLocation) {
this.content = content;
this.sourceLocation = sourceLocation;
}
Expand All @@ -21,7 +24,7 @@ public String getContent() {
return content;
}

public SourceLocation getSourceLocation() {
public @Nullable SourceLocation getSourceLocation() {
return sourceLocation;
}
}
Loading
Loading