Skip to content

Commit 5b72d47

Browse files
committed
Add 'UniqueArgumentDefinitionNamesRule' validation rule
Replicates graphql/graphql-js@2170e4a
1 parent e00c129 commit 5b72d47

File tree

6 files changed

+268
-4
lines changed

6 files changed

+268
-4
lines changed

docs/modules/validation.rst

+1
Original file line numberDiff line numberDiff line change
@@ -143,5 +143,6 @@ Rules
143143
.. autoclass:: UniqueTypeNamesRule
144144
.. autoclass:: UniqueEnumValueNamesRule
145145
.. autoclass:: UniqueFieldDefinitionNamesRule
146+
.. autoclass:: UniqueArgumentDefinitionNamesRule
146147
.. autoclass:: UniqueDirectiveNamesRule
147148
.. autoclass:: PossibleTypeExtensionsRule

src/graphql/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@
336336
UniqueTypeNamesRule,
337337
UniqueEnumValueNamesRule,
338338
UniqueFieldDefinitionNamesRule,
339+
UniqueArgumentDefinitionNamesRule,
339340
UniqueDirectiveNamesRule,
340341
PossibleTypeExtensionsRule,
341342
# Custom validation rules
@@ -688,6 +689,7 @@
688689
"UniqueTypeNamesRule",
689690
"UniqueEnumValueNamesRule",
690691
"UniqueFieldDefinitionNamesRule",
692+
"UniqueArgumentDefinitionNamesRule",
691693
"UniqueDirectiveNamesRule",
692694
"PossibleTypeExtensionsRule",
693695
"NoDeprecatedCustomRule",

src/graphql/validation/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
from .rules.unique_type_names import UniqueTypeNamesRule
102102
from .rules.unique_enum_value_names import UniqueEnumValueNamesRule
103103
from .rules.unique_field_definition_names import UniqueFieldDefinitionNamesRule
104+
from .rules.unique_argument_definition_names import UniqueArgumentDefinitionNamesRule
104105
from .rules.unique_directive_names import UniqueDirectiveNamesRule
105106
from .rules.possible_type_extensions import PossibleTypeExtensionsRule
106107

@@ -148,6 +149,7 @@
148149
"UniqueTypeNamesRule",
149150
"UniqueEnumValueNamesRule",
150151
"UniqueFieldDefinitionNamesRule",
152+
"UniqueArgumentDefinitionNamesRule",
151153
"UniqueDirectiveNamesRule",
152154
"PossibleTypeExtensionsRule",
153155
"NoDeprecatedCustomRule",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from collections import defaultdict
2+
from operator import attrgetter
3+
from typing import Any, Callable, Collection, Dict, List, TypeVar
4+
5+
from ...error import GraphQLError
6+
from ...language import (
7+
DirectiveDefinitionNode,
8+
FieldDefinitionNode,
9+
InputValueDefinitionNode,
10+
InterfaceTypeDefinitionNode,
11+
InterfaceTypeExtensionNode,
12+
NameNode,
13+
ObjectTypeDefinitionNode,
14+
ObjectTypeExtensionNode,
15+
VisitorAction,
16+
SKIP,
17+
)
18+
from . import SDLValidationRule
19+
20+
__all__ = ["UniqueArgumentDefinitionNamesRule"]
21+
22+
23+
class UniqueArgumentDefinitionNamesRule(SDLValidationRule):
24+
"""Unique argument definition names
25+
26+
A GraphQL Object or Interface type is only valid if all its fields have uniquely
27+
named arguments.
28+
A GraphQL Directive is only valid if all its arguments are uniquely named.
29+
30+
See https://spec.graphql.org/draft/#sec-Argument-Uniqueness
31+
"""
32+
33+
def enter_directive_definition(
34+
self, node: DirectiveDefinitionNode, *_args: Any
35+
) -> VisitorAction:
36+
return self.check_arg_uniqueness(f"@{node.name.value}", node.arguments)
37+
38+
def enter_interface_type_definition(
39+
self, node: InterfaceTypeDefinitionNode, *_args: Any
40+
) -> VisitorAction:
41+
return self.check_arg_uniqueness_per_field(node.name, node.fields)
42+
43+
def enter_interface_type_extension(
44+
self, node: InterfaceTypeExtensionNode, *_args: Any
45+
) -> VisitorAction:
46+
return self.check_arg_uniqueness_per_field(node.name, node.fields)
47+
48+
def enter_object_type_definition(
49+
self, node: ObjectTypeDefinitionNode, *_args: Any
50+
) -> VisitorAction:
51+
return self.check_arg_uniqueness_per_field(node.name, node.fields)
52+
53+
def enter_object_type_extension(
54+
self, node: ObjectTypeExtensionNode, *_args: Any
55+
) -> VisitorAction:
56+
return self.check_arg_uniqueness_per_field(node.name, node.fields)
57+
58+
def check_arg_uniqueness_per_field(
59+
self,
60+
name: NameNode,
61+
fields: Collection[FieldDefinitionNode],
62+
) -> VisitorAction:
63+
type_name = name.value
64+
for field_def in fields:
65+
field_name = field_def.name.value
66+
argument_nodes = field_def.arguments or ()
67+
self.check_arg_uniqueness(f"{type_name}.{field_name}", argument_nodes)
68+
return SKIP
69+
70+
def check_arg_uniqueness(
71+
self, parent_name: str, argument_nodes: Collection[InputValueDefinitionNode]
72+
) -> VisitorAction:
73+
seen_args = group_by(argument_nodes, attrgetter("name.value"))
74+
for arg_name, arg_nodes in seen_args.items():
75+
if len(arg_nodes) > 1:
76+
self.report_error(
77+
GraphQLError(
78+
f"Argument '{parent_name}({arg_name}:)'"
79+
" can only be defined once.",
80+
[node.name for node in arg_nodes],
81+
)
82+
)
83+
return SKIP
84+
85+
86+
K = TypeVar("K")
87+
T = TypeVar("T")
88+
89+
90+
def group_by(items: Collection[T], key_fn: Callable[[T], K]) -> Dict[K, List[T]]:
91+
"""Group an unsorted collection of items by a key derived via a function."""
92+
result: Dict[K, List[T]] = defaultdict(list)
93+
for item in items:
94+
key = key_fn(item)
95+
result[key].append(item)
96+
return result

src/graphql/validation/specified_rules.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
from .rules.unique_type_names import UniqueTypeNamesRule
9191
from .rules.unique_enum_value_names import UniqueEnumValueNamesRule
9292
from .rules.unique_field_definition_names import UniqueFieldDefinitionNamesRule
93+
from .rules.unique_argument_definition_names import UniqueArgumentDefinitionNamesRule
9394
from .rules.unique_directive_names import UniqueDirectiveNamesRule
9495
from .rules.possible_type_extensions import PossibleTypeExtensionsRule
9596

@@ -102,7 +103,7 @@
102103
# most clear output when encountering multiple validation errors.
103104

104105
specified_rules: FrozenList[Type[ASTValidationRule]] = FrozenList(
105-
[
106+
(
106107
ExecutableDefinitionsRule,
107108
UniqueOperationNamesRule,
108109
LoneAnonymousOperationRule,
@@ -129,7 +130,7 @@
129130
VariablesInAllowedPositionRule,
130131
OverlappingFieldsCanBeMergedRule,
131132
UniqueInputFieldNamesRule,
132-
]
133+
)
133134
)
134135
specified_rules.__doc__ = """\
135136
This list includes all validation rules defined by the GraphQL spec.
@@ -139,12 +140,13 @@
139140
"""
140141

141142
specified_sdl_rules: FrozenList[Type[ASTValidationRule]] = FrozenList(
142-
[
143+
(
143144
LoneSchemaDefinitionRule,
144145
UniqueOperationTypesRule,
145146
UniqueTypeNamesRule,
146147
UniqueEnumValueNamesRule,
147148
UniqueFieldDefinitionNamesRule,
149+
UniqueArgumentDefinitionNamesRule,
148150
UniqueDirectiveNamesRule,
149151
KnownTypeNamesRule,
150152
KnownDirectivesRule,
@@ -154,7 +156,7 @@
154156
UniqueArgumentNamesRule,
155157
UniqueInputFieldNamesRule,
156158
ProvidedRequiredArgumentsOnDirectivesRule,
157-
]
159+
)
158160
)
159161
specified_sdl_rules.__doc__ = """\
160162
This list includes all rules for validating SDL.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
from functools import partial
2+
3+
from graphql.validation.rules.unique_argument_definition_names import (
4+
UniqueArgumentDefinitionNamesRule,
5+
)
6+
7+
from .harness import assert_sdl_validation_errors
8+
9+
assert_sdl_errors = partial(
10+
assert_sdl_validation_errors, UniqueArgumentDefinitionNamesRule
11+
)
12+
13+
assert_sdl_valid = partial(assert_sdl_errors, errors=[])
14+
15+
16+
def describe_validate_unique_argument_definition_names():
17+
def no_args():
18+
assert_sdl_valid(
19+
"""
20+
type SomeObject {
21+
someField: String
22+
}
23+
24+
interface SomeInterface {
25+
someField: String
26+
}
27+
28+
directive @someDirective on QUERY
29+
"""
30+
)
31+
32+
def one_argument():
33+
assert_sdl_valid(
34+
"""
35+
type SomeObject {
36+
someField(foo: String): String
37+
}
38+
39+
interface SomeInterface {
40+
someField(foo: String): String
41+
}
42+
43+
extend type SomeObject {
44+
anotherField(foo: String): String
45+
}
46+
47+
extend interface SomeInterface {
48+
anotherField(foo: String): String
49+
}
50+
51+
directive @someDirective(foo: String) on QUERY
52+
"""
53+
)
54+
55+
def multiple_arguments():
56+
assert_sdl_valid(
57+
"""
58+
type SomeObject {
59+
someField(
60+
foo: String
61+
bar: String
62+
): String
63+
}
64+
65+
interface SomeInterface {
66+
someField(
67+
foo: String
68+
bar: String
69+
): String
70+
}
71+
72+
extend type SomeObject {
73+
anotherField(
74+
foo: String
75+
bar: String
76+
): String
77+
}
78+
79+
extend interface SomeInterface {
80+
anotherField(
81+
foo: String
82+
bar: String
83+
): String
84+
}
85+
86+
directive @someDirective(
87+
foo: String
88+
bar: String
89+
) on QUERY
90+
"""
91+
)
92+
93+
def duplicating_arguments():
94+
assert_sdl_errors(
95+
"""
96+
type SomeObject {
97+
someField(
98+
foo: String
99+
bar: String
100+
foo: String
101+
): String
102+
}
103+
104+
interface SomeInterface {
105+
someField(
106+
foo: String
107+
bar: String
108+
foo: String
109+
): String
110+
}
111+
112+
extend type SomeObject {
113+
anotherField(
114+
foo: String
115+
bar: String
116+
bar: String
117+
): String
118+
}
119+
120+
extend interface SomeInterface {
121+
anotherField(
122+
bar: String
123+
foo: String
124+
foo: String
125+
): String
126+
}
127+
128+
directive @someDirective(
129+
foo: String
130+
bar: String
131+
foo: String
132+
) on QUERY
133+
""",
134+
[
135+
{
136+
"message": "Argument 'SomeObject.someField(foo:)'"
137+
" can only be defined once.",
138+
"locations": [(4, 17), (6, 17)],
139+
},
140+
{
141+
"message": "Argument 'SomeInterface.someField(foo:)'"
142+
" can only be defined once.",
143+
"locations": [(12, 17), (14, 17)],
144+
},
145+
{
146+
"message": "Argument 'SomeObject.anotherField(bar:)'"
147+
" can only be defined once.",
148+
"locations": [(21, 17), (22, 17)],
149+
},
150+
{
151+
"message": "Argument 'SomeInterface.anotherField(foo:)'"
152+
" can only be defined once.",
153+
"locations": [(29, 17), (30, 17)],
154+
},
155+
{
156+
"message": "Argument '@someDirective(foo:)'"
157+
" can only be defined once.",
158+
"locations": [(35, 15), (37, 15)],
159+
},
160+
],
161+
)

0 commit comments

Comments
 (0)