Skip to content
Open
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: 69 additions & 1 deletion .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,74 @@ 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
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;
}
}
2 changes: 2 additions & 0 deletions src/main/java/graphql/language/Definition.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@


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

@PublicApi
@NullMarked
public interface Definition<T extends Definition> extends Node<T> {

}
5 changes: 4 additions & 1 deletion src/main/java/graphql/language/IgnoredChar.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package graphql.language;

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

import java.io.Serializable;
import java.util.Objects;
Expand All @@ -12,6 +14,7 @@
* This costs more memory but for certain use cases (like editors) this maybe be useful
*/
@PublicApi
@NullMarked
public class IgnoredChar implements Serializable {

public enum IgnoredCharKind {
Expand Down Expand Up @@ -51,7 +54,7 @@ public String toString() {
}

@Override
public boolean equals(Object o) {
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/graphql/language/IgnoredChars.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.google.common.collect.ImmutableList;
import graphql.PublicApi;
import graphql.collect.ImmutableKit;
import org.jspecify.annotations.NullMarked;

import java.io.Serializable;
import java.util.List;
Expand All @@ -14,6 +15,7 @@
* This costs more memory but for certain use cases (like editors) this maybe be useful
*/
@PublicApi
@NullMarked
public class IgnoredChars implements Serializable {

private final ImmutableList<IgnoredChar> left;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@


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

import java.util.List;

Expand All @@ -11,6 +12,7 @@
* @param <T> for two
*/
@PublicApi
@NullMarked
public interface ImplementingTypeDefinition<T extends TypeDefinition> extends TypeDefinition<T> {

List<Type> getImplements();
Expand Down
25 changes: 15 additions & 10 deletions src/main/java/graphql/language/InlineFragment.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
import graphql.collect.ImmutableKit;
import graphql.util.TraversalControl;
import graphql.util.TraverserContext;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;

import java.util.ArrayList;
import java.util.LinkedHashMap;
Expand All @@ -20,8 +23,9 @@
import static graphql.language.NodeChildrenContainer.newNodeChildrenContainer;

@PublicApi
@NullMarked
public class InlineFragment extends AbstractNode<InlineFragment> implements Selection<InlineFragment>, SelectionSetContainer<InlineFragment>, DirectivesContainer<InlineFragment> {
private final TypeName typeCondition;
private final @Nullable TypeName typeCondition;
private final NodeUtil.DirectivesHolder directives;
private final SelectionSet selectionSet;

Expand All @@ -30,10 +34,10 @@ public class InlineFragment extends AbstractNode<InlineFragment> implements Sele
public static final String CHILD_SELECTION_SET = "selectionSet";

@Internal
protected InlineFragment(TypeName typeCondition,
protected InlineFragment(@Nullable TypeName typeCondition,
List<Directive> directives,
SelectionSet selectionSet,
SourceLocation sourceLocation,
@Nullable SourceLocation sourceLocation,
List<Comment> comments,
IgnoredChars ignoredChars,
Map<String, String> additionalData) {
Expand All @@ -48,8 +52,8 @@ protected InlineFragment(TypeName typeCondition,
*
* @param typeCondition the type condition of the inline fragment
*/
public InlineFragment(TypeName typeCondition) {
this(typeCondition, emptyList(), null, null, emptyList(), IgnoredChars.EMPTY, emptyMap());
public InlineFragment(@Nullable TypeName typeCondition) {
this(typeCondition, emptyList(), SelectionSet.newSelectionSet().build(), null, emptyList(), IgnoredChars.EMPTY, emptyMap());
Copy link
Member

Choose a reason for hiding this comment

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

This is technically a breaking change, although it's to make this more correct

An inline fragment should not have an empty selection set, as per the specification.

}

/**
Expand All @@ -58,11 +62,11 @@ public InlineFragment(TypeName typeCondition) {
* @param typeCondition the type condition of the inline fragment
* @param selectionSet of the inline fragment
*/
public InlineFragment(TypeName typeCondition, SelectionSet selectionSet) {
public InlineFragment(@Nullable TypeName typeCondition, SelectionSet selectionSet) {
this(typeCondition, emptyList(), selectionSet, null, emptyList(), IgnoredChars.EMPTY, emptyMap());
}

public TypeName getTypeCondition() {
public @Nullable TypeName getTypeCondition() {
return typeCondition;
}

Expand Down Expand Up @@ -121,7 +125,7 @@ public InlineFragment withNewChildren(NodeChildrenContainer newChildren) {
}

@Override
public boolean isEqualTo(Node o) {
public boolean isEqualTo(@Nullable Node o) {
if (this == o) {
return true;
}
Expand All @@ -132,8 +136,8 @@ public boolean isEqualTo(Node o) {
public InlineFragment deepCopy() {
return new InlineFragment(
deepCopy(typeCondition),
deepCopy(directives.getDirectives()),
deepCopy(selectionSet),
assertNotNull(deepCopy(directives.getDirectives()), "directives cannot be null"),
assertNotNull(deepCopy(selectionSet), "selectionSet cannot be null"),
getSourceLocation(),
getComments(),
getIgnoredChars(),
Expand Down Expand Up @@ -164,6 +168,7 @@ public InlineFragment transform(Consumer<Builder> builderConsumer) {
return builder.build();
}

@NullUnmarked
public static final class Builder implements NodeDirectivesBuilder {
private SourceLocation sourceLocation;
private ImmutableList<Comment> comments = emptyList();
Expand Down
15 changes: 10 additions & 5 deletions src/main/java/graphql/language/InputObjectTypeDefinition.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
import graphql.util.FpKit;
import graphql.util.TraversalControl;
import graphql.util.TraverserContext;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;

import java.util.LinkedHashMap;
import java.util.List;
Expand All @@ -19,6 +22,7 @@
import static graphql.language.NodeChildrenContainer.newNodeChildrenContainer;

@PublicApi
@NullMarked
public class InputObjectTypeDefinition extends AbstractDescribedNode<InputObjectTypeDefinition> implements TypeDefinition<InputObjectTypeDefinition>, DirectivesContainer<InputObjectTypeDefinition>, NamedNode<InputObjectTypeDefinition> {

private final String name;
Expand All @@ -32,8 +36,8 @@ public class InputObjectTypeDefinition extends AbstractDescribedNode<InputObject
protected InputObjectTypeDefinition(String name,
List<Directive> directives,
List<InputValueDefinition> inputValueDefinitions,
Description description,
SourceLocation sourceLocation,
@Nullable Description description,
@Nullable SourceLocation sourceLocation,
List<Comment> comments,
IgnoredChars ignoredChars,
Map<String, String> additionalData) {
Expand Down Expand Up @@ -94,7 +98,7 @@ public InputObjectTypeDefinition withNewChildren(NodeChildrenContainer newChildr
}

@Override
public boolean isEqualTo(Node o) {
public boolean isEqualTo(@Nullable Node o) {
if (this == o) {
return true;
}
Expand All @@ -110,8 +114,8 @@ public boolean isEqualTo(Node o) {
@Override
public InputObjectTypeDefinition deepCopy() {
return new InputObjectTypeDefinition(name,
deepCopy(directives.getDirectives()),
deepCopy(inputValueDefinitions),
assertNotNull(deepCopy(directives.getDirectives()), "directives cannot be null"),
assertNotNull(deepCopy(inputValueDefinitions), "inputValueDefinitions cannot be null"),
description,
getSourceLocation(),
getComments(),
Expand Down Expand Up @@ -144,6 +148,7 @@ public InputObjectTypeDefinition transform(Consumer<Builder> builderConsumer) {
return builder.build();
}

@NullUnmarked
public static final class Builder implements NodeDirectivesBuilder {
private SourceLocation sourceLocation;
private ImmutableList<Comment> comments = emptyList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import graphql.Internal;
import graphql.PublicApi;
import graphql.collect.ImmutableKit;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;

import java.util.LinkedHashMap;
import java.util.List;
Expand All @@ -14,14 +17,15 @@
import static graphql.collect.ImmutableKit.emptyList;

@PublicApi
@NullMarked
public class InputObjectTypeExtensionDefinition extends InputObjectTypeDefinition implements SDLExtensionDefinition {

@Internal
protected InputObjectTypeExtensionDefinition(String name,
List<Directive> directives,
List<InputValueDefinition> inputValueDefinitions,
Description description,
SourceLocation sourceLocation,
@Nullable Description description,
@Nullable SourceLocation sourceLocation,
List<Comment> comments,
IgnoredChars ignoredChars,
Map<String, String> additionalData) {
Expand All @@ -31,8 +35,8 @@ protected InputObjectTypeExtensionDefinition(String name,
@Override
public InputObjectTypeExtensionDefinition deepCopy() {
return new InputObjectTypeExtensionDefinition(getName(),
deepCopy(getDirectives()),
deepCopy(getInputValueDefinitions()),
assertNotNull(deepCopy(getDirectives()), "directives cannot be null"),
assertNotNull(deepCopy(getInputValueDefinitions()), "inputValueDefinitions cannot be null"),
getDescription(),
getSourceLocation(),
getComments(),
Expand Down Expand Up @@ -67,6 +71,7 @@ public InputObjectTypeExtensionDefinition transformExtension(Consumer<Builder> b
return builder.build();
}

@NullUnmarked
public static final class Builder implements NodeDirectivesBuilder {
private SourceLocation sourceLocation;
private ImmutableList<Comment> comments = emptyList();
Expand Down
Loading
Loading