diff --git a/.claude/commands/jspecify-annotate.md b/.claude/commands/jspecify-annotate.md index 7083a6b3da..b6e59da6af 100644 --- a/.claude/commands/jspecify-annotate.md +++ b/.claude/commands/jspecify-annotate.md @@ -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 @@ -12,6 +20,62 @@ 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: @@ -19,7 +83,10 @@ The spec defines which elements are required (non-null) vs optional (nullable). 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. @@ -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` with `@Nullable T getNode()` — node may be null, but T represents the object type - diff --git a/src/main/java/graphql/language/Comment.java b/src/main/java/graphql/language/Comment.java index a7a546facf..65096c6782 100644 --- a/src/main/java/graphql/language/Comment.java +++ b/src/main/java/graphql/language/Comment.java @@ -1,6 +1,8 @@ package graphql.language; import graphql.PublicApi; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import java.io.Serializable; @@ -8,11 +10,12 @@ * 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; } @@ -21,7 +24,7 @@ public String getContent() { return content; } - public SourceLocation getSourceLocation() { + public @Nullable SourceLocation getSourceLocation() { return sourceLocation; } } diff --git a/src/main/java/graphql/language/Definition.java b/src/main/java/graphql/language/Definition.java index f0e7d74dc9..5402bfe3c6 100644 --- a/src/main/java/graphql/language/Definition.java +++ b/src/main/java/graphql/language/Definition.java @@ -2,8 +2,10 @@ import graphql.PublicApi; +import org.jspecify.annotations.NullMarked; @PublicApi +@NullMarked public interface Definition extends Node { } diff --git a/src/main/java/graphql/language/IgnoredChar.java b/src/main/java/graphql/language/IgnoredChar.java index eaa1689d0c..d316a4d6b1 100644 --- a/src/main/java/graphql/language/IgnoredChar.java +++ b/src/main/java/graphql/language/IgnoredChar.java @@ -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; @@ -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 { @@ -51,7 +54,7 @@ public String toString() { } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } diff --git a/src/main/java/graphql/language/IgnoredChars.java b/src/main/java/graphql/language/IgnoredChars.java index ce3a1ad59c..241b4cc744 100644 --- a/src/main/java/graphql/language/IgnoredChars.java +++ b/src/main/java/graphql/language/IgnoredChars.java @@ -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; @@ -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 left; diff --git a/src/main/java/graphql/language/Node.java b/src/main/java/graphql/language/Node.java index 962934d76b..917594b876 100644 --- a/src/main/java/graphql/language/Node.java +++ b/src/main/java/graphql/language/Node.java @@ -4,6 +4,7 @@ import graphql.PublicApi; import graphql.util.TraversalControl; import graphql.util.TraverserContext; +import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import java.io.Serializable; @@ -21,6 +22,7 @@ * Every Node is immutable */ @PublicApi +@NullMarked public interface Node extends Serializable { /** diff --git a/src/main/java/graphql/language/NodeChildrenContainer.java b/src/main/java/graphql/language/NodeChildrenContainer.java index d2e1b06fea..a986ebca76 100644 --- a/src/main/java/graphql/language/NodeChildrenContainer.java +++ b/src/main/java/graphql/language/NodeChildrenContainer.java @@ -1,6 +1,9 @@ package graphql.language; import graphql.PublicApi; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -15,6 +18,7 @@ * Container of children of a {@link Node}. */ @PublicApi +@NullMarked public class NodeChildrenContainer { private final Map> children = new LinkedHashMap<>(); @@ -27,7 +31,7 @@ public List getChildren(String key) { return (List) children.getOrDefault(key, emptyList()); } - public T getChildOrNull(String key) { + public @Nullable T getChildOrNull(String key) { List result = children.getOrDefault(key, emptyList()); if (result.size() > 1) { throw new IllegalStateException("children " + key + " is not a single value"); @@ -61,6 +65,7 @@ public boolean isEmpty() { return this.children.isEmpty(); } + @NullUnmarked public static class Builder { private final Map> children = new LinkedHashMap<>(); diff --git a/src/main/java/graphql/language/NodeTraverser.java b/src/main/java/graphql/language/NodeTraverser.java index 916c290f95..781552a47c 100644 --- a/src/main/java/graphql/language/NodeTraverser.java +++ b/src/main/java/graphql/language/NodeTraverser.java @@ -7,6 +7,8 @@ import graphql.util.Traverser; import graphql.util.TraverserContext; import graphql.util.TraverserVisitor; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import java.util.Collection; import java.util.Collections; @@ -18,6 +20,7 @@ * Lets you traverse a {@link Node} tree. */ @PublicApi +@NullMarked public class NodeTraverser { @@ -151,7 +154,7 @@ private Object doTraverse(Collection roots, TraverserVisitor tra } @SuppressWarnings("TypeParameterUnusedInFormals") - public static T oneVisitWithResult(Node node, NodeVisitor nodeVisitor) { + public static @Nullable T oneVisitWithResult(Node node, NodeVisitor nodeVisitor) { DefaultTraverserContext context = DefaultTraverserContext.simple(node); node.accept(context, nodeVisitor); return (T) context.getNewAccumulate(); diff --git a/src/main/java/graphql/language/NodeVisitor.java b/src/main/java/graphql/language/NodeVisitor.java index 2ed79570b3..ca40709f5d 100644 --- a/src/main/java/graphql/language/NodeVisitor.java +++ b/src/main/java/graphql/language/NodeVisitor.java @@ -3,11 +3,13 @@ import graphql.PublicApi; import graphql.util.TraversalControl; import graphql.util.TraverserContext; +import org.jspecify.annotations.NullMarked; /** * Used by {@link NodeTraverser} to visit {@link Node}. */ @PublicApi +@NullMarked public interface NodeVisitor { TraversalControl visitArgument(Argument node, TraverserContext data); diff --git a/src/main/java/graphql/language/NodeVisitorStub.java b/src/main/java/graphql/language/NodeVisitorStub.java index f0fcd6b3fd..b5d00073c4 100644 --- a/src/main/java/graphql/language/NodeVisitorStub.java +++ b/src/main/java/graphql/language/NodeVisitorStub.java @@ -3,11 +3,13 @@ import graphql.PublicApi; import graphql.util.TraversalControl; import graphql.util.TraverserContext; +import org.jspecify.annotations.NullMarked; /** * Convenient implementation of {@link NodeVisitor} for easy subclassing methods handling different types of Nodes in one method. */ @PublicApi +@NullMarked public class NodeVisitorStub implements NodeVisitor { @Override public TraversalControl visitArgument(Argument node, TraverserContext context) { diff --git a/src/main/java/graphql/language/NonNullType.java b/src/main/java/graphql/language/NonNullType.java index e86e39c244..08dcf53ebb 100644 --- a/src/main/java/graphql/language/NonNullType.java +++ b/src/main/java/graphql/language/NonNullType.java @@ -6,6 +6,9 @@ import graphql.PublicApi; 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; @@ -18,6 +21,7 @@ import static graphql.language.NodeChildrenContainer.newNodeChildrenContainer; @PublicApi +@NullMarked public class NonNullType extends AbstractNode implements Type { private final Type type; @@ -25,7 +29,7 @@ public class NonNullType extends AbstractNode implements Type comments, IgnoredChars ignoredChars, Map additionalData) { + protected NonNullType(Type type, @Nullable SourceLocation sourceLocation, List comments, IgnoredChars ignoredChars, Map additionalData) { super(sourceLocation, comments, ignoredChars, additionalData); this.type = type; } @@ -63,7 +67,7 @@ public NonNullType withNewChildren(NodeChildrenContainer newChildren) { } @Override - public boolean isEqualTo(Node o) { + public boolean isEqualTo(@Nullable Node o) { if (this == o) { return true; } @@ -77,7 +81,7 @@ public boolean isEqualTo(Node o) { @Override public NonNullType deepCopy() { - return new NonNullType(deepCopy(type), getSourceLocation(), getComments(), getIgnoredChars(), getAdditionalData()); + return new NonNullType(assertNotNull(deepCopy(type), "type cannot be null"), getSourceLocation(), getComments(), getIgnoredChars(), getAdditionalData()); } @Override @@ -106,6 +110,7 @@ public NonNullType transform(Consumer builderConsumer) { return builder.build(); } + @NullUnmarked public static final class Builder implements NodeBuilder { private SourceLocation sourceLocation; private Type type; diff --git a/src/main/java/graphql/language/ObjectField.java b/src/main/java/graphql/language/ObjectField.java index 8c4532a41c..5265218c31 100644 --- a/src/main/java/graphql/language/ObjectField.java +++ b/src/main/java/graphql/language/ObjectField.java @@ -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.LinkedHashMap; import java.util.List; @@ -19,6 +22,7 @@ import static graphql.language.NodeChildrenContainer.newNodeChildrenContainer; @PublicApi +@NullMarked public class ObjectField extends AbstractNode implements NamedNode { private final String name; @@ -27,7 +31,7 @@ public class ObjectField extends AbstractNode implements NamedNode< public static final String CHILD_VALUE = "value"; @Internal - protected ObjectField(String name, Value value, SourceLocation sourceLocation, List comments, IgnoredChars ignoredChars, Map additionalData) { + protected ObjectField(String name, Value value, @Nullable SourceLocation sourceLocation, List comments, IgnoredChars ignoredChars, Map additionalData) { super(sourceLocation, comments, ignoredChars, additionalData); this.name = assertNotNull(name); this.value = assertNotNull(value); @@ -72,7 +76,7 @@ public ObjectField withNewChildren(NodeChildrenContainer newChildren) { } @Override - public boolean isEqualTo(Node o) { + public boolean isEqualTo(@Nullable Node o) { if (this == o) { return true; } @@ -88,7 +92,7 @@ public boolean isEqualTo(Node o) { @Override public ObjectField deepCopy() { - return new ObjectField(name, deepCopy(this.value), getSourceLocation(), getComments(), getIgnoredChars(), getAdditionalData()); + return new ObjectField(name, assertNotNull(deepCopy(this.value), "value cannot be null"), getSourceLocation(), getComments(), getIgnoredChars(), getAdditionalData()); } @Override @@ -114,6 +118,7 @@ public ObjectField transform(Consumer builderConsumer) { return builder.build(); } + @NullUnmarked public static final class Builder implements NodeBuilder { private SourceLocation sourceLocation; private String name; diff --git a/src/main/java/graphql/language/ObjectTypeDefinition.java b/src/main/java/graphql/language/ObjectTypeDefinition.java index fca017594e..6437ac5e96 100644 --- a/src/main/java/graphql/language/ObjectTypeDefinition.java +++ b/src/main/java/graphql/language/ObjectTypeDefinition.java @@ -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; @@ -21,6 +24,7 @@ import static graphql.language.NodeChildrenContainer.newNodeChildrenContainer; @PublicApi +@NullMarked public class ObjectTypeDefinition extends AbstractDescribedNode implements ImplementingTypeDefinition, DirectivesContainer, NamedNode { private final String name; private final ImmutableList implementz; @@ -36,8 +40,8 @@ protected ObjectTypeDefinition(String name, List implementz, List directives, List fieldDefinitions, - Description description, - SourceLocation sourceLocation, + @Nullable Description description, + @Nullable SourceLocation sourceLocation, List comments, IgnoredChars ignoredChars, Map additionalData) { @@ -117,7 +121,7 @@ public ObjectTypeDefinition withNewChildren(NodeChildrenContainer newChildren) { } @Override - public boolean isEqualTo(Node o) { + public boolean isEqualTo(@Nullable Node o) { if (this == o) { return true; } @@ -133,9 +137,9 @@ public boolean isEqualTo(Node o) { @Override public ObjectTypeDefinition deepCopy() { return new ObjectTypeDefinition(name, - deepCopy(implementz), - deepCopy(directives.getDirectives()), - deepCopy(fieldDefinitions), + assertNotNull(deepCopy(implementz), "implementz cannot be null"), + assertNotNull(deepCopy(directives.getDirectives()), "directives cannot be null"), + assertNotNull(deepCopy(fieldDefinitions), "fieldDefinitions cannot be null"), description, getSourceLocation(), getComments(), @@ -168,6 +172,7 @@ public ObjectTypeDefinition transform(Consumer builderConsumer) { return builder.build(); } + @NullUnmarked public static final class Builder implements NodeDirectivesBuilder { private SourceLocation sourceLocation; private ImmutableList comments = emptyList(); diff --git a/src/main/java/graphql/language/ObjectTypeExtensionDefinition.java b/src/main/java/graphql/language/ObjectTypeExtensionDefinition.java index 575827564f..b6f344e39d 100644 --- a/src/main/java/graphql/language/ObjectTypeExtensionDefinition.java +++ b/src/main/java/graphql/language/ObjectTypeExtensionDefinition.java @@ -5,6 +5,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; @@ -16,6 +19,7 @@ import static graphql.collect.ImmutableKit.emptyMap; @PublicApi +@NullMarked public class ObjectTypeExtensionDefinition extends ObjectTypeDefinition implements SDLExtensionDefinition { @Internal @@ -23,8 +27,8 @@ protected ObjectTypeExtensionDefinition(String name, List implementz, List directives, List fieldDefinitions, - Description description, - SourceLocation sourceLocation, + @Nullable Description description, + @Nullable SourceLocation sourceLocation, List comments, IgnoredChars ignoredChars, Map additionalData) { @@ -44,9 +48,9 @@ public ObjectTypeExtensionDefinition(String name) { @Override public ObjectTypeExtensionDefinition deepCopy() { return new ObjectTypeExtensionDefinition(getName(), - deepCopy(getImplements()), - deepCopy(getDirectives()), - deepCopy(getFieldDefinitions()), + assertNotNull(deepCopy(getImplements()), "implements cannot be null"), + assertNotNull(deepCopy(getDirectives()), "directives cannot be null"), + assertNotNull(deepCopy(getFieldDefinitions()), "fieldDefinitions cannot be null"), getDescription(), getSourceLocation(), getComments(), @@ -81,6 +85,7 @@ public ObjectTypeExtensionDefinition transformExtension(Consumer builde return builder.build(); } + @NullUnmarked public static final class Builder implements NodeDirectivesBuilder { private SourceLocation sourceLocation; private ImmutableList comments = emptyList(); diff --git a/src/main/java/graphql/language/OperationDefinition.java b/src/main/java/graphql/language/OperationDefinition.java index 824180bb49..85854dff91 100644 --- a/src/main/java/graphql/language/OperationDefinition.java +++ b/src/main/java/graphql/language/OperationDefinition.java @@ -8,6 +8,9 @@ import graphql.language.NodeUtil.DirectivesHolder; 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; @@ -22,30 +25,31 @@ import static graphql.language.NodeChildrenContainer.newNodeChildrenContainer; @PublicApi -public class OperationDefinition extends AbstractNode implements Definition, SelectionSetContainer, DirectivesContainer, NamedNode { +@NullMarked +public class OperationDefinition extends AbstractNode implements Definition, SelectionSetContainer, DirectivesContainer { public enum Operation { QUERY, MUTATION, SUBSCRIPTION } - private final String name; + private final @Nullable String name; - private final Operation operation; + private final @Nullable Operation operation; private final ImmutableList variableDefinitions; private final DirectivesHolder directives; - private final SelectionSet selectionSet; + private final @Nullable SelectionSet selectionSet; public static final String CHILD_VARIABLE_DEFINITIONS = "variableDefinitions"; public static final String CHILD_DIRECTIVES = "directives"; public static final String CHILD_SELECTION_SET = "selectionSet"; @Internal - protected OperationDefinition(String name, - Operation operation, + protected OperationDefinition(@Nullable String name, + @Nullable Operation operation, List variableDefinitions, List directives, - SelectionSet selectionSet, - SourceLocation sourceLocation, + @Nullable SelectionSet selectionSet, + @Nullable SourceLocation sourceLocation, List comments, IgnoredChars ignoredChars, Map additionalData) { @@ -57,12 +61,12 @@ protected OperationDefinition(String name, this.selectionSet = selectionSet; } - public OperationDefinition(String name, - Operation operation) { + public OperationDefinition(@Nullable String name, + @Nullable Operation operation) { this(name, operation, emptyList(), emptyList(), null, null, emptyList(), IgnoredChars.EMPTY, emptyMap()); } - public OperationDefinition(String name) { + public OperationDefinition(@Nullable String name) { this(name, null, emptyList(), emptyList(), null, null, emptyList(), IgnoredChars.EMPTY, emptyMap()); } @@ -93,11 +97,11 @@ public OperationDefinition withNewChildren(NodeChildrenContainer newChildren) { ); } - public String getName() { + public @Nullable String getName() { return name; } - public Operation getOperation() { + public @Nullable Operation getOperation() { return operation; } @@ -125,12 +129,12 @@ public boolean hasDirective(String directiveName) { } @Override - public SelectionSet getSelectionSet() { + public @Nullable SelectionSet getSelectionSet() { return selectionSet; } @Override - public boolean isEqualTo(Node o) { + public boolean isEqualTo(@Nullable Node o) { if (this == o) { return true; } @@ -148,8 +152,8 @@ public boolean isEqualTo(Node o) { public OperationDefinition deepCopy() { return new OperationDefinition(name, operation, - deepCopy(variableDefinitions), - deepCopy(directives.getDirectives()), + assertNotNull(deepCopy(variableDefinitions), "variableDefinitions cannot be null"), + assertNotNull(deepCopy(directives.getDirectives()), "directives cannot be null"), deepCopy(selectionSet), getSourceLocation(), getComments(), @@ -183,6 +187,7 @@ public OperationDefinition transform(Consumer builderConsumer) { return builder.build(); } + @NullUnmarked public static final class Builder implements NodeDirectivesBuilder { private SourceLocation sourceLocation; private ImmutableList comments = emptyList(); diff --git a/src/main/java/graphql/language/OperationTypeDefinition.java b/src/main/java/graphql/language/OperationTypeDefinition.java index 5041e692cf..c8c2ab97ee 100644 --- a/src/main/java/graphql/language/OperationTypeDefinition.java +++ b/src/main/java/graphql/language/OperationTypeDefinition.java @@ -6,6 +6,9 @@ import graphql.PublicApi; 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; @@ -20,6 +23,7 @@ import static graphql.language.NodeChildrenContainer.newNodeChildrenContainer; @PublicApi +@NullMarked public class OperationTypeDefinition extends AbstractNode implements NamedNode { private final String name; @@ -28,7 +32,7 @@ public class OperationTypeDefinition extends AbstractNode comments, IgnoredChars ignoredChars, Map additionalData) { + protected OperationTypeDefinition(String name, TypeName typeName, @Nullable SourceLocation sourceLocation, List comments, IgnoredChars ignoredChars, Map additionalData) { super(sourceLocation, comments, ignoredChars, additionalData); this.name = name; this.typeName = typeName; @@ -75,7 +79,7 @@ public OperationTypeDefinition withNewChildren(NodeChildrenContainer newChildren } @Override - public boolean isEqualTo(Node o) { + public boolean isEqualTo(@Nullable Node o) { if (this == o) { return true; } @@ -90,7 +94,7 @@ public boolean isEqualTo(Node o) { @Override public OperationTypeDefinition deepCopy() { - return new OperationTypeDefinition(name, deepCopy(typeName), getSourceLocation(), getComments(), getIgnoredChars(), getAdditionalData()); + return new OperationTypeDefinition(name, assertNotNull(deepCopy(typeName), "typeName cannot be null"), getSourceLocation(), getComments(), getIgnoredChars(), getAdditionalData()); } @Override @@ -116,6 +120,7 @@ public OperationTypeDefinition transform(Consumer builderConsumer) { return builder.build(); } + @NullUnmarked public static final class Builder implements NodeBuilder { private SourceLocation sourceLocation; private ImmutableList comments = emptyList(); diff --git a/src/main/java/graphql/language/PrettyAstPrinter.java b/src/main/java/graphql/language/PrettyAstPrinter.java index a5fc4628b1..d52345ecd7 100644 --- a/src/main/java/graphql/language/PrettyAstPrinter.java +++ b/src/main/java/graphql/language/PrettyAstPrinter.java @@ -5,6 +5,9 @@ import graphql.parser.CommentParser; import graphql.parser.NodeToRuleCapturingParser; import graphql.parser.ParserEnvironment; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import java.util.Collections; import java.util.List; @@ -26,6 +29,7 @@ * @see AstPrinter */ @ExperimentalApi +@NullMarked public class PrettyAstPrinter extends AstPrinter { private final CommentParser commentParser; private final PrettyPrinterOptions options; @@ -218,7 +222,7 @@ private NodePrinter unionTypeDefinition(String nodeName) { }; } - private String node(Node node, Class startClass) { + private String node(Node node, @Nullable Class startClass) { if (startClass != null) { assertTrue(startClass.isInstance(node), "The starting class must be in the inherit tree"); } @@ -238,15 +242,15 @@ private String node(Node node, Class startClass) { return builder.toString(); } - private boolean isEmpty(List list) { + private boolean isEmpty(@Nullable List list) { return list == null || list.isEmpty(); } - private boolean isEmpty(String s) { + private boolean isEmpty(@Nullable String s) { return s == null || s.isBlank(); } - private List nvl(List list) { + private List nvl(@Nullable List list) { return list != null ? list : ImmutableKit.emptyList(); } @@ -342,7 +346,7 @@ private String join(String delim, String... args) { return joiner.toString(); } - private String block(List nodes, Node parentNode, String prefix, String suffix, String separatorMultiline, String separatorSingleLine, String whenEmpty) { + private String block(List nodes, Node parentNode, String prefix, String suffix, String separatorMultiline, @Nullable String separatorSingleLine, @Nullable String whenEmpty) { if (isEmpty(nodes)) { return whenEmpty != null ? whenEmpty : prefix + suffix; } @@ -403,6 +407,7 @@ private StringBuilder indent(StringBuilder stringBuilder) { /** * Contains options that modify how a document is printed. */ + @NullUnmarked public static class PrettyPrinterOptions { private final String indentText; private static final PrettyPrinterOptions defaultOptions = new PrettyPrinterOptions(IndentType.SPACE, 2); @@ -429,6 +434,7 @@ public enum IndentType { } } + @NullUnmarked public static class Builder { private IndentType indentType; private int indentWidth = 1; diff --git a/src/main/java/graphql/language/SDLDefinition.java b/src/main/java/graphql/language/SDLDefinition.java index 7b07a24919..0e7cfe3420 100644 --- a/src/main/java/graphql/language/SDLDefinition.java +++ b/src/main/java/graphql/language/SDLDefinition.java @@ -2,6 +2,7 @@ import graphql.PublicApi; +import org.jspecify.annotations.NullMarked; /** * An interface for Schema Definition Language (SDL) definitions. @@ -9,6 +10,7 @@ * @param the actual Node type */ @PublicApi +@NullMarked public interface SDLDefinition extends Definition { } diff --git a/src/main/java/graphql/language/SDLExtensionDefinition.java b/src/main/java/graphql/language/SDLExtensionDefinition.java index 2b71cd46ab..a955b8aad5 100644 --- a/src/main/java/graphql/language/SDLExtensionDefinition.java +++ b/src/main/java/graphql/language/SDLExtensionDefinition.java @@ -2,11 +2,13 @@ import graphql.PublicApi; +import org.jspecify.annotations.NullMarked; /** * A marker interface for Schema Definition Language (SDL) extension definitions. */ @PublicApi +@NullMarked public interface SDLExtensionDefinition { } diff --git a/src/main/java/graphql/language/SourceLocation.java b/src/main/java/graphql/language/SourceLocation.java index a4ae90a039..b97cc103ac 100644 --- a/src/main/java/graphql/language/SourceLocation.java +++ b/src/main/java/graphql/language/SourceLocation.java @@ -7,24 +7,27 @@ import graphql.schema.GraphQLSchemaElement; import graphql.schema.GraphQLTypeUtil; import graphql.schema.idl.SchemaGenerator; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import java.io.Serializable; import java.util.Objects; @PublicApi +@NullMarked public class SourceLocation implements Serializable { public static final SourceLocation EMPTY = new SourceLocation(-1, -1); private final int line; private final int column; - private final String sourceName; + private final @Nullable String sourceName; public SourceLocation(int line, int column) { this(line, column, null); } - public SourceLocation(int line, int column, String sourceName) { + public SourceLocation(int line, int column, @Nullable String sourceName) { this.line = line; this.column = column; this.sourceName = sourceName; @@ -38,12 +41,12 @@ public int getColumn() { return column; } - public String getSourceName() { + public @Nullable String getSourceName() { return sourceName; } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } @@ -91,7 +94,7 @@ public String toString() { * * @return the source location if available or null if it's not. */ - public static SourceLocation getLocation(GraphQLSchemaElement schemaElement) { + public static @Nullable SourceLocation getLocation(GraphQLSchemaElement schemaElement) { if (schemaElement instanceof GraphQLModifiedType) { schemaElement = GraphQLTypeUtil.unwrapAllAs((GraphQLModifiedType) schemaElement); } diff --git a/src/main/java/graphql/language/Type.java b/src/main/java/graphql/language/Type.java index a3141e56d0..85d06564de 100644 --- a/src/main/java/graphql/language/Type.java +++ b/src/main/java/graphql/language/Type.java @@ -2,8 +2,10 @@ import graphql.PublicApi; +import org.jspecify.annotations.NullMarked; @PublicApi +@NullMarked public interface Type extends Node { } diff --git a/src/main/java/graphql/validation/OperationValidator.java b/src/main/java/graphql/validation/OperationValidator.java index fea8df24aa..1607917b45 100644 --- a/src/main/java/graphql/validation/OperationValidator.java +++ b/src/main/java/graphql/validation/OperationValidator.java @@ -1011,7 +1011,11 @@ private void collectUsedFragmentsInDefinition(Set result, String fragmen // --- OverlappingFieldsCanBeMerged --- private void validateOverlappingFieldsCanBeMerged(OperationDefinition operationDefinition) { - overlappingFieldsImpl(operationDefinition.getSelectionSet(), validationContext.getOutputType()); + SelectionSet selectionSet = operationDefinition.getSelectionSet(); + if (selectionSet == null) { + return; + } + overlappingFieldsImpl(selectionSet, validationContext.getOutputType()); } private void overlappingFieldsImpl(SelectionSet selectionSet, @Nullable GraphQLOutputType graphQLOutputType) { @@ -1557,7 +1561,7 @@ private void validateUniqueOperationNames(OperationDefinition operationDefinitio return; } if (operationNames.contains(name)) { - String message = i18n(DuplicateOperationName, "UniqueOperationNames.oneOperation", operationDefinition.getName()); + String message = i18n(DuplicateOperationName, "UniqueOperationNames.oneOperation", name); addError(DuplicateOperationName, operationDefinition.getSourceLocation(), message); } else { operationNames.add(name); @@ -1646,14 +1650,19 @@ private void validateSubscriptionUniqueRootField(OperationDefinition operationDe .objectType(subscriptionType) .graphQLContext(validationContext.getGraphQLContext()) .build(); - MergedSelectionSet fields = fieldCollector.collectFields(collectorParameters, operationDef.getSelectionSet()); + SelectionSet selectionSet = operationDef.getSelectionSet(); + if (selectionSet == null) { + return; + } + String opName = operationDef.getName() != null ? operationDef.getName() : ""; + MergedSelectionSet fields = fieldCollector.collectFields(collectorParameters, selectionSet); if (fields.size() > 1) { - String message = i18n(SubscriptionMultipleRootFields, "SubscriptionUniqueRootField.multipleRootFields", operationDef.getName()); + String message = i18n(SubscriptionMultipleRootFields, "SubscriptionUniqueRootField.multipleRootFields", opName); addError(SubscriptionMultipleRootFields, operationDef.getSourceLocation(), message); } else { MergedField mergedField = fields.getSubFieldsList().get(0); if (isIntrospectionField(mergedField)) { - String message = i18n(SubscriptionIntrospectionRootField, "SubscriptionIntrospectionRootField.introspectionRootField", operationDef.getName(), mergedField.getName()); + String message = i18n(SubscriptionIntrospectionRootField, "SubscriptionIntrospectionRootField.introspectionRootField", opName, mergedField.getName()); addError(SubscriptionIntrospectionRootField, mergedField.getSingleField().getSourceLocation(), message); } } diff --git a/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy b/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy index 1b6cdafa61..7b08cc153b 100644 --- a/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy +++ b/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy @@ -109,8 +109,6 @@ class JSpecifyAnnotationsCheck extends Specification { "graphql.language.AstSignature", "graphql.language.AstSorter", "graphql.language.AstTransformer", - "graphql.language.Comment", - "graphql.language.Definition", "graphql.language.DescribedNode", "graphql.language.Description", "graphql.language.Directive", @@ -125,8 +123,6 @@ class JSpecifyAnnotationsCheck extends Specification { "graphql.language.FieldDefinition", "graphql.language.FragmentDefinition", "graphql.language.FragmentSpread", - "graphql.language.IgnoredChar", - "graphql.language.IgnoredChars", "graphql.language.ImplementingTypeDefinition", "graphql.language.InlineFragment", "graphql.language.InputObjectTypeDefinition", @@ -135,22 +131,8 @@ class JSpecifyAnnotationsCheck extends Specification { "graphql.language.InterfaceTypeDefinition", "graphql.language.InterfaceTypeExtensionDefinition", "graphql.language.ListType", - "graphql.language.Node", - "graphql.language.NodeChildrenContainer", "graphql.language.NodeDirectivesBuilder", "graphql.language.NodeParentTree", - "graphql.language.NodeTraverser", - "graphql.language.NodeVisitor", - "graphql.language.NodeVisitorStub", - "graphql.language.NonNullType", - "graphql.language.ObjectField", - "graphql.language.ObjectTypeDefinition", - "graphql.language.ObjectTypeExtensionDefinition", - "graphql.language.OperationDefinition", - "graphql.language.OperationTypeDefinition", - "graphql.language.PrettyAstPrinter", - "graphql.language.SDLDefinition", - "graphql.language.SDLExtensionDefinition", "graphql.language.SDLNamedDefinition", "graphql.language.ScalarTypeDefinition", "graphql.language.ScalarTypeExtensionDefinition", @@ -159,8 +141,6 @@ class JSpecifyAnnotationsCheck extends Specification { "graphql.language.Selection", "graphql.language.SelectionSet", "graphql.language.SelectionSetContainer", - "graphql.language.SourceLocation", - "graphql.language.Type", "graphql.language.TypeDefinition", "graphql.language.TypeKind", "graphql.language.TypeName",