diff --git a/build.gradle b/build.gradle index c284e6b896..8a3ee27468 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) { } - diff --git a/src/main/java/graphql/schema/impl/GraphQLTypeCollectingVisitor.java b/src/main/java/graphql/schema/impl/GraphQLTypeCollectingVisitor.java index 0ce7026426..d1ab9d1d45 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); } diff --git a/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy b/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy index a36dfaf924..41d9343da6 100644 --- a/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy +++ b/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy @@ -6,12 +6,19 @@ 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.Specification import static graphql.Scalars.GraphQLBoolean @@ -41,6 +48,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 +198,161 @@ class SchemaUtilTest extends Specification { !(cacheEnabled.getType() instanceof GraphQLTypeReference) } + def "can rebuild schema after removing root type that made an implemented interface reachable"() { + given: + def schema = schemaWithObjectImplementingInterfaceThroughTypeReference() + 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] + } + + def "can transform schema after deleting root type that made an implemented interface reachable"() { + given: + def schema = schemaWithObjectImplementingInterfaceThroughTypeReference() + 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] + } + + def "can rebuild schema after removing root type that made a union member reachable"() { + given: + def schema = schemaWithUnionMemberThroughTypeReference() + 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] + } + + def "can rebuild schema after removing root type that made an interface implemented by another interface reachable"() { + given: + def schema = schemaWithInterfaceImplementingInterfaceThroughTypeReference() + 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 schemaWithObjectImplementingInterfaceThroughTypeReference() { + 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() + } + + private GraphQLSchema schemaWithUnionMemberThroughTypeReference() { + 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 schemaWithInterfaceImplementingInterfaceThroughTypeReference() { + 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")