From b261a43fc67ee697f4ce95e2bfb9c38aabfea193 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sun, 22 Feb 2026 10:27:04 +1100 Subject: [PATCH 01/10] Add corrections to prompt --- .claude/commands/jspecify-annotate.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.claude/commands/jspecify-annotate.md b/.claude/commands/jspecify-annotate.md index 7083a6b3da..fde80eb6de 100644 --- a/.claude/commands/jspecify-annotate.md +++ b/.claude/commands/jspecify-annotate.md @@ -12,6 +12,13 @@ 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) +## API Compatibility and Breaking Changes + +When adding JSpecify annotations, **DO NOT break existing public APIs**. +- **Do not remove interfaces** from public classes (e.g., if a class implements `NamedNode`, it must continue to do so). +- **Be extremely careful when changing methods to return `@Nullable`**. If an interface contract (or widespread ecosystem usage) expects a non-null return value, changing it to `@Nullable` is a breaking change that will cause compilation errors or `NullPointerException`s for callers. For example, if a method returned `null` implicitly but its interface requires non-null, you must honor the non-null contract (e.g., returning an empty string or default value instead of `null`). +- **Do not change the binary signature** of methods or constructors in a way that breaks backwards compatibility. + ## 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: @@ -26,7 +33,9 @@ If you find NullAway errors, try and make the smallest possible change to fix th ## Formatting Guidelines -Do not make spacing or formatting changes. Avoid adjusting whitespace, line breaks, or other formatting when editing code. These changes make diffs messy and harder to review. Only make the minimal changes necessary to accomplish the task. +- **Zero Formatting Changes**: Do NOT reformat the code. +- **Minimise Whitespace/Newline Changes**: Do not add or remove blank lines unless absolutely necessary. Keep the diff as clean as possible to ease the review process. +- Avoid adjusting whitespace, line breaks, or other formatting when editing code. These changes make diffs messy and harder to review. Only make the minimal changes necessary to accomplish the task. ## Cleaning up Finally, can you remove this class from the JSpecifyAnnotationsCheck as an exemption From 4df42055b40e2adbf32bab071a2ef71151987ecb Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:19:10 +1100 Subject: [PATCH 02/10] Add first lot of changes --- src/main/java/graphql/language/NamedNode.java | 5 +++-- .../java/graphql/language/NodeTraverser.java | 2 ++ src/main/java/graphql/language/NonNullType.java | 11 ++++++++--- src/main/java/graphql/language/ObjectField.java | 11 ++++++++--- .../graphql/language/ObjectTypeDefinition.java | 17 +++++++++++------ .../language/ObjectTypeExtensionDefinition.java | 15 ++++++++++----- .../language/OperationTypeDefinition.java | 11 ++++++++--- .../java/graphql/language/SDLDefinition.java | 2 ++ .../language/SDLExtensionDefinition.java | 2 ++ .../archunit/JSpecifyAnnotationsCheck.groovy | 10 ---------- 10 files changed, 54 insertions(+), 32 deletions(-) diff --git a/src/main/java/graphql/language/NamedNode.java b/src/main/java/graphql/language/NamedNode.java index 4e852dad45..21bfb30a5a 100644 --- a/src/main/java/graphql/language/NamedNode.java +++ b/src/main/java/graphql/language/NamedNode.java @@ -3,6 +3,7 @@ import graphql.PublicApi; import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** * Represents a language node that has a name @@ -12,7 +13,7 @@ public interface NamedNode extends Node { /** - * @return the name of this node + * @return the name of this node, or null if this node is anonymous (e.g. an anonymous operation definition) */ - String getName(); + @Nullable String getName(); } diff --git a/src/main/java/graphql/language/NodeTraverser.java b/src/main/java/graphql/language/NodeTraverser.java index 916c290f95..a0f6ba8063 100644 --- a/src/main/java/graphql/language/NodeTraverser.java +++ b/src/main/java/graphql/language/NodeTraverser.java @@ -7,6 +7,7 @@ import graphql.util.Traverser; import graphql.util.TraverserContext; import graphql.util.TraverserVisitor; +import org.jspecify.annotations.NullMarked; import java.util.Collection; import java.util.Collections; @@ -18,6 +19,7 @@ * Lets you traverse a {@link Node} tree. */ @PublicApi +@NullMarked public class NodeTraverser { diff --git a/src/main/java/graphql/language/NonNullType.java b/src/main/java/graphql/language/NonNullType.java index e86e39c244..da38a1b414 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 deepCopy should not return 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..23b1db02e1 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 deepCopy should not return 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..1f8c37a39a 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 deepCopy should not return null"), + assertNotNull(deepCopy(directives.getDirectives()), "directives deepCopy should not return null"), + assertNotNull(deepCopy(fieldDefinitions), "fieldDefinitions deepCopy should not return 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..8e3477b8d4 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()), "implementz deepCopy should not return null"), + assertNotNull(deepCopy(getDirectives()), "directives deepCopy should not return null"), + assertNotNull(deepCopy(getFieldDefinitions()), "fieldDefinitions deepCopy should not return 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/OperationTypeDefinition.java b/src/main/java/graphql/language/OperationTypeDefinition.java index 5041e692cf..c1186aa751 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 deepCopy should not return 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/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/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy b/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy index 1b6cdafa61..ffdf0d2f78 100644 --- a/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy +++ b/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy @@ -139,18 +139,8 @@ class JSpecifyAnnotationsCheck extends Specification { "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", From 8fb836ef7b7b3775fb0b8fa1b5eeec54f7de56d3 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:30:27 +1100 Subject: [PATCH 03/10] Add operation definition --- .../graphql/language/OperationDefinition.java | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/src/main/java/graphql/language/OperationDefinition.java b/src/main/java/graphql/language/OperationDefinition.java index 824180bb49..909621891d 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,15 +25,16 @@ import static graphql.language.NodeChildrenContainer.newNodeChildrenContainer; @PublicApi +@NullMarked public class OperationDefinition extends AbstractNode implements Definition, SelectionSetContainer, DirectivesContainer, NamedNode { 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; @@ -40,12 +44,12 @@ public enum Operation { 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 SourceLocation sourceLocation, List comments, IgnoredChars ignoredChars, Map additionalData) { @@ -57,15 +61,6 @@ protected OperationDefinition(String name, this.selectionSet = selectionSet; } - public OperationDefinition(String name, - Operation operation) { - this(name, operation, emptyList(), emptyList(), null, null, emptyList(), IgnoredChars.EMPTY, emptyMap()); - } - - public OperationDefinition(String name) { - this(name, null, emptyList(), emptyList(), null, null, emptyList(), IgnoredChars.EMPTY, emptyMap()); - } - @Override public List getChildren() { List result = new ArrayList<>(); @@ -93,11 +88,12 @@ public OperationDefinition withNewChildren(NodeChildrenContainer newChildren) { ); } - public String getName() { + @Override + public @Nullable String getName() { return name; } - public Operation getOperation() { + public @Nullable Operation getOperation() { return operation; } @@ -130,7 +126,7 @@ public SelectionSet getSelectionSet() { } @Override - public boolean isEqualTo(Node o) { + public boolean isEqualTo(@Nullable Node o) { if (this == o) { return true; } @@ -148,9 +144,9 @@ public boolean isEqualTo(Node o) { public OperationDefinition deepCopy() { return new OperationDefinition(name, operation, - deepCopy(variableDefinitions), - deepCopy(directives.getDirectives()), - deepCopy(selectionSet), + assertNotNull(deepCopy(variableDefinitions), "variableDefinitions deepCopy should not return null"), + assertNotNull(deepCopy(directives.getDirectives()), "directives deepCopy should not return null"), + assertNotNull(deepCopy(selectionSet), "selectionSet deepCopy should not return null"), getSourceLocation(), getComments(), getIgnoredChars(), @@ -183,6 +179,7 @@ public OperationDefinition transform(Consumer builderConsumer) { return builder.build(); } + @NullUnmarked public static final class Builder implements NodeDirectivesBuilder { private SourceLocation sourceLocation; private ImmutableList comments = emptyList(); From 298fea2bc3ab74ecbb6f320db8cca718c476c3cc Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:40:27 +1100 Subject: [PATCH 04/10] Add pretty ast printer --- .../graphql/language/PrettyAstPrinter.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/java/graphql/language/PrettyAstPrinter.java b/src/main/java/graphql/language/PrettyAstPrinter.java index a5fc4628b1..5b2bac45ec 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(); } @@ -318,7 +322,7 @@ private String node(Node node) { return node(node, null); } - private String spaced(String... args) { + private String spaced(@Nullable String... args) { return join(" ", args); } @@ -330,7 +334,7 @@ private Function append(String suffix) { return text -> text + suffix; } - private String join(String delim, String... args) { + private String join(String delim, @Nullable String... args) { StringJoiner joiner = new StringJoiner(delim); for (final String arg : args) { @@ -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; } @@ -429,6 +433,7 @@ public enum IndentType { } } + @NullUnmarked public static class Builder { private IndentType indentType; private int indentWidth = 1; From 82712c7264537929db48dd25b614906a72461027 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:46:21 +1100 Subject: [PATCH 05/10] Override name as non null --- src/main/java/graphql/language/SDLNamedDefinition.java | 2 ++ src/main/java/graphql/language/TypeDefinition.java | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/main/java/graphql/language/SDLNamedDefinition.java b/src/main/java/graphql/language/SDLNamedDefinition.java index 44b4bf85a6..c773761424 100644 --- a/src/main/java/graphql/language/SDLNamedDefinition.java +++ b/src/main/java/graphql/language/SDLNamedDefinition.java @@ -2,6 +2,7 @@ import graphql.PublicApi; +import org.jspecify.annotations.NullMarked; /** * A interface for named Schema Definition Language (SDL) definition. @@ -9,6 +10,7 @@ * @param the actual Node type */ @PublicApi +@NullMarked public interface SDLNamedDefinition extends SDLDefinition { /** diff --git a/src/main/java/graphql/language/TypeDefinition.java b/src/main/java/graphql/language/TypeDefinition.java index f75c2c5147..d70fc3ed60 100644 --- a/src/main/java/graphql/language/TypeDefinition.java +++ b/src/main/java/graphql/language/TypeDefinition.java @@ -2,6 +2,7 @@ import graphql.PublicApi; +import org.jspecify.annotations.NullMarked; /** * An interface for type definitions in a Schema Definition Language (SDL). @@ -9,6 +10,9 @@ * @param the actual Node type */ @PublicApi +@NullMarked public interface TypeDefinition extends SDLNamedDefinition, DirectivesContainer, NamedNode { + @Override + String getName(); } From 7a9e810b58c77fc5bd7e255c789a4be9f9241a15 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:55:33 +1100 Subject: [PATCH 06/10] Add operation validator --- src/main/java/graphql/validation/OperationValidator.java | 6 +++--- .../groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/graphql/validation/OperationValidator.java b/src/main/java/graphql/validation/OperationValidator.java index fea8df24aa..49db7c6330 100644 --- a/src/main/java/graphql/validation/OperationValidator.java +++ b/src/main/java/graphql/validation/OperationValidator.java @@ -1557,7 +1557,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); @@ -1648,12 +1648,12 @@ private void validateSubscriptionUniqueRootField(OperationDefinition operationDe .build(); MergedSelectionSet fields = fieldCollector.collectFields(collectorParameters, operationDef.getSelectionSet()); if (fields.size() > 1) { - String message = i18n(SubscriptionMultipleRootFields, "SubscriptionUniqueRootField.multipleRootFields", operationDef.getName()); + String message = i18n(SubscriptionMultipleRootFields, "SubscriptionUniqueRootField.multipleRootFields", Objects.toString(operationDef.getName(), "")); 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", Objects.toString(operationDef.getName(), ""), 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 ffdf0d2f78..b386d23de1 100644 --- a/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy +++ b/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy @@ -141,7 +141,6 @@ class JSpecifyAnnotationsCheck extends Specification { "graphql.language.NodeParentTree", "graphql.language.NodeVisitor", "graphql.language.NodeVisitorStub", - "graphql.language.SDLNamedDefinition", "graphql.language.ScalarTypeDefinition", "graphql.language.ScalarTypeExtensionDefinition", "graphql.language.SchemaDefinition", @@ -151,7 +150,6 @@ class JSpecifyAnnotationsCheck extends Specification { "graphql.language.SelectionSetContainer", "graphql.language.SourceLocation", "graphql.language.Type", - "graphql.language.TypeDefinition", "graphql.language.TypeKind", "graphql.language.TypeName", "graphql.language.UnionTypeDefinition", From dd4929636b20be373a366f8c4cec56d2541b093a Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:38:01 +1100 Subject: [PATCH 07/10] Adjust AST Printer --- src/main/java/graphql/language/AstPrinter.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/graphql/language/AstPrinter.java b/src/main/java/graphql/language/AstPrinter.java index a5e1535ea5..d11466868a 100644 --- a/src/main/java/graphql/language/AstPrinter.java +++ b/src/main/java/graphql/language/AstPrinter.java @@ -365,10 +365,11 @@ private NodePrinter operationDefinition() { // Anonymous queries with no directives or variable definitions can use // the query short form. if (isEmpty(name) && isEmpty(node.getDirectives()) && isEmpty(node.getVariableDefinitions()) - && node.getOperation() == OperationDefinition.Operation.QUERY) { + && (node.getOperation() == null || node.getOperation() == OperationDefinition.Operation.QUERY)) { node(out, node.getSelectionSet()); } else { - out.append(node.getOperation().toString().toLowerCase()); + OperationDefinition.Operation op = node.getOperation(); + out.append(op != null ? op.toString().toLowerCase() : "query"); if (!isEmpty(name)) { out.append(' '); out.append(name); From 9e31132dd83e4df867c26cf0fc9828c54ef17895 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:40:22 +1100 Subject: [PATCH 08/10] Account for a nullable op name --- src/main/java/graphql/analysis/QueryTraverser.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/graphql/analysis/QueryTraverser.java b/src/main/java/graphql/analysis/QueryTraverser.java index 4825dbf746..7e4745e1be 100644 --- a/src/main/java/graphql/analysis/QueryTraverser.java +++ b/src/main/java/graphql/analysis/QueryTraverser.java @@ -166,7 +166,10 @@ public void visitField(QueryVisitorFieldEnvironment env) { } private GraphQLObjectType getRootTypeFromOperation(OperationDefinition operationDefinition) { - switch (operationDefinition.getOperation()) { + OperationDefinition.Operation op = operationDefinition.getOperation() != null + ? operationDefinition.getOperation() + : OperationDefinition.Operation.QUERY; + switch (op) { case MUTATION: return assertNotNull(schema.getMutationType()); case QUERY: From 1063251b709d0e6c054ccc47d773ea6a16fe0005 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:42:25 +1100 Subject: [PATCH 09/10] Fix test for non-null selection set --- .../graphql/schema/DataFetchingEnvironmentImplTest.groovy | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/test/groovy/graphql/schema/DataFetchingEnvironmentImplTest.groovy b/src/test/groovy/graphql/schema/DataFetchingEnvironmentImplTest.groovy index 561b881bd2..4797aafed0 100644 --- a/src/test/groovy/graphql/schema/DataFetchingEnvironmentImplTest.groovy +++ b/src/test/groovy/graphql/schema/DataFetchingEnvironmentImplTest.groovy @@ -9,6 +9,7 @@ import graphql.language.Argument import graphql.language.Field import graphql.language.FragmentDefinition import graphql.language.OperationDefinition +import graphql.language.SelectionSet import graphql.language.StringValue import graphql.language.TypeName import org.dataloader.BatchLoader @@ -29,7 +30,10 @@ class DataFetchingEnvironmentImplTest extends Specification { def frag = FragmentDefinition.newFragmentDefinition().name("frag").typeCondition(new TypeName("t")).build() def dataLoader = DataLoaderFactory.newDataLoader({ keys -> CompletableFuture.completedFuture(keys) } as BatchLoader) - def operationDefinition = new OperationDefinition("q") + def operationDefinition = OperationDefinition.newOperationDefinition() + .name("q") + .selectionSet(SelectionSet.newSelectionSet().selection(new Field("f")).build()) + .build() def document = toDocument("{ f }") def executionId = ExecutionId.from("123") def fragmentByName = [frag: frag] From f4e0f1bb6317014ca12472932960feebe9713327 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sun, 8 Mar 2026 11:41:51 +1100 Subject: [PATCH 10/10] Make OperationDefinition.operation non-null, default to QUERY MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the GraphQL spec and graphql-js reference implementation, the operation type is always known — shorthand queries are QUERY. Default the Builder to Operation.QUERY and remove null checks in callers. Co-Authored-By: Claude Opus 4.6 --- src/main/java/graphql/analysis/QueryTraverser.java | 5 +---- src/main/java/graphql/language/AstPrinter.java | 4 ++-- src/main/java/graphql/language/AstSorter.java | 2 +- src/main/java/graphql/language/OperationDefinition.java | 8 ++++---- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/main/java/graphql/analysis/QueryTraverser.java b/src/main/java/graphql/analysis/QueryTraverser.java index 7e4745e1be..4825dbf746 100644 --- a/src/main/java/graphql/analysis/QueryTraverser.java +++ b/src/main/java/graphql/analysis/QueryTraverser.java @@ -166,10 +166,7 @@ public void visitField(QueryVisitorFieldEnvironment env) { } private GraphQLObjectType getRootTypeFromOperation(OperationDefinition operationDefinition) { - OperationDefinition.Operation op = operationDefinition.getOperation() != null - ? operationDefinition.getOperation() - : OperationDefinition.Operation.QUERY; - switch (op) { + switch (operationDefinition.getOperation()) { case MUTATION: return assertNotNull(schema.getMutationType()); case QUERY: diff --git a/src/main/java/graphql/language/AstPrinter.java b/src/main/java/graphql/language/AstPrinter.java index d11466868a..e6bd85ee84 100644 --- a/src/main/java/graphql/language/AstPrinter.java +++ b/src/main/java/graphql/language/AstPrinter.java @@ -365,11 +365,11 @@ private NodePrinter operationDefinition() { // Anonymous queries with no directives or variable definitions can use // the query short form. if (isEmpty(name) && isEmpty(node.getDirectives()) && isEmpty(node.getVariableDefinitions()) - && (node.getOperation() == null || node.getOperation() == OperationDefinition.Operation.QUERY)) { + && node.getOperation() == OperationDefinition.Operation.QUERY) { node(out, node.getSelectionSet()); } else { OperationDefinition.Operation op = node.getOperation(); - out.append(op != null ? op.toString().toLowerCase() : "query"); + out.append(op.toString().toLowerCase()); if (!isEmpty(name)) { out.append(' '); out.append(name); diff --git a/src/main/java/graphql/language/AstSorter.java b/src/main/java/graphql/language/AstSorter.java index d24969ea94..a76f089a43 100644 --- a/src/main/java/graphql/language/AstSorter.java +++ b/src/main/java/graphql/language/AstSorter.java @@ -281,7 +281,7 @@ private Comparator comparingDefinitions() { Function byType = d -> { if (d instanceof OperationDefinition) { OperationDefinition.Operation operation = ((OperationDefinition) d).getOperation(); - if (OperationDefinition.Operation.QUERY == operation || operation == null) { + if (OperationDefinition.Operation.QUERY == operation) { return 101; } if (OperationDefinition.Operation.MUTATION == operation) { diff --git a/src/main/java/graphql/language/OperationDefinition.java b/src/main/java/graphql/language/OperationDefinition.java index 909621891d..845dd4e146 100644 --- a/src/main/java/graphql/language/OperationDefinition.java +++ b/src/main/java/graphql/language/OperationDefinition.java @@ -34,7 +34,7 @@ public enum Operation { private final @Nullable String name; - private final @Nullable Operation operation; + private final Operation operation; private final ImmutableList variableDefinitions; private final DirectivesHolder directives; private final SelectionSet selectionSet; @@ -45,7 +45,7 @@ public enum Operation { @Internal protected OperationDefinition(@Nullable String name, - @Nullable Operation operation, + Operation operation, List variableDefinitions, List directives, SelectionSet selectionSet, @@ -93,7 +93,7 @@ public OperationDefinition withNewChildren(NodeChildrenContainer newChildren) { return name; } - public @Nullable Operation getOperation() { + public Operation getOperation() { return operation; } @@ -184,7 +184,7 @@ public static final class Builder implements NodeDirectivesBuilder { private SourceLocation sourceLocation; private ImmutableList comments = emptyList(); private String name; - private Operation operation; + private Operation operation = Operation.QUERY; private ImmutableList variableDefinitions = emptyList(); private ImmutableList directives = emptyList(); private SelectionSet selectionSet;