3030import static graphql .util .TraversalControl .CONTINUE ;
3131import static java .lang .String .format ;
3232
33+ /**
34+ * A visitor that collects all {@link GraphQLNamedType}s during schema traversal.
35+ * <p>
36+ * This visitor must be used with a traverser that calls {@code getChildrenWithTypeReferences()}
37+ * to get children (see {@link SchemaUtil#visitPartiallySchema}). This means that when a field,
38+ * argument, or input field references a type via {@link GraphQLTypeReference}, the traverser
39+ * will see the type reference as a child, not the actual type it points to. Type references
40+ * themselves are not collected - only concrete type instances are stored in the result map.
41+ * <p>
42+ * Because type references are not followed, this visitor also tracks "indirect strong references"
43+ * - types that are directly referenced (not via type reference) by fields, arguments, and input
44+ * fields. This handles edge cases where schema transformations replace type references with
45+ * actual types, which would otherwise be missed during traversal.
46+ *
47+ * @see SchemaUtil#visitPartiallySchema
48+ * @see #fixDanglingReplacedTypes
49+ */
3350@ Internal
3451public class GraphQLTypeCollectingVisitor extends GraphQLTypeVisitorStub {
3552
@@ -55,31 +72,22 @@ public TraversalControl visitGraphQLScalarType(GraphQLScalarType node, Traverser
5572
5673 @ Override
5774 public TraversalControl visitGraphQLObjectType (GraphQLObjectType node , TraverserContext <GraphQLSchemaElement > context ) {
58- if (isNotTypeReference (node .getName ())) {
59- assertTypeUniqueness (node , result );
60- } else {
61- save (node .getName (), node );
62- }
75+ assertTypeUniqueness (node , result );
76+ save (node .getName (), node );
6377 return CONTINUE ;
6478 }
6579
6680 @ Override
6781 public TraversalControl visitGraphQLInputObjectType (GraphQLInputObjectType node , TraverserContext <GraphQLSchemaElement > context ) {
68- if (isNotTypeReference (node .getName ())) {
69- assertTypeUniqueness (node , result );
70- } else {
71- save (node .getName (), node );
72- }
82+ assertTypeUniqueness (node , result );
83+ save (node .getName (), node );
7384 return CONTINUE ;
7485 }
7586
7687 @ Override
7788 public TraversalControl visitGraphQLInterfaceType (GraphQLInterfaceType node , TraverserContext <GraphQLSchemaElement > context ) {
78- if (isNotTypeReference (node .getName ())) {
79- assertTypeUniqueness (node , result );
80- } else {
81- save (node .getName (), node );
82- }
89+ assertTypeUniqueness (node , result );
90+ save (node .getName (), node );
8391 return CONTINUE ;
8492 }
8593
@@ -114,43 +122,25 @@ public TraversalControl visitGraphQLAppliedDirectiveArgument(GraphQLAppliedDirec
114122 return CONTINUE ;
115123 }
116124
117- private < T > void saveIndirectStrongReference (Supplier <GraphQLType > typeSupplier ) {
125+ private void saveIndirectStrongReference (Supplier <GraphQLType > typeSupplier ) {
118126 GraphQLNamedType type = unwrapAllAs (typeSupplier .get ());
119127 if (!(type instanceof GraphQLTypeReference )) {
120128 indirectStrongReferences .put (type .getName (), type );
121129 }
122130 }
123131
124-
125- private boolean isNotTypeReference (String name ) {
126- return result .containsKey (name ) && !(result .get (name ) instanceof GraphQLTypeReference );
127- }
128-
129132 private void save (String name , GraphQLNamedType type ) {
130133 result .put (name , type );
131134 }
132135
133-
134- /*
135- From https://spec.graphql.org/October2021/#sec-Type-System
136-
137- All types within a GraphQL schema must have unique names. No two provided types may have the same name.
138- No provided type may have a name which conflicts with any built in types (including Scalar and Introspection types).
139-
140- Enforcing this helps avoid problems later down the track fo example https://github.com/graphql-java/graphql-java/issues/373
141- */
142136 private void assertTypeUniqueness (GraphQLNamedType type , Map <String , GraphQLNamedType > result ) {
143- GraphQLType existingType = result .get (type .getName ());
144- // do we have an existing definition
137+ GraphQLNamedType existingType = result .get (type .getName ());
145138 if (existingType != null ) {
146- // type references are ok
147- if (!(existingType instanceof GraphQLTypeReference || type instanceof GraphQLTypeReference )) {
148- assertUniqueTypeObjects (type , existingType );
149- }
139+ assertUniqueTypeObjects (type , existingType );
150140 }
151141 }
152142
153- private void assertUniqueTypeObjects (GraphQLNamedType type , GraphQLType existingType ) {
143+ private void assertUniqueTypeObjects (GraphQLNamedType type , GraphQLNamedType existingType ) {
154144 // object comparison here is deliberate
155145 if (existingType != type ) {
156146 throw new AssertException (format ("All types within a GraphQL schema must have unique names. No two provided types may have the same name.\n " +
@@ -166,15 +156,30 @@ public ImmutableMap<String, GraphQLNamedType> getResult() {
166156 }
167157
168158 /**
169- * It's possible for certain schema edits to create a situation where a field / arg / input field had a type reference, then
170- * it got replaced with an actual strong reference and then the schema gets edited such that the only reference
171- * to that type is the replaced strong reference. This edge case means that the replaced reference can be
172- * missed if it's the only way to get to that type because this visitor asks for the children as original type,
173- * e.g. the type reference and not the replaced reference.
159+ * Fixes an edge case where types might be missed during traversal due to replaced type references.
160+ * <p>
161+ * The problem: During schema construction or transformation, a field's type might initially
162+ * be a {@link GraphQLTypeReference} (a placeholder). Later, this reference gets replaced with the
163+ * actual type instance. However, the schema traverser uses {@code getChildrenWithTypeReferences()}
164+ * to discover children, which returns the original type references, not the replaced strong references.
165+ * <p>
166+ * The scenario:
167+ * <ol>
168+ * <li>Field {@code foo} has type reference to {@code Bar}</li>
169+ * <li>During schema editing, the reference is replaced with the actual {@code Bar} type</li>
170+ * <li>Further edits remove all other paths to {@code Bar}</li>
171+ * <li>Now the only way to reach {@code Bar} is via the replaced reference in {@code foo}</li>
172+ * <li>But the traverser still sees the original type reference as the child, so {@code Bar}
173+ * is never visited as a child node</li>
174+ * </ol>
175+ * <p>
176+ * The fix: During traversal, we also capture types directly from fields/arguments/inputs
177+ * (in {@link #indirectStrongReferences}). After traversal, we merge any types that were captured
178+ * this way but weren't found through normal traversal.
174179 *
175- * @param visitedTypes the types collected by this visitor
180+ * @param visitedTypes the types collected through normal traversal
176181 *
177- * @return a fixed up map where the only
182+ * @return the fixed map including any dangling replaced types
178183 */
179184 private Map <String , GraphQLNamedType > fixDanglingReplacedTypes (Map <String , GraphQLNamedType > visitedTypes ) {
180185 for (GraphQLNamedType indirectStrongReference : indirectStrongReferences .values ()) {
0 commit comments