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 extends GraphQLType> 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")