Skip to content
Draft
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
73 changes: 70 additions & 3 deletions .claude/commands/jspecify-annotate.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,89 @@ 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
2. Remove all the redundant `@NonNull` annotations that IntelliJ added, if there are any
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)

## 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.
If a class implements or represents a GraphQL specification concept, prioritize the spec's nullability requirements

## Validation Strategy

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

## 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.
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/graphql/execution/ResponseMapFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import graphql.ExperimentalApi;
import graphql.PublicSpi;
import org.jspecify.annotations.NullMarked;

import java.util.List;
import java.util.Map;
Expand All @@ -12,6 +13,7 @@
*/
@ExperimentalApi
@PublicSpi
@NullMarked
public interface ResponseMapFactory {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import graphql.ExperimentalApi;
import graphql.normalized.incremental.NormalizedDeferredExecution;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

/**
Expand All @@ -11,6 +12,7 @@
* for the normalized representation of @defer.
*/
@ExperimentalApi
@NullMarked
public class DeferredExecution {
private final String label;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
import graphql.schema.GraphQLSchema;
import graphql.validation.ValidationError;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import org.jspecify.annotations.NullUnmarked;

import java.util.AbstractMap;
import java.util.Arrays;
Expand All @@ -45,6 +47,7 @@
* @see graphql.execution.instrumentation.Instrumentation
*/
@PublicApi
@NullMarked
public class ChainedInstrumentation implements Instrumentation {

// This class is inspired from https://github.com/leangen/graphql-spqr/blob/master/src/main/java/io/leangen/graphql/GraphQLRuntime.java#L80
Expand Down Expand Up @@ -118,24 +121,24 @@ protected void chainedConsume(InstrumentationState state, BiConsumer<Instrumenta
}

@Override
public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters, InstrumentationState state) {
public @Nullable InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters, InstrumentationState state) {
return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginExecution(parameters, specificState));
}


@Override
public InstrumentationContext<Document> beginParse(InstrumentationExecutionParameters parameters, InstrumentationState state) {
public @Nullable InstrumentationContext<Document> beginParse(InstrumentationExecutionParameters parameters, InstrumentationState state) {
return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginParse(parameters, specificState));
}


@Override
public InstrumentationContext<List<ValidationError>> beginValidation(InstrumentationValidationParameters parameters, InstrumentationState state) {
public @Nullable InstrumentationContext<List<ValidationError>> beginValidation(InstrumentationValidationParameters parameters, InstrumentationState state) {
return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginValidation(parameters, specificState));
}

@Override
public InstrumentationContext<ExecutionResult> beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState state) {
public @Nullable InstrumentationContext<ExecutionResult> beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState state) {
return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginExecuteOperation(parameters, specificState));
}

Expand All @@ -145,7 +148,7 @@ public InstrumentationContext<ExecutionResult> beginExecuteOperation(Instrumenta
}

@Override
public ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) {
public @Nullable ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) {
if (instrumentations.isEmpty()) {
return ExecutionStrategyInstrumentationContext.NOOP;
}
Expand All @@ -172,12 +175,12 @@ public ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrument

@ExperimentalApi
@Override
public InstrumentationContext<Object> beginDeferredField(InstrumentationFieldParameters parameters, InstrumentationState state) {
public @Nullable InstrumentationContext<Object> beginDeferredField(InstrumentationFieldParameters parameters, InstrumentationState state) {
return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginDeferredField(parameters, specificState));
}

@Override
public InstrumentationContext<ExecutionResult> beginSubscribedFieldEvent(InstrumentationFieldParameters parameters, InstrumentationState state) {
public @Nullable InstrumentationContext<ExecutionResult> beginSubscribedFieldEvent(InstrumentationFieldParameters parameters, InstrumentationState state) {
return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginSubscribedFieldEvent(parameters, specificState));
}

Expand All @@ -188,12 +191,12 @@ public InstrumentationContext<ExecutionResult> beginSubscribedFieldEvent(Instrum

@SuppressWarnings("deprecation")
@Override
public InstrumentationContext<Object> beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
public @Nullable InstrumentationContext<Object> beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginFieldFetch(parameters, specificState));
}

@Override
public FieldFetchingInstrumentationContext beginFieldFetching(InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
public @Nullable FieldFetchingInstrumentationContext beginFieldFetching(InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
if (instrumentations.isEmpty()) {
return FieldFetchingInstrumentationContext.NOOP;
}
Expand Down Expand Up @@ -264,6 +267,7 @@ public CompletableFuture<ExecutionResult> instrumentExecutionResult(ExecutionRes
return resultsFuture.thenApply((results) -> results.isEmpty() ? executionResult : results.get(results.size() - 1));
}

@NullUnmarked
static class ChainedInstrumentationState implements InstrumentationState {
Copy link
Member

Choose a reason for hiding this comment

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

TODO: I think the prompt has confused builders and static classes

private final List<InstrumentationState> instrumentationStates;

Expand All @@ -286,6 +290,7 @@ private static CompletableFuture<InstrumentationState> combineAll(List<Instrumen
}
}

@NullUnmarked
private static class ChainedInstrumentationContext<T> implements InstrumentationContext<T> {

private final ImmutableList<InstrumentationContext<T>> contexts;
Expand All @@ -305,6 +310,7 @@ public void onCompleted(T result, Throwable t) {
}
}

@NullUnmarked
private static class ChainedExecutionStrategyInstrumentationContext implements ExecutionStrategyInstrumentationContext {

private final ImmutableList<ExecutionStrategyInstrumentationContext> contexts;
Expand Down Expand Up @@ -334,6 +340,7 @@ public void onFieldValuesException() {
}
}

@NullUnmarked
private static class ChainedExecuteObjectInstrumentationContext implements ExecuteObjectInstrumentationContext {

private final ImmutableList<ExecuteObjectInstrumentationContext> contexts;
Expand Down Expand Up @@ -363,6 +370,7 @@ public void onFieldValuesException() {
}
}

@NullUnmarked
private static class ChainedFieldFetchingInstrumentationContext implements FieldFetchingInstrumentationContext {

private final ImmutableList<FieldFetchingInstrumentationContext> contexts;
Expand Down Expand Up @@ -392,6 +400,7 @@ public void onCompleted(Object result, Throwable t) {
}
}

@NullUnmarked
private static class ChainedDeferredExecutionStrategyInstrumentationContext implements InstrumentationContext<Object> {


Expand All @@ -412,6 +421,7 @@ public void onCompleted(Object result, Throwable t) {
}
}

@NullUnmarked
@FunctionalInterface
private interface ChainedInstrumentationFunction<I, S, V, R> {
R apply(I instrumentation, S state, V value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
import graphql.PublicApi;
import graphql.collect.ImmutableMapWithNullValues;
import graphql.language.Document;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.NullUnmarked;

import java.util.Map;
import java.util.function.Consumer;

import static graphql.Assert.assertNotNull;

@PublicApi
@NullMarked
public class DocumentAndVariables {
private final Document document;
private final ImmutableMapWithNullValues<String, Object> variables;
Expand Down Expand Up @@ -37,6 +40,7 @@ public static Builder newDocumentAndVariables() {
return new Builder();
}

@NullUnmarked
public static class Builder {
private Document document;
private Map<String, Object> variables;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters;
import graphql.language.Document;
import graphql.validation.ValidationError;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

import java.util.List;
Expand Down Expand Up @@ -39,6 +40,7 @@
* as itself.
*/
@PublicApi
@NullMarked
public class NoContextChainedInstrumentation extends ChainedInstrumentation {

public NoContextChainedInstrumentation(List<Instrumentation> instrumentations) {
Expand All @@ -49,28 +51,28 @@ public NoContextChainedInstrumentation(Instrumentation... instrumentations) {
super(instrumentations);
}

private <T> T runAll(InstrumentationState state, BiConsumer<Instrumentation, InstrumentationState> stateConsumer) {
private <T> @Nullable T runAll(InstrumentationState state, BiConsumer<Instrumentation, InstrumentationState> stateConsumer) {
chainedConsume(state, stateConsumer);
return null;
}

@Override
public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters, InstrumentationState state) {
public @Nullable InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters, InstrumentationState state) {
return runAll(state, (instrumentation, specificState) -> instrumentation.beginExecution(parameters, specificState));
}

@Override
public InstrumentationContext<Document> beginParse(InstrumentationExecutionParameters parameters, InstrumentationState state) {
public @Nullable InstrumentationContext<Document> beginParse(InstrumentationExecutionParameters parameters, InstrumentationState state) {
return runAll(state, (instrumentation, specificState) -> instrumentation.beginParse(parameters, specificState));
}

@Override
public InstrumentationContext<List<ValidationError>> beginValidation(InstrumentationValidationParameters parameters, InstrumentationState state) {
public @Nullable InstrumentationContext<List<ValidationError>> beginValidation(InstrumentationValidationParameters parameters, InstrumentationState state) {
return runAll(state, (instrumentation, specificState) -> instrumentation.beginValidation(parameters, specificState));
}

@Override
public InstrumentationContext<ExecutionResult> beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState state) {
public @Nullable InstrumentationContext<ExecutionResult> beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState state) {
return runAll(state, (instrumentation, specificState) -> instrumentation.beginExecuteOperation(parameters, specificState));
}

Expand All @@ -80,7 +82,7 @@ public InstrumentationContext<ExecutionResult> beginExecuteOperation(Instrumenta
}

@Override
public ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) {
public @Nullable ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) {
return runAll(state, (instrumentation, specificState) -> instrumentation.beginExecutionStrategy(parameters, specificState));
}

Expand All @@ -90,12 +92,12 @@ public ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrument
}

@Override
public InstrumentationContext<Object> beginDeferredField(InstrumentationFieldParameters parameters, InstrumentationState state) {
public @Nullable InstrumentationContext<Object> beginDeferredField(InstrumentationFieldParameters parameters, InstrumentationState state) {
return runAll(state, (instrumentation, specificState) -> instrumentation.beginDeferredField(parameters, specificState));
}

@Override
public InstrumentationContext<ExecutionResult> beginSubscribedFieldEvent(InstrumentationFieldParameters parameters, InstrumentationState state) {
public @Nullable InstrumentationContext<ExecutionResult> beginSubscribedFieldEvent(InstrumentationFieldParameters parameters, InstrumentationState state) {
return runAll(state, (instrumentation, specificState) -> instrumentation.beginSubscribedFieldEvent(parameters, specificState));
}

Expand All @@ -105,12 +107,12 @@ public InstrumentationContext<ExecutionResult> beginSubscribedFieldEvent(Instrum
}

@Override
public InstrumentationContext<Object> beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
public @Nullable InstrumentationContext<Object> beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
return runAll(state, (instrumentation, specificState) -> instrumentation.beginFieldFetch(parameters, specificState));
}

@Override
public FieldFetchingInstrumentationContext beginFieldFetching(InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
public @Nullable FieldFetchingInstrumentationContext beginFieldFetching(InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
return runAll(state, (instrumentation, specificState) -> instrumentation.beginFieldFetching(parameters, specificState));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package graphql.execution.instrumentation;

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

/**
* An implementation of {@link graphql.execution.instrumentation.Instrumentation} that does nothing. It can be used
Expand All @@ -11,6 +12,7 @@
* @deprecated use {@link SimplePerformantInstrumentation} instead as a base class.
*/
@PublicApi
@NullMarked
@Deprecated(since = "2022-10-05")
public class SimpleInstrumentation implements Instrumentation {

Expand Down
Loading
Loading