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

2 changes: 2 additions & 0 deletions src/main/java/graphql/execution/ResultNodesInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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

import java.util.concurrent.atomic.AtomicInteger;

Expand All @@ -14,6 +15,7 @@
* </p>
*/
@PublicApi
@NullMarked
public class ResultNodesInfo {

public static final String MAX_RESULT_NODES = "__MAX_RESULT_NODES";
Expand Down
35 changes: 19 additions & 16 deletions src/main/java/graphql/execution/ResultPath.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import graphql.AssertException;
import graphql.PublicApi;
import graphql.collect.ImmutableKit;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

import java.util.ArrayList;
import java.util.LinkedList;
Expand All @@ -21,6 +23,7 @@
* class represents that path as a series of segments.
*/
@PublicApi
@NullMarked
public class ResultPath {
private static final ResultPath ROOT_PATH = new ResultPath();

Expand All @@ -33,8 +36,8 @@ public static ResultPath rootPath() {
return ROOT_PATH;
}

private final ResultPath parent;
private final Object segment;
private final @Nullable ResultPath parent;
private final @Nullable Object segment;

// hash is effective immutable but lazily initialized similar to the hash code of java.lang.String
private int hash;
Expand Down Expand Up @@ -85,7 +88,7 @@ public ResultPath getPathWithoutListEnd() {
if (segment instanceof String) {
return this;
}
return parent;
return assertNotNull(parent, "parent should not be null for non-root path");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

TODO: check this

}

/**
Expand All @@ -104,18 +107,18 @@ public boolean isNamedSegment() {


public String getSegmentName() {
return (String) segment;
return (String) assertNotNull(segment, "segment should not be null");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

TODO: check if we want these asserts here

}

public int getSegmentIndex() {
return (int) segment;
return (int) assertNotNull(segment, "segment should not be null");
}

public Object getSegmentValue() {
return segment;
return assertNotNull(segment, "segment should not be null");
}

public ResultPath getParent() {
public @Nullable ResultPath getParent() {
return parent;
}

Expand Down Expand Up @@ -201,7 +204,7 @@ public ResultPath segment(int segment) {
*
* @return a new path with the last segment dropped off
*/
public ResultPath dropSegment() {
public @Nullable ResultPath dropSegment() {
if (this == rootPath()) {
return null;
}
Expand All @@ -218,7 +221,7 @@ public ResultPath dropSegment() {
*/
public ResultPath replaceSegment(int segment) {
assertTrue(!ROOT_PATH.equals(this), "You MUST not call this with the root path");
return new ResultPath(parent, segment);
return new ResultPath(assertNotNull(parent, "parent should not be null"), segment);
}

/**
Expand All @@ -231,7 +234,7 @@ public ResultPath replaceSegment(int segment) {
*/
public ResultPath replaceSegment(String segment) {
assertTrue(!ROOT_PATH.equals(this), "You MUST not call this with the root path");
return new ResultPath(parent, segment);
return new ResultPath(assertNotNull(parent, "parent should not be null"), segment);
}


Expand All @@ -258,12 +261,12 @@ public ResultPath append(ResultPath path) {

public ResultPath sibling(String siblingField) {
assertTrue(!ROOT_PATH.equals(this), "You MUST not call this with the root path");
return new ResultPath(this.parent, siblingField);
return new ResultPath(assertNotNull(this.parent, "parent should not be null"), siblingField);
}

public ResultPath sibling(int siblingField) {
assertTrue(!ROOT_PATH.equals(this), "You MUST not call this with the root path");
return new ResultPath(this.parent, siblingField);
return new ResultPath(assertNotNull(this.parent, "parent should not be null"), siblingField);
}

/**
Expand All @@ -275,7 +278,7 @@ public List<Object> toList() {
}
LinkedList<Object> list = new LinkedList<>();
ResultPath p = this;
while (p.segment != null) {
while (p != null && p.segment != null) {
list.addFirst(p.segment);
p = p.parent;
}
Expand All @@ -291,7 +294,7 @@ public List<String> getKeysOnly() {
}
LinkedList<String> list = new LinkedList<>();
ResultPath p = this;
while (p.segment != null) {
while (p != null && p.segment != null) {
if (p.segment instanceof String) {
list.addFirst((String) p.segment);
}
Expand Down Expand Up @@ -328,15 +331,15 @@ public boolean equals(Object o) {

ResultPath self = this;
ResultPath that = (ResultPath) o;
while (self.segment != null && that.segment != null) {
while (self != null && self.segment != null && that != null && that.segment != null) {
if (!Objects.equals(self.segment, that.segment)) {
return false;
}
self = self.parent;
that = that.parent;
}

return self.isRootPath() && that.isRootPath();
return (self == null || self.isRootPath()) && (that == null || that.isRootPath());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

TODO: double check this change in return statement

}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import graphql.ExceptionWhileDataFetching;
import graphql.PublicApi;
import graphql.language.SourceLocation;
import org.jspecify.annotations.NullMarked;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
Expand All @@ -12,6 +13,7 @@
* into the error collection
*/
@PublicApi
@NullMarked
public class SimpleDataFetcherExceptionHandler implements DataFetcherExceptionHandler {

static final SimpleDataFetcherExceptionHandler defaultImpl = new SimpleDataFetcherExceptionHandler();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import graphql.language.Field;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLObjectType;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import org.reactivestreams.FlowAdapters;
import org.reactivestreams.Publisher;

Expand All @@ -40,6 +42,7 @@
* See <a href="https://www.reactive-streams.org/">https://www.reactive-streams.org/</a>
*/
@PublicApi
@NullMarked
public class SubscriptionExecutionStrategy extends ExecutionStrategy {

/**
Expand Down Expand Up @@ -132,7 +135,7 @@ private CompletableFuture<Publisher<Object>> createSourceEventStream(ExecutionCo
* @return a reactive streams {@link Publisher} always
*/
@SuppressWarnings("unchecked")
private static Publisher<Object> mkReactivePublisher(Object publisherObj) {
private static @Nullable Publisher<Object> mkReactivePublisher(@Nullable Object publisherObj) {
if (publisherObj != null) {
if (publisherObj instanceof Publisher) {
return (Publisher<Object>) publisherObj;
Expand Down Expand Up @@ -182,7 +185,7 @@ private CompletableFuture<ExecutionResult> executeSubscriptionEvent(ExecutionCon
executionContext.getDataLoaderDispatcherStrategy().subscriptionEventCompletionDone(newParameters.getDeferredCallContext());
CompletableFuture<ExecutionResult> overallResult = fieldValueInfo
.getFieldValueFuture()
.thenApply(val -> new ExecutionResultImpl(val, newParameters.getDeferredCallContext().getErrors()))
.thenApply(val -> new ExecutionResultImpl(val, Assert.assertNotNull(newParameters.getDeferredCallContext(), "deferredCallContext should not be null").getErrors()))
.thenApply(executionResult -> wrapWithRootFieldName(newParameters, executionResult));

// dispatch instrumentation so they can know about each subscription event
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import graphql.GraphQLException;
import graphql.PublicApi;
import graphql.language.SourceLocation;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

import java.util.List;

Expand All @@ -15,13 +17,14 @@
* contained in the GraphQL query.
*/
@PublicApi
@NullMarked
public class UnknownOperationException extends GraphQLException implements GraphQLError {
public UnknownOperationException(String message) {
super(message);
}

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

Expand Down
2 changes: 2 additions & 0 deletions src/main/java/graphql/execution/UnresolvedTypeException.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
import graphql.schema.GraphQLNamedOutputType;
import graphql.schema.GraphQLType;
import graphql.schema.GraphQLTypeUtil;
import org.jspecify.annotations.NullMarked;

/**
* This is thrown if a {@link graphql.schema.TypeResolver} fails to give back a concrete type
* or provides a type that doesn't implement the given interface or union.
*/
@PublicApi
@NullMarked
public class UnresolvedTypeException extends GraphQLException {

private final GraphQLNamedOutputType interfaceOrUnionType;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package graphql.execution.conditional;

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

/**
* This callback interface allows custom implementations to decide if a field is included in a query or not.
Expand All @@ -9,6 +10,7 @@
* decisions on whether fields are considered part of a query.
*/
@ExperimentalApi
@NullMarked
public interface ConditionalNodeDecision {

/**
Expand Down
Loading
Loading