Skip to content

Commit af50c0b

Browse files
committed
Merge pull request #60 from jhgg/unique-input-field-names
Implement [RFC] Move input field uniqueness validator
2 parents de3b371 + 1084b78 commit af50c0b

File tree

5 files changed

+86
-22
lines changed

5 files changed

+86
-22
lines changed

graphql/core/language/parser.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -379,25 +379,16 @@ def parse_array(parser, is_const):
379379
def parse_object(parser, is_const):
380380
start = parser.token.start
381381
expect(parser, TokenKind.BRACE_L)
382-
field_names = set()
383382
fields = []
384383
while not skip(parser, TokenKind.BRACE_R):
385-
fields.append(parse_object_field(parser, is_const, field_names))
384+
fields.append(parse_object_field(parser, is_const))
386385
return ast.ObjectValue(fields=fields, loc=loc(parser, start))
387386

388387

389-
def parse_object_field(parser, is_const, field_names):
388+
def parse_object_field(parser, is_const):
390389
start = parser.token.start
391-
name = parse_name(parser)
392-
if name.value in field_names:
393-
raise LanguageError(
394-
parser.source,
395-
start,
396-
"Duplicate input object field {}.".format(name.value)
397-
)
398-
field_names.add(name.value)
399390
return ast.ObjectField(
400-
name=name,
391+
name=parse_name(parser),
401392
value=(
402393
expect(parser, TokenKind.COLON),
403394
parse_value(parser, is_const))[1],

graphql/core/validation/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
Rules.DefaultValuesOfCorrectType,
2929
Rules.VariablesInAllowedPosition,
3030
Rules.OverlappingFieldsCanBeMerged,
31+
Rules.UniqueInputFieldNames
3132
]
3233

3334

graphql/core/validation/rules.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ def reduce_spread_fragments(spreads):
246246
)
247247
for fragment_definition in self.fragment_definitions
248248
if fragment_definition.name.value not in fragment_names_used
249-
]
249+
]
250250

251251
if errors:
252252
return errors
@@ -315,7 +315,7 @@ def __init__(self, context):
315315
node.name.value: self.gather_spreads(node)
316316
for node in context.get_ast().definitions
317317
if isinstance(node, ast.FragmentDefinition)
318-
}
318+
}
319319
self.known_to_lead_to_cycle = set()
320320

321321
def enter_FragmentDefinition(self, node, *args):
@@ -450,7 +450,7 @@ def leave_OperationDefinition(self, *args):
450450
)
451451
for variable_definition in self.variable_definitions
452452
if variable_definition.variable.name.value not in self.variable_name_used
453-
]
453+
]
454454

455455
if errors:
456456
return errors
@@ -851,7 +851,7 @@ def leave_SelectionSet(self, node, *args):
851851
return [
852852
GraphQLError(self.fields_conflict_message(reason_name, reason), list(fields)) for
853853
(reason_name, reason), fields in conflicts
854-
]
854+
]
855855

856856
@staticmethod
857857
def same_type(type1, type2):
@@ -967,3 +967,26 @@ def reason_message(cls, reason):
967967
for reason_name, sub_reason in reason)
968968

969969
return reason
970+
971+
972+
class UniqueInputFieldNames(ValidationRule):
973+
def __init__(self, context):
974+
super(UniqueInputFieldNames, self).__init__(context)
975+
self.known_names = {}
976+
977+
def enter_ObjectValue(self, *args):
978+
self.known_names = {}
979+
980+
def enter_ObjectField(self, node, *args):
981+
field_name = node.name.value
982+
if field_name in self.known_names:
983+
return GraphQLError(
984+
self.duplicate_input_field_message(field_name),
985+
[self.known_names[field_name], node.name]
986+
)
987+
988+
self.known_names[field_name] = node.name
989+
990+
@staticmethod
991+
def duplicate_input_field_message(field_name):
992+
return 'There can only be one input field named "{}".'.format(field_name)

tests/core_language/test_parser.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,6 @@ def test_parses_constant_default_values():
4242
assert 'Syntax Error GraphQL (1:37) Unexpected $' in str(excinfo.value)
4343

4444

45-
def test_duplicate_keys_in_input_object_is_syntax_error():
46-
with raises(LanguageError) as excinfo:
47-
parse('{ field(arg: { a: 1, a: 2 }) }')
48-
assert 'Syntax Error GraphQL (1:22) Duplicate input object field a.' in str(excinfo.value)
49-
50-
5145
def test_parses_kitchen_sink():
5246
parse(KITCHEN_SINK)
5347

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from graphql.core.language.location import SourceLocation as L
2+
from graphql.core.validation.rules import UniqueInputFieldNames
3+
from utils import expect_passes_rule, expect_fails_rule
4+
5+
6+
def duplicate_field(name, l1, l2):
7+
return {
8+
'message': UniqueInputFieldNames.duplicate_input_field_message(name),
9+
'locations': [l1, l2]
10+
}
11+
12+
13+
def test_input_object_with_fields():
14+
expect_passes_rule(UniqueInputFieldNames, '''
15+
{
16+
field(arg: { f: true })
17+
}
18+
''')
19+
20+
21+
def test_same_input_object_within_two_args():
22+
expect_passes_rule(UniqueInputFieldNames, '''
23+
{
24+
field(arg1: { f: true }, arg2: { f: true })
25+
}
26+
''')
27+
28+
29+
def test_multiple_input_object_fields():
30+
expect_passes_rule(UniqueInputFieldNames, '''
31+
{
32+
field(arg: { f1: "value", f2: "value", f3: "value" })
33+
}
34+
''')
35+
36+
37+
def test_duplicate_input_object_fields():
38+
expect_fails_rule(UniqueInputFieldNames, '''
39+
{
40+
field(arg: { f1: "value", f1: "value" })
41+
}
42+
''', [
43+
duplicate_field("f1", L(3, 22), L(3, 35))
44+
])
45+
46+
47+
def test_many_duplicate_input_object_fields():
48+
expect_fails_rule(UniqueInputFieldNames, '''
49+
{
50+
field(arg: { f1: "value", f1: "value", f1: "value" })
51+
}
52+
''', [
53+
duplicate_field('f1', L(3, 22), L(3, 35)),
54+
duplicate_field('f1', L(3, 22), L(3, 48))
55+
])

0 commit comments

Comments
 (0)