Skip to content

Commit 8028bc4

Browse files
committed
Merge pull request #27 from jhgg/no-fragment-cycles
Implement NoFragmentCycles validation (also tests)
2 parents 7702eea + e936820 commit 8028bc4

File tree

2 files changed

+203
-2
lines changed

2 files changed

+203
-2
lines changed

graphql/core/validation/rules.py

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from ..error import GraphQLError
33
from ..type.definition import is_composite_type, is_input_type, is_leaf_type, GraphQLNonNull
44
from ..language import ast
5-
from ..language.visitor import Visitor
5+
from ..language.visitor import Visitor, visit
66
from ..language.printer import print_ast
77

88

@@ -239,7 +239,74 @@ class PossibleFragmentSpreads(ValidationRule):
239239

240240

241241
class NoFragmentCycles(ValidationRule):
242-
pass
242+
def __init__(self, context):
243+
super(NoFragmentCycles, self).__init__(context)
244+
self.spreads_in_fragment = {
245+
node.name.value: self.gather_spreads(node)
246+
for node in context.get_ast().definitions
247+
if isinstance(node, ast.FragmentDefinition)
248+
}
249+
self.known_to_lead_to_cycle = set()
250+
251+
def enter_FragmentDefinition(self, node, *args):
252+
errors = []
253+
initial_name = node.name.value
254+
spread_path = []
255+
256+
# This will convert the ast.FragmentDefinition to something that we can add
257+
# to a set. Otherwise we get a `unhashable type: dict` error.
258+
# This makes it so that we can define a way to uniquely identify a FragmentDefinition
259+
# within a set.
260+
fragment_node_to_hashable = lambda fs: (fs.loc['start'], fs.loc['end'], fs.name.value)
261+
262+
def detect_cycle_recursive(fragment_name):
263+
spread_nodes = self.spreads_in_fragment[fragment_name]
264+
265+
for spread_node in spread_nodes:
266+
if fragment_node_to_hashable(spread_node) in self.known_to_lead_to_cycle:
267+
continue
268+
269+
if spread_node.name.value == initial_name:
270+
cycle_path = spread_path + [spread_node]
271+
self.known_to_lead_to_cycle |= set(map(fragment_node_to_hashable, cycle_path))
272+
273+
errors.append(GraphQLError(
274+
self.cycle_error_message(initial_name, [s.name.value for s in spread_path]),
275+
cycle_path
276+
))
277+
continue
278+
279+
if any(spread is spread_node for spread in spread_path):
280+
continue
281+
282+
spread_path.append(spread_node)
283+
detect_cycle_recursive(spread_node.name.value)
284+
spread_path.pop()
285+
286+
detect_cycle_recursive(initial_name)
287+
if errors:
288+
return errors
289+
290+
@staticmethod
291+
def cycle_error_message(fragment_name, spread_names):
292+
via = ' via {}'.format(', '.join(spread_names)) if spread_names else ''
293+
return 'Cannot spread fragment "{}" within itself{}.'.format(fragment_name, via)
294+
295+
@classmethod
296+
def gather_spreads(cls, node):
297+
visitor = cls.CollectFragmentSpreadNodesVisitor()
298+
visit(node, visitor)
299+
return visitor.collect_fragment_spread_nodes()
300+
301+
class CollectFragmentSpreadNodesVisitor(Visitor):
302+
def __init__(self):
303+
self.spread_nodes = []
304+
305+
def enter_FragmentSpread(self, node, *args):
306+
self.spread_nodes.append(node)
307+
308+
def collect_fragment_spread_nodes(self):
309+
return self.spread_nodes
243310

244311

245312
class NoUndefinedVariables(ValidationRule):
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
from graphql.core.language.location import SourceLocation as L
2+
from graphql.core.validation.rules import NoFragmentCycles
3+
from utils import expect_passes_rule, expect_fails_rule
4+
5+
6+
def cycle_error_message(fragment_name, spread_names, *locations):
7+
return {
8+
'message': NoFragmentCycles.cycle_error_message(fragment_name, spread_names),
9+
'locations': list(locations)
10+
}
11+
12+
13+
def test_single_reference_is_valid():
14+
expect_passes_rule(NoFragmentCycles, '''
15+
fragment fragA on Dog { ...fragB }
16+
fragment fragB on Dog { name }
17+
''')
18+
19+
20+
def test_spreading_twice_is_not_circular():
21+
expect_passes_rule(NoFragmentCycles, '''
22+
fragment fragA on Dog { ...fragB, ...fragB }
23+
fragment fragB on Dog { name }
24+
''')
25+
26+
27+
def test_spreading_twice_indirectly_is_not_circular():
28+
expect_passes_rule(NoFragmentCycles, '''
29+
fragment fragA on Dog { ...fragB, ...fragC }
30+
fragment fragB on Dog { ...fragC }
31+
fragment fragC on Dog { name }
32+
''')
33+
34+
35+
def test_double_spread_within_abstract_types():
36+
expect_passes_rule(NoFragmentCycles, '''
37+
fragment nameFragment on Pet {
38+
... on Dog { name }
39+
... on Cat { name }
40+
}
41+
fragment spreadsInAnon on Pet {
42+
... on Dog { ...nameFragment }
43+
... on Cat { ...nameFragment }
44+
}
45+
''')
46+
47+
48+
def test_spreading_recursively_within_field_fails():
49+
expect_fails_rule(NoFragmentCycles, '''
50+
fragment fragA on Human { relatives { ...fragA } },
51+
''', [
52+
cycle_error_message('fragA', [], L(2, 43))
53+
])
54+
55+
56+
def test_no_spreading_itself_directly():
57+
expect_fails_rule(NoFragmentCycles, '''
58+
fragment fragA on Dog { ...fragA }
59+
''', [
60+
cycle_error_message('fragA', [], L(2, 29))
61+
])
62+
63+
64+
def test_no_spreading_itself_directly_within_inline_fragment():
65+
expect_fails_rule(NoFragmentCycles, '''
66+
fragment fragA on Pet {
67+
... on Dog {
68+
...fragA
69+
}
70+
}
71+
''', [
72+
cycle_error_message('fragA', [], L(4, 13))
73+
])
74+
75+
76+
def test_no_spreading_itself_indirectly():
77+
expect_fails_rule(NoFragmentCycles, '''
78+
fragment fragA on Dog { ...fragB }
79+
fragment fragB on Dog { ...fragA }
80+
''', [
81+
cycle_error_message('fragA', ['fragB'], L(2, 29), L(3, 29))
82+
])
83+
84+
85+
def test_no_spreading_itself_indirectly_reports_opposite_order():
86+
expect_fails_rule(NoFragmentCycles, '''
87+
fragment fragB on Dog { ...fragA }
88+
fragment fragA on Dog { ...fragB }
89+
''', [
90+
cycle_error_message('fragB', ['fragA'], L(2, 29), L(3, 29))
91+
])
92+
93+
94+
def test_no_spreading_itself_indirectly_within_inline_fragment():
95+
expect_fails_rule(NoFragmentCycles, '''
96+
fragment fragA on Pet {
97+
... on Dog {
98+
...fragB
99+
}
100+
}
101+
fragment fragB on Pet {
102+
... on Dog {
103+
...fragA
104+
}
105+
}
106+
''', [
107+
cycle_error_message('fragA', ['fragB'], L(4, 13), L(9, 13))
108+
])
109+
110+
111+
def test_no_spreading_itself_deeply():
112+
expect_fails_rule(NoFragmentCycles, '''
113+
fragment fragA on Dog { ...fragB }
114+
fragment fragB on Dog { ...fragC }
115+
fragment fragC on Dog { ...fragO }
116+
fragment fragX on Dog { ...fragY }
117+
fragment fragY on Dog { ...fragZ }
118+
fragment fragZ on Dog { ...fragO }
119+
fragment fragO on Dog { ...fragA, ...fragX }
120+
''', [
121+
cycle_error_message('fragA', ['fragB', 'fragC', 'fragO'], L(2, 29), L(3, 29), L(4, 29), L(8, 29)),
122+
cycle_error_message('fragX', ['fragY', 'fragZ', 'fragO'], L(5, 29), L(6, 29), L(7, 29), L(8, 39))
123+
])
124+
125+
126+
def test_no_spreading_itself_deeply_two_paths(): # -- new rule
127+
expect_fails_rule(NoFragmentCycles, '''
128+
fragment fragA on Dog { ...fragB, ...fragC }
129+
fragment fragB on Dog { ...fragA }
130+
fragment fragC on Dog { ...fragA }
131+
''', [
132+
cycle_error_message('fragA', ['fragB'], L(2, 29), L(3, 29)),
133+
cycle_error_message('fragA', ['fragC'], L(2, 39), L(4, 29))
134+
])

0 commit comments

Comments
 (0)