From 7e4209cd9c365888fdc3d404e36ef5771f799e06 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 20 May 2026 15:04:40 +1000 Subject: [PATCH 1/6] Make generated annotation marking idempotent --- build.gradle | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index c284e6b89..8a3ee2746 100644 --- a/build.gradle +++ b/build.gradle @@ -607,11 +607,13 @@ tasks.register('markGeneratedEqualsHashCode') { def jacocoDir = layout.buildDirectory.dir('classes-jacoco/java/main') inputs.dir(originalDir) + inputs.property("generatedAnnotationTaskVersion", 2) outputs.dir(jacocoDir) doLast { def src = originalDir.get().asFile def dest = jacocoDir.get().asFile + project.delete(dest) if (!src.exists()) return // Copy all class files to a separate directory for JaCoCo @@ -635,8 +637,10 @@ tasks.register('markGeneratedEqualsHashCode') { if (method.invisibleAnnotations == null) { method.invisibleAnnotations = [] } - method.invisibleAnnotations.add(new org.objectweb.asm.tree.AnnotationNode(ANNOTATION)) - modified = true + if (!method.invisibleAnnotations.any { it.desc == ANNOTATION }) { + method.invisibleAnnotations.add(new org.objectweb.asm.tree.AnnotationNode(ANNOTATION)) + modified = true + } } } @@ -799,4 +803,3 @@ tasks.withType(GenerateModuleMetadata) { } - From ac6eb4e3931f4b17c0dac42ce350c9a7d07ba002 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 20 May 2026 13:37:56 +1000 Subject: [PATCH 2/6] Add failing schema rebuild repros for issue 3384 --- .../graphql/schema/impl/SchemaUtilTest.groovy | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy b/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy index a36dfaf92..374a15dd2 100644 --- a/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy +++ b/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy @@ -6,12 +6,20 @@ import graphql.NestedInputSchema import graphql.introspection.Introspection import graphql.schema.GraphQLAppliedDirectiveArgument import graphql.schema.GraphQLArgument +import graphql.schema.GraphQLCodeRegistry import graphql.schema.GraphQLFieldDefinition import graphql.schema.GraphQLInputObjectType import graphql.schema.GraphQLObjectType +import graphql.schema.GraphQLSchema +import graphql.schema.GraphQLSchemaElement import graphql.schema.GraphQLType +import graphql.schema.GraphQLTypeVisitorStub import graphql.schema.GraphQLTypeReference import graphql.schema.GraphQLUnionType +import graphql.schema.SchemaTransformer +import graphql.util.TraversalControl +import graphql.util.TraverserContext +import spock.lang.Issue import spock.lang.Specification import static graphql.Scalars.GraphQLBoolean @@ -41,6 +49,7 @@ import static graphql.schema.GraphQLArgument.newArgument import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition import static graphql.schema.GraphQLInputObjectField.newInputObjectField import static graphql.schema.GraphQLInputObjectType.newInputObject +import static graphql.schema.GraphQLInterfaceType.newInterface import static graphql.schema.GraphQLList.list import static graphql.schema.GraphQLObjectType.newObject import static graphql.schema.GraphQLSchema.newSchema @@ -190,6 +199,74 @@ class SchemaUtilTest extends Specification { !(cacheEnabled.getType() instanceof GraphQLTypeReference) } + @Issue("https://github.com/graphql-java/graphql-java/issues/3384") + def "can rebuild schema after removing root type that made an implemented interface reachable"() { + given: + def schema = issue3384Schema() + def node = schema.getType("Node") + + when: + def rebuiltSchema = newSchema(schema) + .mutation((GraphQLObjectType) null) + .build() + + then: + rebuiltSchema.getMutationType() == null + rebuiltSchema.getType("Node") == node + rebuiltSchema.getObjectType("Person").interfaces == [node] + } + + @Issue("https://github.com/graphql-java/graphql-java/issues/3384") + def "can transform schema after deleting root type that made an implemented interface reachable"() { + given: + def schema = issue3384Schema() + def node = schema.getType("Node") + + when: + def transformedSchema = SchemaTransformer.transformSchemaWithDeletes(schema, new GraphQLTypeVisitorStub() { + @Override + TraversalControl visitGraphQLObjectType(GraphQLObjectType graphQLObjectType, TraverserContext context) { + if (graphQLObjectType.name == "Mutation") { + return deleteNode(context) + } + return TraversalControl.CONTINUE + } + }) + + then: + transformedSchema.getMutationType() == null + transformedSchema.getType("Node") == node + transformedSchema.getObjectType("Person").interfaces == [node] + } + + private GraphQLSchema issue3384Schema() { + def node = newInterface() + .name("Node") + .field(newFieldDefinition().name("id").type(GraphQLString)) + .build() + def person = newObject() + .name("Person") + .withInterface(typeRef("Node")) + .field(newFieldDefinition().name("id").type(GraphQLString)) + .build() + def query = newObject() + .name("Query") + .field(newFieldDefinition().name("person").type(person)) + .build() + def mutation = newObject() + .name("Mutation") + .field(newFieldDefinition().name("node").type(node)) + .build() + def codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver(node, { env -> person }) + .build() + return newSchema() + .query(query) + .mutation(mutation) + .codeRegistry(codeRegistry) + .build() + } + def "redefined types are caught"() { when: final GraphQLInputObjectType attributeListInputObjectType = newInputObject().name("attributes") From af13d37fdae72ebf5ebd91a629b7306096181a9a Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 20 May 2026 15:10:46 +1000 Subject: [PATCH 3/6] Fix type collection for resolved schema references --- .../impl/GraphQLTypeCollectingVisitor.java | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/main/java/graphql/schema/impl/GraphQLTypeCollectingVisitor.java b/src/main/java/graphql/schema/impl/GraphQLTypeCollectingVisitor.java index 0ce702642..d1ab9d1d4 100644 --- a/src/main/java/graphql/schema/impl/GraphQLTypeCollectingVisitor.java +++ b/src/main/java/graphql/schema/impl/GraphQLTypeCollectingVisitor.java @@ -27,7 +27,6 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; -import java.util.function.Supplier; import static graphql.schema.GraphQLTypeUtil.unwrapAllAs; import static graphql.util.TraversalControl.CONTINUE; @@ -43,8 +42,9 @@ * themselves are not collected - only concrete type instances are stored in the result map. *

* Because type references are not followed, this visitor also tracks "indirect strong references" - * - types that are directly referenced (not via type reference) by fields, arguments, and input - * fields. This handles edge cases where schema transformations replace type references with + * - types that are directly referenced (not via type reference) by fields, arguments, + * input fields, implemented interfaces, and union members. This handles edge cases where + * schema transformations replace type references with * actual types, which would otherwise be missed during traversal. * * @see SchemaUtil#visitPartiallySchema @@ -77,6 +77,7 @@ public TraversalControl visitGraphQLScalarType(GraphQLScalarType node, Traverser public TraversalControl visitGraphQLObjectType(GraphQLObjectType node, TraverserContext context) { assertTypeUniqueness(node, result); save(node.getName(), node); + saveIndirectStrongReferences(node.getInterfaces()); return CONTINUE; } @@ -91,6 +92,7 @@ public TraversalControl visitGraphQLInputObjectType(GraphQLInputObjectType node, public TraversalControl visitGraphQLInterfaceType(GraphQLInterfaceType node, TraverserContext context) { assertTypeUniqueness(node, result); save(node.getName(), node); + saveIndirectStrongReferences(node.getInterfaces()); return CONTINUE; } @@ -98,40 +100,47 @@ public TraversalControl visitGraphQLInterfaceType(GraphQLInterfaceType node, Tra public TraversalControl visitGraphQLUnionType(GraphQLUnionType node, TraverserContext context) { assertTypeUniqueness(node, result); save(node.getName(), node); + saveIndirectStrongReferences(node.getTypes()); return CONTINUE; } @Override public TraversalControl visitGraphQLFieldDefinition(GraphQLFieldDefinition node, TraverserContext context) { - saveIndirectStrongReference(node::getType); + saveIndirectStrongReference(node.getType()); return CONTINUE; } @Override public TraversalControl visitGraphQLInputObjectField(GraphQLInputObjectField node, TraverserContext context) { - saveIndirectStrongReference(node::getType); + saveIndirectStrongReference(node.getType()); return CONTINUE; } @Override public TraversalControl visitGraphQLArgument(GraphQLArgument node, TraverserContext context) { - saveIndirectStrongReference(node::getType); + saveIndirectStrongReference(node.getType()); return CONTINUE; } @Override public TraversalControl visitGraphQLAppliedDirectiveArgument(GraphQLAppliedDirectiveArgument node, TraverserContext context) { - saveIndirectStrongReference(node::getType); + saveIndirectStrongReference(node.getType()); return CONTINUE; } - private void saveIndirectStrongReference(Supplier typeSupplier) { - GraphQLNamedType type = unwrapAllAs(typeSupplier.get()); + private void saveIndirectStrongReference(GraphQLType graphQLType) { + GraphQLNamedType type = unwrapAllAs(graphQLType); if (!(type instanceof GraphQLTypeReference)) { indirectStrongReferences.put(type.getName(), type); } } + private void saveIndirectStrongReferences(List types) { + for (GraphQLType type : types) { + saveIndirectStrongReference(type); + } + } + private void save(String name, GraphQLNamedType type) { result.put(name, type); } From 78af7584413a0d8861a68e49eec8b1279857b0f8 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 20 May 2026 15:36:50 +1000 Subject: [PATCH 4/6] Add schema rebuild coverage for union and interface references --- .../graphql/schema/impl/SchemaUtilTest.groovy | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy b/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy index 374a15dd2..279305ad2 100644 --- a/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy +++ b/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy @@ -239,6 +239,40 @@ class SchemaUtilTest extends Specification { transformedSchema.getObjectType("Person").interfaces == [node] } + @Issue("https://github.com/graphql-java/graphql-java/issues/3384") + def "can rebuild schema after removing root type that made a union member reachable"() { + given: + def schema = issue3384UnionSchema() + def cat = schema.getObjectType("Cat") + + when: + def rebuiltSchema = newSchema(schema) + .mutation((GraphQLObjectType) null) + .build() + + then: + rebuiltSchema.getMutationType() == null + rebuiltSchema.getType("Cat") == cat + rebuiltSchema.getType("Pet").types == [cat] + } + + @Issue("https://github.com/graphql-java/graphql-java/issues/3384") + def "can rebuild schema after removing root type that made an interface implemented by another interface reachable"() { + given: + def schema = issue3384InterfaceInheritanceSchema() + def node = schema.getType("Node") + + when: + def rebuiltSchema = newSchema(schema) + .mutation((GraphQLObjectType) null) + .build() + + then: + rebuiltSchema.getMutationType() == null + rebuiltSchema.getType("Node") == node + rebuiltSchema.getType("NamedNode").interfaces == [node] + } + private GraphQLSchema issue3384Schema() { def node = newInterface() .name("Node") @@ -267,6 +301,63 @@ class SchemaUtilTest extends Specification { .build() } + private GraphQLSchema issue3384UnionSchema() { + def cat = newObject() + .name("Cat") + .field(newFieldDefinition().name("name").type(GraphQLString)) + .build() + def pet = GraphQLUnionType.newUnionType() + .name("Pet") + .possibleType(typeRef("Cat")) + .build() + def query = newObject() + .name("Query") + .field(newFieldDefinition().name("pet").type(pet)) + .build() + def mutation = newObject() + .name("Mutation") + .field(newFieldDefinition().name("cat").type(cat)) + .build() + def codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver(pet, { env -> cat }) + .build() + return newSchema() + .query(query) + .mutation(mutation) + .codeRegistry(codeRegistry) + .build() + } + + private GraphQLSchema issue3384InterfaceInheritanceSchema() { + def node = newInterface() + .name("Node") + .field(newFieldDefinition().name("id").type(GraphQLString)) + .build() + def namedNode = newInterface() + .name("NamedNode") + .withInterface(typeRef("Node")) + .field(newFieldDefinition().name("id").type(GraphQLString)) + .field(newFieldDefinition().name("name").type(GraphQLString)) + .build() + def query = newObject() + .name("Query") + .field(newFieldDefinition().name("node").type(namedNode)) + .build() + def mutation = newObject() + .name("Mutation") + .field(newFieldDefinition().name("node").type(node)) + .build() + def codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver(node, { env -> null }) + .typeResolver(namedNode, { env -> null }) + .build() + return newSchema() + .query(query) + .mutation(mutation) + .codeRegistry(codeRegistry) + .build() + } + def "redefined types are caught"() { when: final GraphQLInputObjectType attributeListInputObjectType = newInputObject().name("attributes") From a24bacabc86bd88d9207dc78dbfa115a8fb6fb91 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 20 May 2026 15:40:00 +1000 Subject: [PATCH 5/6] Remove issue annotations from schema rebuild tests --- src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy b/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy index 279305ad2..52d0a93cd 100644 --- a/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy +++ b/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy @@ -19,7 +19,6 @@ import graphql.schema.GraphQLUnionType import graphql.schema.SchemaTransformer import graphql.util.TraversalControl import graphql.util.TraverserContext -import spock.lang.Issue import spock.lang.Specification import static graphql.Scalars.GraphQLBoolean @@ -199,7 +198,6 @@ class SchemaUtilTest extends Specification { !(cacheEnabled.getType() instanceof GraphQLTypeReference) } - @Issue("https://github.com/graphql-java/graphql-java/issues/3384") def "can rebuild schema after removing root type that made an implemented interface reachable"() { given: def schema = issue3384Schema() @@ -216,7 +214,6 @@ class SchemaUtilTest extends Specification { rebuiltSchema.getObjectType("Person").interfaces == [node] } - @Issue("https://github.com/graphql-java/graphql-java/issues/3384") def "can transform schema after deleting root type that made an implemented interface reachable"() { given: def schema = issue3384Schema() @@ -239,7 +236,6 @@ class SchemaUtilTest extends Specification { transformedSchema.getObjectType("Person").interfaces == [node] } - @Issue("https://github.com/graphql-java/graphql-java/issues/3384") def "can rebuild schema after removing root type that made a union member reachable"() { given: def schema = issue3384UnionSchema() @@ -256,7 +252,6 @@ class SchemaUtilTest extends Specification { rebuiltSchema.getType("Pet").types == [cat] } - @Issue("https://github.com/graphql-java/graphql-java/issues/3384") def "can rebuild schema after removing root type that made an interface implemented by another interface reachable"() { given: def schema = issue3384InterfaceInheritanceSchema() From d9bf4ecb0e35c5fa9c83b98367131af35e6ed686 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 20 May 2026 15:41:36 +1000 Subject: [PATCH 6/6] Rename schema rebuild test helpers --- .../graphql/schema/impl/SchemaUtilTest.groovy | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy b/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy index 52d0a93cd..41d9343da 100644 --- a/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy +++ b/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy @@ -200,7 +200,7 @@ class SchemaUtilTest extends Specification { def "can rebuild schema after removing root type that made an implemented interface reachable"() { given: - def schema = issue3384Schema() + def schema = schemaWithObjectImplementingInterfaceThroughTypeReference() def node = schema.getType("Node") when: @@ -216,7 +216,7 @@ class SchemaUtilTest extends Specification { def "can transform schema after deleting root type that made an implemented interface reachable"() { given: - def schema = issue3384Schema() + def schema = schemaWithObjectImplementingInterfaceThroughTypeReference() def node = schema.getType("Node") when: @@ -238,7 +238,7 @@ class SchemaUtilTest extends Specification { def "can rebuild schema after removing root type that made a union member reachable"() { given: - def schema = issue3384UnionSchema() + def schema = schemaWithUnionMemberThroughTypeReference() def cat = schema.getObjectType("Cat") when: @@ -254,7 +254,7 @@ class SchemaUtilTest extends Specification { def "can rebuild schema after removing root type that made an interface implemented by another interface reachable"() { given: - def schema = issue3384InterfaceInheritanceSchema() + def schema = schemaWithInterfaceImplementingInterfaceThroughTypeReference() def node = schema.getType("Node") when: @@ -268,7 +268,7 @@ class SchemaUtilTest extends Specification { rebuiltSchema.getType("NamedNode").interfaces == [node] } - private GraphQLSchema issue3384Schema() { + private GraphQLSchema schemaWithObjectImplementingInterfaceThroughTypeReference() { def node = newInterface() .name("Node") .field(newFieldDefinition().name("id").type(GraphQLString)) @@ -296,7 +296,7 @@ class SchemaUtilTest extends Specification { .build() } - private GraphQLSchema issue3384UnionSchema() { + private GraphQLSchema schemaWithUnionMemberThroughTypeReference() { def cat = newObject() .name("Cat") .field(newFieldDefinition().name("name").type(GraphQLString)) @@ -323,7 +323,7 @@ class SchemaUtilTest extends Specification { .build() } - private GraphQLSchema issue3384InterfaceInheritanceSchema() { + private GraphQLSchema schemaWithInterfaceImplementingInterfaceThroughTypeReference() { def node = newInterface() .name("Node") .field(newFieldDefinition().name("id").type(GraphQLString))