Skip to content

Commit 10922b7

Browse files
Raymie Stataclaude
andcommitted
Fix ShallowTypeRefCollector: scan applied directives on args in all contexts
ShallowTypeRefCollector called scanArgumentType(arg) for GraphQLArgument objects in several places but never scanAppliedDirectives(arg.getAppliedDirectives()), and never descended into enum values to scan their applied directives. This left GraphQLTypeReference objects in applied directive argument types unresolved, causing the AppliedDirectiveArgumentsAreValid validator to spuriously fail with "Invalid argument 'x' for applied directive of name 'y'". Three locations fixed: 1. handleObjectType / handleInterfaceType — field argument applied directives were not scanned. Added scanAppliedDirectives(arg.getAppliedDirectives()) inside the field-argument loop of both methods. 2. handleTypeDef — already scanned applied directives on the enum type itself (via the GraphQLDirectiveContainer branch), but never descended into enum values. Added handleEnumType() which scans applied directives on each GraphQLEnumValueDefinition. 3. handleDirective — directive definition argument applied directives were not scanned. Added scanAppliedDirectives(argument.getAppliedDirectives()) inside the argument loop. SchemaUtil.replaceTypeReferences (used by the standard Builder) does a full deep traversal via getChildrenWithTypeReferences() and handles all of these correctly; this change brings ShallowTypeRefCollector in line with that behavior. Adds four regression tests in FastBuilderTest covering: - Applied directive on object type field argument - Applied directive on interface field argument - Applied directive on enum value - Applied directive on directive definition argument Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 32c996c commit 10922b7

3 files changed

Lines changed: 272 additions & 0 deletions

File tree

src/main/java/graphql/schema/ShallowTypeRefCollector.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ public void handleTypeDef(GraphQLNamedType type) {
4545
handleObjectType((GraphQLObjectType) type);
4646
} else if (type instanceof GraphQLInterfaceType) {
4747
handleInterfaceType((GraphQLInterfaceType) type);
48+
} else if (type instanceof GraphQLEnumType) {
49+
handleEnumType((GraphQLEnumType) type);
4850
}
4951
// Scan applied directives on all directive container types
5052
if (type instanceof GraphQLDirectiveContainer) {
@@ -55,6 +57,12 @@ public void handleTypeDef(GraphQLNamedType type) {
5557
}
5658
}
5759

60+
private void handleEnumType(GraphQLEnumType enumType) {
61+
for (GraphQLEnumValueDefinition value : enumType.getValues()) {
62+
scanAppliedDirectives(value.getAppliedDirectives());
63+
}
64+
}
65+
5866
private void handleObjectType(GraphQLObjectType objectType) {
5967
// Scan fields for type references
6068
for (GraphQLFieldDefinition field : objectType.getFieldDefinitions()) {
@@ -64,6 +72,7 @@ private void handleObjectType(GraphQLObjectType objectType) {
6472
// Scan field arguments
6573
for (GraphQLArgument arg : field.getArguments()) {
6674
scanArgumentType(arg);
75+
scanAppliedDirectives(arg.getAppliedDirectives());
6776
}
6877
// Scan applied directives on field
6978
scanAppliedDirectives(field.getAppliedDirectives());
@@ -98,6 +107,7 @@ private void handleInterfaceType(GraphQLInterfaceType interfaceType) {
98107
// Scan field arguments
99108
for (GraphQLArgument arg : field.getArguments()) {
100109
scanArgumentType(arg);
110+
scanAppliedDirectives(arg.getAppliedDirectives());
101111
}
102112
// Scan applied directives on field
103113
scanAppliedDirectives(field.getAppliedDirectives());
@@ -184,6 +194,7 @@ public void scanAppliedDirectives(List<GraphQLAppliedDirective> appliedDirective
184194
public void handleDirective(GraphQLDirective directive) {
185195
for (GraphQLArgument argument : directive.getArguments()) {
186196
scanArgumentType(argument);
197+
scanAppliedDirectives(argument.getAppliedDirectives());
187198
}
188199
}
189200

src/test/groovy/graphql/schema/FastBuilderTest.groovy

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package graphql.schema
33
import graphql.AssertException
44
import graphql.Scalars
55
import graphql.introspection.Introspection
6+
import graphql.language.EnumValue
7+
import graphql.schema.validation.InvalidSchemaException
68
import spock.lang.Specification
79

810
import static graphql.Scalars.GraphQLString
@@ -11,6 +13,7 @@ import static graphql.schema.GraphQLDirective.newDirective
1113
import static graphql.schema.GraphQLAppliedDirective.newDirective as newAppliedDirective
1214
import static graphql.schema.GraphQLAppliedDirectiveArgument.newArgument as newAppliedArgument
1315
import static graphql.schema.GraphQLEnumType.newEnum
16+
import static graphql.schema.GraphQLEnumValueDefinition.newEnumValueDefinition
1417
import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition
1518
import static graphql.schema.GraphQLInputObjectField.newInputObjectField
1619
import static graphql.schema.GraphQLInputObjectType.newInputObject
@@ -2108,4 +2111,261 @@ class FastBuilderTest extends Specification {
21082111
def searchField = schema.queryType.getFieldDefinition("search")
21092112
searchField.getArgument("filter").getType() == filterInput
21102113
}
2114+
2115+
def "applied directive on directive definition argument with type-ref enum arg resolves correctly"() {
2116+
given: "an enum type used as the applied directive's argument type"
2117+
def colorEnum = newEnum()
2118+
.name("Color")
2119+
.value("RED")
2120+
.value("GREEN")
2121+
.build()
2122+
2123+
and: "a helper directive whose arg type is Color — applied to directive definition args"
2124+
def annotateDirective = newDirective()
2125+
.name("annotate")
2126+
.validLocation(Introspection.DirectiveLocation.ARGUMENT_DEFINITION)
2127+
.argument(newArgument()
2128+
.name("color")
2129+
.type(typeRef("Color")))
2130+
.build()
2131+
2132+
and: "an applied @annotate on a directive definition argument, with unresolved type ref"
2133+
def appliedAnnotate = newAppliedDirective()
2134+
.name("annotate")
2135+
.argument(newAppliedArgument()
2136+
.name("color")
2137+
.type(typeRef("Color"))
2138+
.valueLiteral(new EnumValue("GREEN"))
2139+
.build())
2140+
.build()
2141+
2142+
and: "a main directive whose argument carries the applied @annotate"
2143+
def mainDirective = newDirective()
2144+
.name("main")
2145+
.validLocation(Introspection.DirectiveLocation.FIELD_DEFINITION)
2146+
.argument(newArgument()
2147+
.name("arg")
2148+
.type(GraphQLString)
2149+
.withAppliedDirective(appliedAnnotate))
2150+
.build()
2151+
2152+
and: "a simple query type"
2153+
def queryType = newObject()
2154+
.name("Query")
2155+
.field(newFieldDefinition()
2156+
.name("field")
2157+
.type(GraphQLString))
2158+
.build()
2159+
2160+
when: "building with FastBuilder and validation enabled"
2161+
def schema = new GraphQLSchema.FastBuilder(
2162+
GraphQLCodeRegistry.newCodeRegistry(), queryType, null, null)
2163+
.addType(colorEnum)
2164+
.additionalDirective(annotateDirective)
2165+
.additionalDirective(mainDirective)
2166+
.withValidation(true)
2167+
.build()
2168+
2169+
then: "schema builds successfully and the applied directive arg type is resolved"
2170+
schema != null
2171+
def resolvedMain = schema.getDirective("main")
2172+
def mainArg = resolvedMain.getArgument("arg")
2173+
def resolvedApplied = mainArg.getAppliedDirective("annotate")
2174+
resolvedApplied != null
2175+
!(resolvedApplied.getArgument("color").type instanceof GraphQLTypeReference)
2176+
resolvedApplied.getArgument("color").type == colorEnum
2177+
}
2178+
2179+
// Regression tests for ShallowTypeRefCollector bug:
2180+
// Applied directives on field arguments and enum values were not scanned for type
2181+
// references, leaving GraphQLTypeReference unresolved and causing spurious
2182+
// InvalidSchemaException from AppliedDirectiveArgumentsAreValid validator.
2183+
2184+
def "applied directive on object type field argument with type-ref enum arg resolves correctly"() {
2185+
given: "an enum type used as a directive argument type"
2186+
def statusEnum = newEnum()
2187+
.name("Status")
2188+
.value("ACTIVE")
2189+
.value("INACTIVE")
2190+
.build()
2191+
2192+
and: "a directive definition referencing Status"
2193+
def metaDirective = newDirective()
2194+
.name("meta")
2195+
.validLocation(Introspection.DirectiveLocation.ARGUMENT_DEFINITION)
2196+
.argument(newArgument()
2197+
.name("status")
2198+
.type(typeRef("Status")))
2199+
.build()
2200+
2201+
and: "an applied directive on a field argument whose arg type is still a GraphQLTypeReference"
2202+
def appliedMeta = newAppliedDirective()
2203+
.name("meta")
2204+
.argument(newAppliedArgument()
2205+
.name("status")
2206+
.type(typeRef("Status"))
2207+
.valueLiteral(new EnumValue("ACTIVE"))
2208+
.build())
2209+
.build()
2210+
2211+
and: "query type with a field whose argument carries the applied directive"
2212+
def queryType = newObject()
2213+
.name("Query")
2214+
.field(newFieldDefinition()
2215+
.name("field")
2216+
.type(GraphQLString)
2217+
.argument(newArgument()
2218+
.name("arg")
2219+
.type(GraphQLString)
2220+
.withAppliedDirective(appliedMeta)))
2221+
.build()
2222+
2223+
when: "building with FastBuilder and validation enabled"
2224+
def schema = new GraphQLSchema.FastBuilder(
2225+
GraphQLCodeRegistry.newCodeRegistry(), queryType, null, null)
2226+
.addType(statusEnum)
2227+
.additionalDirective(metaDirective)
2228+
.withValidation(true)
2229+
.build()
2230+
2231+
then: "schema builds successfully and the applied directive arg type is resolved"
2232+
schema != null
2233+
def fieldArg = schema.queryType.getFieldDefinition("field").getArgument("arg")
2234+
def resolvedApplied = fieldArg.getAppliedDirective("meta")
2235+
resolvedApplied != null
2236+
!(resolvedApplied.getArgument("status").type instanceof GraphQLTypeReference)
2237+
resolvedApplied.getArgument("status").type == statusEnum
2238+
}
2239+
2240+
def "applied directive on interface field argument with type-ref enum arg resolves correctly"() {
2241+
given: "an enum type used as a directive argument type"
2242+
def statusEnum = newEnum()
2243+
.name("Status")
2244+
.value("ACTIVE")
2245+
.value("INACTIVE")
2246+
.build()
2247+
2248+
and: "a directive definition referencing Status"
2249+
def metaDirective = newDirective()
2250+
.name("meta")
2251+
.validLocation(Introspection.DirectiveLocation.ARGUMENT_DEFINITION)
2252+
.argument(newArgument()
2253+
.name("status")
2254+
.type(typeRef("Status")))
2255+
.build()
2256+
2257+
and: "an applied directive on an interface field argument with unresolved type ref"
2258+
def appliedMeta = newAppliedDirective()
2259+
.name("meta")
2260+
.argument(newAppliedArgument()
2261+
.name("status")
2262+
.type(typeRef("Status"))
2263+
.valueLiteral(new EnumValue("ACTIVE"))
2264+
.build())
2265+
.build()
2266+
2267+
and: "an interface type with a field argument that has the applied directive"
2268+
def nodeInterface = GraphQLInterfaceType.newInterface()
2269+
.name("Node")
2270+
.typeResolver { null }
2271+
.field(newFieldDefinition()
2272+
.name("find")
2273+
.type(GraphQLString)
2274+
.argument(newArgument()
2275+
.name("filter")
2276+
.type(GraphQLString)
2277+
.withAppliedDirective(appliedMeta)))
2278+
.build()
2279+
2280+
and: "a query type referencing the interface"
2281+
def queryType = newObject()
2282+
.name("Query")
2283+
.field(newFieldDefinition()
2284+
.name("node")
2285+
.type(typeRef("Node")))
2286+
.build()
2287+
2288+
when: "building with FastBuilder and validation enabled"
2289+
def schema = new GraphQLSchema.FastBuilder(
2290+
GraphQLCodeRegistry.newCodeRegistry(), queryType, null, null)
2291+
.addType(statusEnum)
2292+
.addType(nodeInterface)
2293+
.additionalDirective(metaDirective)
2294+
.withValidation(true)
2295+
.build()
2296+
2297+
then: "schema builds successfully and the applied directive arg type is resolved"
2298+
schema != null
2299+
def iface = schema.getType("Node") as GraphQLInterfaceType
2300+
def fieldArg = iface.getFieldDefinition("find").getArgument("filter")
2301+
def resolvedApplied = fieldArg.getAppliedDirective("meta")
2302+
resolvedApplied != null
2303+
!(resolvedApplied.getArgument("status").type instanceof GraphQLTypeReference)
2304+
resolvedApplied.getArgument("status").type == statusEnum
2305+
}
2306+
2307+
def "applied directive on enum value with type-ref enum arg resolves correctly"() {
2308+
given: "a Color enum type used as the directive argument type"
2309+
def colorEnum = newEnum()
2310+
.name("Color")
2311+
.value("RED")
2312+
.value("GREEN")
2313+
.build()
2314+
2315+
and: "a directive definition referencing Color"
2316+
def metaDirective = newDirective()
2317+
.name("meta")
2318+
.validLocation(Introspection.DirectiveLocation.ENUM_VALUE)
2319+
.argument(newArgument()
2320+
.name("color")
2321+
.type(typeRef("Color")))
2322+
.build()
2323+
2324+
and: "an applied directive on an enum value with unresolved type ref"
2325+
def appliedMeta = newAppliedDirective()
2326+
.name("meta")
2327+
.argument(newAppliedArgument()
2328+
.name("color")
2329+
.type(typeRef("Color"))
2330+
.valueLiteral(new EnumValue("GREEN"))
2331+
.build())
2332+
.build()
2333+
2334+
and: "an enum type whose values have the applied directive"
2335+
def statusEnum = newEnum()
2336+
.name("Status")
2337+
.value(newEnumValueDefinition()
2338+
.name("ACTIVE")
2339+
.value("ACTIVE")
2340+
.withAppliedDirective(appliedMeta)
2341+
.build())
2342+
.value("INACTIVE")
2343+
.build()
2344+
2345+
and: "a query type"
2346+
def queryType = newObject()
2347+
.name("Query")
2348+
.field(newFieldDefinition()
2349+
.name("field")
2350+
.type(GraphQLString))
2351+
.build()
2352+
2353+
when: "building with FastBuilder and validation enabled"
2354+
def schema = new GraphQLSchema.FastBuilder(
2355+
GraphQLCodeRegistry.newCodeRegistry(), queryType, null, null)
2356+
.addType(statusEnum)
2357+
.addType(colorEnum)
2358+
.additionalDirective(metaDirective)
2359+
.withValidation(true)
2360+
.build()
2361+
2362+
then: "schema builds successfully and the applied directive arg type is resolved"
2363+
schema != null
2364+
def resolvedStatus = schema.getType("Status") as GraphQLEnumType
2365+
def activeValue = resolvedStatus.getValue("ACTIVE")
2366+
def resolvedApplied = activeValue.getAppliedDirective("meta")
2367+
resolvedApplied != null
2368+
!(resolvedApplied.getArgument("color").type instanceof GraphQLTypeReference)
2369+
resolvedApplied.getArgument("color").type == colorEnum
2370+
}
21112371
}

src/test/groovy/graphql/schema/idl/FastSchemaGeneratorTest.groovy

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,4 +329,5 @@ class FastSchemaGeneratorTest extends Specification {
329329
notThrown(InvalidSchemaException)
330330
schema != null
331331
}
332+
332333
}

0 commit comments

Comments
 (0)