Skip to content

Commit 36b4972

Browse files
committed
unique_directives_per_location: check in extensions
Replicates graphql/graphql-js@4aa2d0a
1 parent 2208c8e commit 36b4972

File tree

2 files changed

+132
-36
lines changed

2 files changed

+132
-36
lines changed

src/graphql/validation/rules/unique_directives_per_location.py

+43-16
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
1+
from collections import defaultdict
12
from typing import Dict, List, Union, cast
23

34
from ...error import GraphQLError
4-
from ...language import DirectiveDefinitionNode, DirectiveNode, Node
5+
from ...language import (
6+
DirectiveDefinitionNode,
7+
DirectiveNode,
8+
Node,
9+
SchemaDefinitionNode,
10+
SchemaExtensionNode,
11+
TypeDefinitionNode,
12+
TypeExtensionNode,
13+
is_type_definition_node,
14+
is_type_extension_node,
15+
)
516
from ...type import specified_directives
617
from . import ASTValidationRule, SDLValidationContext, ValidationContext
718

@@ -27,29 +38,45 @@ def __init__(self, context: Union[ValidationContext, SDLValidationContext]):
2738
)
2839
for directive in defined_directives:
2940
unique_directive_map[directive.name] = not directive.is_repeatable
41+
3042
ast_definitions = context.document.definitions
3143
for def_ in ast_definitions:
3244
if isinstance(def_, DirectiveDefinitionNode):
3345
unique_directive_map[def_.name.value] = not def_.repeatable
3446
self.unique_directive_map = unique_directive_map
3547

48+
self.schema_directives: Dict[str, DirectiveNode] = {}
49+
self.type_directives_map: Dict[str, Dict[str, DirectiveNode]] = defaultdict(
50+
dict
51+
)
52+
3653
# Many different AST nodes may contain directives. Rather than listing them all,
3754
# just listen for entering any node, and check to see if it defines any directives.
3855
def enter(self, node: Node, *_args):
3956
directives: List[DirectiveNode] = getattr(node, "directives", None)
40-
if directives:
41-
known_directives: Dict[str, DirectiveNode] = {}
42-
for directive in directives:
43-
directive_name = directive.name.value
44-
45-
if self.unique_directive_map.get(directive_name):
46-
if directive_name in known_directives:
47-
self.report_error(
48-
GraphQLError(
49-
f"The directive '@{directive_name}'"
50-
" can only be used once at this location.",
51-
[known_directives[directive_name], directive],
52-
)
57+
if not directives:
58+
return
59+
60+
if isinstance(node, (SchemaDefinitionNode, SchemaExtensionNode)):
61+
seen_directives = self.schema_directives
62+
elif is_type_definition_node(node) or is_type_extension_node(node):
63+
node = cast(Union[TypeDefinitionNode, TypeExtensionNode], node)
64+
type_name = node.name.value
65+
seen_directives = self.type_directives_map[type_name]
66+
else:
67+
seen_directives = {}
68+
69+
for directive in directives:
70+
directive_name = directive.name.value
71+
72+
if self.unique_directive_map.get(directive_name):
73+
if directive_name in seen_directives:
74+
self.report_error(
75+
GraphQLError(
76+
f"The directive '@{directive_name}'"
77+
" can only be used once at this location.",
78+
[seen_directives[directive_name], directive],
5379
)
54-
else:
55-
known_directives[directive_name] = directive
80+
)
81+
else:
82+
seen_directives[directive_name] = directive

tests/validation/test_unique_directives_per_location.py

+89-20
Original file line numberDiff line numberDiff line change
@@ -182,22 +182,12 @@ def duplicate_directives_on_sdl_definitions():
182182
SCHEMA | SCALAR | OBJECT | INTERFACE | UNION | INPUT_OBJECT
183183
184184
schema @nonRepeatable @nonRepeatable { query: Dummy }
185-
extend schema @nonRepeatable @nonRepeatable
186185
187186
scalar TestScalar @nonRepeatable @nonRepeatable
188-
extend scalar TestScalar @nonRepeatable @nonRepeatable
189-
190187
type TestObject @nonRepeatable @nonRepeatable
191-
extend type TestObject @nonRepeatable @nonRepeatable
192-
193188
interface TestInterface @nonRepeatable @nonRepeatable
194-
extend interface TestInterface @nonRepeatable @nonRepeatable
195-
196189
union TestUnion @nonRepeatable @nonRepeatable
197-
extend union TestUnion @nonRepeatable @nonRepeatable
198-
199190
input TestInput @nonRepeatable @nonRepeatable
200-
extend input TestInput @nonRepeatable @nonRepeatable
201191
""",
202192
[
203193
{
@@ -208,57 +198,136 @@ def duplicate_directives_on_sdl_definitions():
208198
{
209199
"message": "The directive '@nonRepeatable'"
210200
" can only be used once at this location.",
211-
"locations": [(6, 27), (6, 42)],
201+
"locations": [(7, 31), (7, 46)],
202+
},
203+
{
204+
"message": "The directive '@nonRepeatable'"
205+
" can only be used once at this location.",
206+
"locations": [(8, 29), (8, 44)],
212207
},
213208
{
214209
"message": "The directive '@nonRepeatable'"
215210
" can only be used once at this location.",
216-
"locations": [(8, 31), (8, 46)],
211+
"locations": [(9, 37), (9, 52)],
217212
},
218213
{
219214
"message": "The directive '@nonRepeatable'"
220215
" can only be used once at this location.",
221-
"locations": [(9, 38), (9, 53)],
216+
"locations": [(10, 29), (10, 44)],
222217
},
223218
{
224219
"message": "The directive '@nonRepeatable'"
225220
" can only be used once at this location.",
226221
"locations": [(11, 29), (11, 44)],
227222
},
223+
],
224+
)
225+
226+
def duplicate_directives_on_sdl_extensions():
227+
assert_sdl_errors(
228+
"""
229+
directive @nonRepeatable on
230+
SCHEMA | SCALAR | OBJECT | INTERFACE | UNION | INPUT_OBJECT
231+
232+
extend schema @nonRepeatable @nonRepeatable
233+
234+
extend scalar TestScalar @nonRepeatable @nonRepeatable
235+
extend type TestObject @nonRepeatable @nonRepeatable
236+
extend interface TestInterface @nonRepeatable @nonRepeatable
237+
extend union TestUnion @nonRepeatable @nonRepeatable
238+
extend input TestInput @nonRepeatable @nonRepeatable
239+
""",
240+
[
241+
{
242+
"message": "The directive '@nonRepeatable'"
243+
" can only be used once at this location.",
244+
"locations": [(5, 27), (5, 42)],
245+
},
228246
{
229247
"message": "The directive '@nonRepeatable'"
230248
" can only be used once at this location.",
231-
"locations": [(12, 36), (12, 51)],
249+
"locations": [(7, 38), (7, 53)],
232250
},
233251
{
234252
"message": "The directive '@nonRepeatable'"
235253
" can only be used once at this location.",
236-
"locations": [(14, 37), (14, 52)],
254+
"locations": [(8, 36), (8, 51)],
237255
},
238256
{
239257
"message": "The directive '@nonRepeatable'"
240258
" can only be used once at this location.",
241-
"locations": [(15, 44), (15, 59)],
259+
"locations": [(9, 44), (9, 59)],
242260
},
243261
{
244262
"message": "The directive '@nonRepeatable'"
245263
" can only be used once at this location.",
246-
"locations": [(17, 29), (17, 44)],
264+
"locations": [(10, 36), (10, 51)],
247265
},
248266
{
249267
"message": "The directive '@nonRepeatable'"
250268
" can only be used once at this location.",
251-
"locations": [(18, 36), (18, 51)],
269+
"locations": [(11, 36), (11, 51)],
252270
},
271+
],
272+
)
273+
274+
def duplicate_directives_between_sdl_definitions_and_extensions():
275+
assert_sdl_errors(
276+
"""
277+
directive @nonRepeatable on SCHEMA
278+
279+
schema @nonRepeatable { query: Dummy }
280+
extend schema @nonRepeatable
281+
""",
282+
[
283+
{
284+
"message": "The directive '@nonRepeatable'"
285+
" can only be used once at this location.",
286+
"locations": [(4, 20), (5, 27)],
287+
},
288+
],
289+
)
290+
291+
assert_sdl_errors(
292+
"""
293+
directive @nonRepeatable on SCALAR
294+
295+
scalar TestScalar @nonRepeatable
296+
extend scalar TestScalar @nonRepeatable
297+
scalar TestScalar @nonRepeatable
298+
""",
299+
[
300+
{
301+
"message": "The directive '@nonRepeatable'"
302+
" can only be used once at this location.",
303+
"locations": [(4, 31), (5, 38)],
304+
},
305+
{
306+
"message": "The directive '@nonRepeatable'"
307+
" can only be used once at this location.",
308+
"locations": [(4, 31), (6, 31)],
309+
},
310+
],
311+
)
312+
313+
assert_sdl_errors(
314+
"""
315+
directive @nonRepeatable on OBJECT
316+
317+
extend type TestObject @nonRepeatable
318+
type TestObject @nonRepeatable
319+
extend type TestObject @nonRepeatable
320+
""",
321+
[
253322
{
254323
"message": "The directive '@nonRepeatable'"
255324
" can only be used once at this location.",
256-
"locations": [(20, 29), (20, 44)],
325+
"locations": [(4, 36), (5, 29)],
257326
},
258327
{
259328
"message": "The directive '@nonRepeatable'"
260329
" can only be used once at this location.",
261-
"locations": [(21, 36), (21, 51)],
330+
"locations": [(4, 36), (6, 36)],
262331
},
263332
],
264333
)

0 commit comments

Comments
 (0)