Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Merge origin/master into validation-refactor
Fix NullAway errors from master's @NullMarked additions to
GraphQLSchema and GraphQLTypeUtil. Add null checks in
OperationValidator and TraversalContext for nullable
getCodeRegistry(), unwrapAll(), and type comparison methods.

Update JSpecifyAnnotationsCheck exemption list to match master's
annotated classes, removing deleted DeferDirective rule entries.
Add @NullMarked to ParseAndValidate to match master.
  • Loading branch information
andimarek committed Feb 13, 2026
commit 23dd3dbf5ad257c347c659087d199282bd150f7b
59 changes: 59 additions & 0 deletions .claude/commands/jspecify-annotate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
The task is to annotate public API classes (marked with `@PublicAPI`) with JSpecify nullability annotations.

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.

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
3. Check Javadoc @param tags mentioning "null", "nullable", "may be null"
4. Check Javadoc @return tags mentioning "null", "optional", "if available"
5. Method implementations that return null or check for null
6. GraphQL specification details (see details below)

## GraphQL Specification Compliance
This is a GraphQL implementation. When determining nullability, consult the GraphQL specification (https://spec.graphql.org/draft/) for the relevant concept. Key principles:

The spec defines which elements are required (non-null) vs optional (nullable). Look for keywords like "MUST" to indicate when an element is required, and conditional words such as "IF" to indicate when an element is optional.

If a class implements or represents a GraphQL specification concept, prioritize the spec's nullability requirements over what IntelliJ inferred.

## How to validate
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.

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

## Cleaning up
Finally, can you remove this class from the JSpecifyAnnotationsCheck as an exemption

You do not need to run the JSpecifyAnnotationsCheck. Removing the completed class is enough.

Remember to delete all unused imports when you're done from the class you've just annotated.

## Generics Annotations

When annotating generic types and methods, follow these JSpecify rules:

### Type Parameter Bounds

The bound on a type parameter determines whether nullable type arguments are allowed:

| Declaration | Allows `@Nullable` type argument? |
|-------------|----------------------------------|
| `<T>` | ❌ No — `Box<@Nullable String>` is illegal |
| `<T extends @Nullable Object>` | ✅ Yes — `Box<@Nullable String>` is legal |

**When to use `<T extends @Nullable Object>`:**
- When callers genuinely need to parameterize with nullable types
- Example: `DataFetcherResult<T extends @Nullable Object>` — data fetchers may return nullable types

**When to keep `<T>`:**
- 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<T>` with `@Nullable T getNode()` — node may be null, but T represents the object type

2 changes: 1 addition & 1 deletion .github/workflows/master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: build and test
run: ./gradlew ${{matrix.gradle-argument}} --info --stacktrace
- name: Publish Test Results
uses: EnricoMi/publish-unit-test-result-action@v2.21.0
uses: EnricoMi/publish-unit-test-result-action@v2.22.0
if: always()
with:
files: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
- name: build and test
run: ./gradlew ${{matrix.gradle-argument}} --info --stacktrace
- name: Publish Test Results
uses: EnricoMi/publish-unit-test-result-action@v2.21.0
uses: EnricoMi/publish-unit-test-result-action@v2.22.0
if: always()
with:
files: |
Expand Down
21 changes: 15 additions & 6 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ plugins {
id 'maven-publish'
id 'antlr'
id 'signing'
id "com.gradleup.shadow" version "9.0.0"
id "com.gradleup.shadow" version "9.3.1"
id "biz.aQute.bnd.builder" version "7.1.0"
id "io.github.gradle-nexus.publish-plugin" version "2.0.0"
id "groovy"
id "me.champeau.jmh" version "0.7.3"
id "net.ltgt.errorprone" version '4.3.0'
//
// Kotlin just for tests - not production code
id 'org.jetbrains.kotlin.jvm' version '2.2.21'
id 'org.jetbrains.kotlin.jvm' version '2.3.0'
}

java {
Expand Down Expand Up @@ -129,11 +129,11 @@ dependencies {

testImplementation 'org.junit.jupiter:junit-jupiter:5.14.1'

testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0'
testImplementation 'org.spockframework:spock-core:2.4-groovy-5.0'
testImplementation 'net.bytebuddy:byte-buddy:1.18.1'
testImplementation 'org.objenesis:objenesis:3.4'
testImplementation 'org.apache.groovy:groovy:4.0.28"'
testImplementation 'org.apache.groovy:groovy-json:4.0.28'
testImplementation 'org.objenesis:objenesis:3.5'
testImplementation 'org.apache.groovy:groovy:5.0.4'
testImplementation 'org.apache.groovy:groovy-json:5.0.4'
testImplementation 'com.google.code.gson:gson:2.13.2'
testImplementation 'org.eclipse.jetty:jetty-server:11.0.26'
testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.20.1'
Expand Down Expand Up @@ -223,6 +223,15 @@ tasks.named('jmhJar') {
}
}

jmh {
if (project.hasProperty('jmhInclude')) {
includes = [project.property('jmhInclude')]
}
if (project.hasProperty('jmhProfilers')) {
profilers = [project.property('jmhProfilers')]
}
}


task extractWithoutGuava(type: Copy) {
from({ zipTree({ "build/libs/graphql-java-${project.version}.jar" }) }) {
Expand Down
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
54 changes: 54 additions & 0 deletions src/jmh/java/benchmark/BuildSchemaBenchmark.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package benchmark;

import graphql.schema.idl.FastSchemaGenerator;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;

import java.util.concurrent.TimeUnit;

@Warmup(iterations = 2, time = 5)
@Measurement(iterations = 3)
@Fork(2)
@State(Scope.Benchmark)
public class BuildSchemaBenchmark {

static String largeSDL = BenchmarkUtils.loadResource("large-schema-4.graphqls");

private TypeDefinitionRegistry registry;

@Setup
public void setup() {
// Parse SDL once before benchmarks run
registry = new SchemaParser().parse(largeSDL);
}

@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public void benchmarkBuildSchemaAvgTime(Blackhole blackhole) {
blackhole.consume(new SchemaGenerator().makeExecutableSchema(registry, RuntimeWiring.MOCKED_WIRING));
}

@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public void benchmarkBuildSchemaAvgTimeFast(Blackhole blackhole) {
blackhole.consume(new FastSchemaGenerator().makeExecutableSchema(
SchemaGenerator.Options.defaultOptions().withValidation(false),
registry,
RuntimeWiring.MOCKED_WIRING));
}
}
20 changes: 18 additions & 2 deletions src/jmh/java/benchmark/CreateSchemaBenchmark.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package benchmark;

import graphql.schema.GraphQLSchema;
import graphql.schema.idl.FastSchemaGenerator;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
Expand All @@ -21,7 +22,7 @@
@Fork(2)
public class CreateSchemaBenchmark {

static String largeSDL = BenchmarkUtils.loadResource("large-schema-3.graphqls");
static String largeSDL = BenchmarkUtils.loadResource("large-schema-4.graphqls");

@Benchmark
@BenchmarkMode(Mode.Throughput)
Expand All @@ -37,11 +38,26 @@ public void benchmarkLargeSchemaCreateAvgTime(Blackhole blackhole) {
blackhole.consume(createSchema(largeSDL));
}

@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public void benchmarkLargeSchemaCreateAvgTimeFast(Blackhole blackhole) {
blackhole.consume(createSchemaFast(largeSDL));
}

private static GraphQLSchema createSchema(String sdl) {
TypeDefinitionRegistry registry = new SchemaParser().parse(sdl);
return new SchemaGenerator().makeExecutableSchema(registry, RuntimeWiring.MOCKED_WIRING);
}

private static GraphQLSchema createSchemaFast(String sdl) {
TypeDefinitionRegistry registry = new SchemaParser().parse(sdl);
return new FastSchemaGenerator().makeExecutableSchema(
SchemaGenerator.Options.defaultOptions().withValidation(false),
registry,
RuntimeWiring.MOCKED_WIRING);
}

@SuppressWarnings("InfiniteLoopStatement")
/// make this a main method if you want to run it in JProfiler etc..
public static void mainXXX(String[] args) {
Expand All @@ -54,4 +70,4 @@ public static void mainXXX(String[] args) {
}
}
}
}
}
35 changes: 29 additions & 6 deletions src/main/java/graphql/Assert.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import java.util.Collection;
import java.util.function.Supplier;
import java.util.regex.Pattern;

import static java.lang.String.format;

Expand Down Expand Up @@ -224,8 +223,6 @@ public static void assertFalse(boolean condition, String msgFmt, Object arg1, Ob

private static final String invalidNameErrorMessage = "Name must be non-null, non-empty and match [_A-Za-z][_0-9A-Za-z]* - was '%s'";

private static final Pattern validNamePattern = Pattern.compile("[_A-Za-z][_0-9A-Za-z]*");

/**
* Validates that the Lexical token name matches the current spec.
* currently non null, non empty,
Expand All @@ -234,11 +231,37 @@ public static void assertFalse(boolean condition, String msgFmt, Object arg1, Ob
*
* @return the name if valid, or AssertException if invalid.
*/
public static String assertValidName(String name) {
if (name != null && !name.isEmpty() && validNamePattern.matcher(name).matches()) {
public static String assertValidName(@Nullable String name) {
if (name != null && isValidName(name)) {
return name;
}
return throwAssert(invalidNameErrorMessage, name);
return throwAssert(invalidNameErrorMessage, String.valueOf(name));
}

/**
* Fast character-by-character validation without regex.
* Checks if name matches [_A-Za-z][_0-9A-Za-z]*
*/
private static boolean isValidName(String name) {
if (name.isEmpty()) {
return false;
}

// First character must be [_A-Za-z]
char first = name.charAt(0);
if (!(first == '_' || (first >= 'A' && first <= 'Z') || (first >= 'a' && first <= 'z'))) {
return false;
}

// Remaining characters must be [_0-9A-Za-z]
for (int i = 1; i < name.length(); i++) {
char c = name.charAt(i);
if (!(c == '_' || (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))) {
return false;
}
}

return true;
}

private static <T> T throwAssert(String format, Object... args) {
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/graphql/AssertException.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package graphql;


import org.jspecify.annotations.NullMarked;

@PublicApi
@NullMarked
public class AssertException extends GraphQLException {

public AssertException(String message) {
Expand Down
58 changes: 58 additions & 0 deletions src/main/java/graphql/Directives.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
import graphql.language.DirectiveDefinition;
import graphql.language.StringValue;
import graphql.schema.GraphQLDirective;
import org.jspecify.annotations.NullMarked;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

import static graphql.Scalars.GraphQLBoolean;
Expand Down Expand Up @@ -34,6 +40,7 @@
* The directives that are understood by graphql-java
*/
@PublicApi
@NullMarked
public class Directives {

private static final String DEPRECATED = "deprecated";
Expand Down Expand Up @@ -249,6 +256,57 @@ public class Directives {
.definition(EXPERIMENTAL_DISABLE_ERROR_PROPAGATION_DIRECTIVE_DEFINITION)
.build();

/**
* The set of all built-in directives that are always present in a graphql schema.
* The iteration order is stable and meaningful.
*/
public static final Set<GraphQLDirective> BUILT_IN_DIRECTIVES;

/**
* A map from directive name to directive for all built-in directives.
*/
public static final Map<String, GraphQLDirective> BUILT_IN_DIRECTIVES_MAP;

static {
LinkedHashSet<GraphQLDirective> directives = new LinkedHashSet<>();
directives.add(IncludeDirective);
directives.add(SkipDirective);
directives.add(DeprecatedDirective);
directives.add(SpecifiedByDirective);
directives.add(OneOfDirective);
directives.add(DeferDirective);
directives.add(ExperimentalDisableErrorPropagationDirective);
BUILT_IN_DIRECTIVES = Collections.unmodifiableSet(directives);

LinkedHashMap<String, GraphQLDirective> map = new LinkedHashMap<>();
for (GraphQLDirective d : BUILT_IN_DIRECTIVES) {
map.put(d.getName(), d);
}
BUILT_IN_DIRECTIVES_MAP = Collections.unmodifiableMap(map);
}

/**
* Returns true if a directive with the provided name is a built-in directive.
*
* @param directiveName the name of the directive in question
*
* @return true if the directive is built-in, false otherwise
*/
public static boolean isBuiltInDirective(String directiveName) {
return BUILT_IN_DIRECTIVES_MAP.containsKey(directiveName);
}

/**
* Returns true if the provided directive is a built-in directive.
*
* @param directive the directive in question
*
* @return true if the directive is built-in, false otherwise
*/
public static boolean isBuiltInDirective(GraphQLDirective directive) {
return isBuiltInDirective(directive.getName());
}

private static Description createDescription(String s) {
return new Description(s, null, false);
}
Expand Down
Loading
Loading
You are viewing a condensed version of this merge commit. You can view the full changes here.