Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .claude/commands/jspecify-annotate.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Previous attempt randomly deleted a constructor parameter which caused that PR to go off the rails. Let's fix that here

## 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:

Expand All @@ -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
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/graphql/analysis/QueryTraverser.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Some follow on changes after adding nullability annotations

case MUTATION:
return assertNotNull(schema.getMutationType());
case QUERY:
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/graphql/language/AstPrinter.java
Original file line number Diff line number Diff line change
Expand Up @@ -365,10 +365,11 @@ private NodePrinter<OperationDefinition> 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);
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/graphql/language/NamedNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -12,7 +13,7 @@
public interface NamedNode<T extends NamedNode> extends Node<T> {

/**
* @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();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This has to be nullable because an operation definition might not have a name

}
2 changes: 2 additions & 0 deletions src/main/java/graphql/language/NodeTraverser.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,6 +19,7 @@
* Lets you traverse a {@link Node} tree.
*/
@PublicApi
@NullMarked
public class NodeTraverser {


Expand Down
11 changes: 8 additions & 3 deletions src/main/java/graphql/language/NonNullType.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,14 +21,15 @@
import static graphql.language.NodeChildrenContainer.newNodeChildrenContainer;

@PublicApi
@NullMarked
public class NonNullType extends AbstractNode<NonNullType> implements Type<NonNullType> {

private final Type type;

public static final String CHILD_TYPE = "type";

@Internal
protected NonNullType(Type type, SourceLocation sourceLocation, List<Comment> comments, IgnoredChars ignoredChars, Map<String, String> additionalData) {
protected NonNullType(Type type, @Nullable SourceLocation sourceLocation, List<Comment> comments, IgnoredChars ignoredChars, Map<String, String> additionalData) {
super(sourceLocation, comments, ignoredChars, additionalData);
this.type = type;
}
Expand Down Expand Up @@ -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;
}
Expand All @@ -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
Expand Down Expand Up @@ -106,6 +110,7 @@ public NonNullType transform(Consumer<Builder> builderConsumer) {
return builder.build();
}

@NullUnmarked
public static final class Builder implements NodeBuilder {
private SourceLocation sourceLocation;
private Type type;
Expand Down
11 changes: 8 additions & 3 deletions src/main/java/graphql/language/ObjectField.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
import graphql.collect.ImmutableKit;
import graphql.util.TraversalControl;
import graphql.util.TraverserContext;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;

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

@PublicApi
@NullMarked
public class ObjectField extends AbstractNode<ObjectField> implements NamedNode<ObjectField> {

private final String name;
Expand All @@ -27,7 +31,7 @@ public class ObjectField extends AbstractNode<ObjectField> implements NamedNode<
public static final String CHILD_VALUE = "value";

@Internal
protected ObjectField(String name, Value value, SourceLocation sourceLocation, List<Comment> comments, IgnoredChars ignoredChars, Map<String, String> additionalData) {
protected ObjectField(String name, Value value, @Nullable SourceLocation sourceLocation, List<Comment> comments, IgnoredChars ignoredChars, Map<String, String> additionalData) {
super(sourceLocation, comments, ignoredChars, additionalData);
this.name = assertNotNull(name);
this.value = assertNotNull(value);
Expand Down Expand Up @@ -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;
}
Expand All @@ -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());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I used to think assertion messages matter but they dont. A stacktrace would lead to this code location - so less code can be better code

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This assert is a special one to satisfy the NullAway check

The problem is that deepCopy is a generic utility and I can't guarantee that always returns not-nullable result

}

@Override
Expand All @@ -114,6 +118,7 @@ public ObjectField transform(Consumer<Builder> builderConsumer) {
return builder.build();
}

@NullUnmarked
public static final class Builder implements NodeBuilder {
private SourceLocation sourceLocation;
private String name;
Expand Down
17 changes: 11 additions & 6 deletions src/main/java/graphql/language/ObjectTypeDefinition.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
import graphql.collect.ImmutableKit;
import graphql.util.TraversalControl;
import graphql.util.TraverserContext;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;

import java.util.ArrayList;
import java.util.LinkedHashMap;
Expand All @@ -21,6 +24,7 @@
import static graphql.language.NodeChildrenContainer.newNodeChildrenContainer;

@PublicApi
@NullMarked
public class ObjectTypeDefinition extends AbstractDescribedNode<ObjectTypeDefinition> implements ImplementingTypeDefinition<ObjectTypeDefinition>, DirectivesContainer<ObjectTypeDefinition>, NamedNode<ObjectTypeDefinition> {
private final String name;
private final ImmutableList<Type> implementz;
Expand All @@ -36,8 +40,8 @@ protected ObjectTypeDefinition(String name,
List<Type> implementz,
List<Directive> directives,
List<FieldDefinition> fieldDefinitions,
Description description,
SourceLocation sourceLocation,
@Nullable Description description,
@Nullable SourceLocation sourceLocation,
List<Comment> comments,
IgnoredChars ignoredChars,
Map<String, String> additionalData) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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(),
Expand Down Expand Up @@ -168,6 +172,7 @@ public ObjectTypeDefinition transform(Consumer<Builder> builderConsumer) {
return builder.build();
}

@NullUnmarked
public static final class Builder implements NodeDirectivesBuilder {
private SourceLocation sourceLocation;
private ImmutableList<Comment> comments = emptyList();
Expand Down
15 changes: 10 additions & 5 deletions src/main/java/graphql/language/ObjectTypeExtensionDefinition.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,15 +19,16 @@
import static graphql.collect.ImmutableKit.emptyMap;

@PublicApi
@NullMarked
public class ObjectTypeExtensionDefinition extends ObjectTypeDefinition implements SDLExtensionDefinition {

@Internal
protected ObjectTypeExtensionDefinition(String name,
List<Type> implementz,
List<Directive> directives,
List<FieldDefinition> fieldDefinitions,
Description description,
SourceLocation sourceLocation,
@Nullable Description description,
@Nullable SourceLocation sourceLocation,
List<Comment> comments,
IgnoredChars ignoredChars,
Map<String, String> additionalData) {
Expand All @@ -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(),
Expand Down Expand Up @@ -81,6 +85,7 @@ public ObjectTypeExtensionDefinition transformExtension(Consumer<Builder> builde
return builder.build();
}

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