From c60fdf62c681406488614c60dbca4fba8ed54584 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:32:22 +0000 Subject: [PATCH 1/5] Initial plan From c02fa4e16fa36db1ac47b4de102e1b492635ba25 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:43:06 +0000 Subject: [PATCH 2/5] Add JSpecify annotations to 10 classes in graphql.language package Co-authored-by: dondonz <13839920+dondonz@users.noreply.github.com> --- src/main/java/graphql/language/Comment.java | 9 ++++++--- src/main/java/graphql/language/Definition.java | 2 ++ src/main/java/graphql/language/IgnoredChar.java | 5 ++++- src/main/java/graphql/language/IgnoredChars.java | 2 ++ src/main/java/graphql/language/Node.java | 2 ++ .../graphql/language/NodeChildrenContainer.java | 7 ++++++- src/main/java/graphql/language/NodeVisitor.java | 2 ++ src/main/java/graphql/language/NodeVisitorStub.java | 2 ++ src/main/java/graphql/language/SourceLocation.java | 13 ++++++++----- src/main/java/graphql/language/Type.java | 2 ++ .../archunit/JSpecifyAnnotationsCheck.groovy | 10 ---------- 11 files changed, 36 insertions(+), 20 deletions(-) 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/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/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/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy b/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy index 994c835aab..7116b1641a 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,13 +131,9 @@ 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", @@ -159,8 +151,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", From 3db2ddeae4f403850111cb8c356d0532dd0bbce4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 21:26:08 +0000 Subject: [PATCH 3/5] Update JSpecify annotation prompt with effectiveness improvements Co-authored-by: dondonz <13839920+dondonz@users.noreply.github.com> --- .claude/commands/jspecify-annotate.md | 69 ++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/.claude/commands/jspecify-annotate.md b/.claude/commands/jspecify-annotate.md index 48d385ea1a..f1644de717 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 @@ -14,6 +22,62 @@ Analyze this Java class and add JSpecify annotations based on: IntelliJ's infer nullity code analysis isn't comprehensive so feel free to make corrections. +## 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: @@ -21,7 +85,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. From e7144c6d2c3979b6cb89548c967bf4aefbf99dff Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Mon, 9 Feb 2026 08:57:25 +1100 Subject: [PATCH 4/5] Allow slashes in dev branch names --- build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index fd9a2d0979..79e8eade5e 100644 --- a/build.gradle +++ b/build.gradle @@ -79,8 +79,10 @@ def getDevelopmentVersion() { def gitRevParse = ["git", "-C", projectDir.toString(), "rev-parse", "--abbrev-ref", "HEAD"].execute() gitRevParse.waitForProcessOutput(gitRevParseOutput, gitRevParseError) def branchName = gitRevParseOutput.toString().trim() + // Replace slashes with dashes to avoid path separator issues in version strings + def sanitizedBranchName = branchName.replace('/', '-') - return makeDevelopmentVersion(["0.0.0", branchName, "SNAPSHOT"]) + return makeDevelopmentVersion(["0.0.0", sanitizedBranchName, "SNAPSHOT"]) } def reactiveStreamsVersion = '1.0.3' From 46653203bf90ec261bccadd70bf7205f78ce572f Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sat, 21 Mar 2026 18:04:37 +1100 Subject: [PATCH 5/5] Remove already-annotated classes from JSpecify exemption list NodeVisitor, NodeVisitorStub, SourceLocation, and Type are already annotated with @NullMarked so they should not be in the exemption list. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy b/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy index 6c1d55ba81..c1bf74ddbf 100644 --- a/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy +++ b/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy @@ -123,8 +123,6 @@ class JSpecifyAnnotationsCheck extends Specification { "graphql.language.ListType", "graphql.language.NodeDirectivesBuilder", "graphql.language.NodeParentTree", - "graphql.language.NodeVisitor", - "graphql.language.NodeVisitorStub", "graphql.language.ScalarTypeDefinition", "graphql.language.ScalarTypeExtensionDefinition", "graphql.language.SchemaDefinition", @@ -132,8 +130,6 @@ class JSpecifyAnnotationsCheck extends Specification { "graphql.language.Selection", "graphql.language.SelectionSet", "graphql.language.SelectionSetContainer", - "graphql.language.SourceLocation", - "graphql.language.Type", "graphql.language.TypeKind", "graphql.language.TypeName", "graphql.language.UnionTypeDefinition",