Skip to content

Commit 5dc00fb

Browse files
committed
[Validation] Perf improvements for fragment cycle detection
Related GraphQL-js commit graphql/graphql-js@4cf2190
1 parent 5a97203 commit 5dc00fb

File tree

2 files changed

+82
-59
lines changed

2 files changed

+82
-59
lines changed

graphql/core/validation/rules/no_fragment_cycles.py

Lines changed: 54 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,71 +5,70 @@
55

66

77
class NoFragmentCycles(ValidationRule):
8-
__slots__ = 'spreads_in_fragment', 'known_to_lead_to_cycle'
8+
__slots__ = 'errors', 'visited_frags', 'spread_path', 'spread_path_index_by_name'
99

1010
def __init__(self, context):
1111
super(NoFragmentCycles, self).__init__(context)
12-
self.spreads_in_fragment = {
13-
node.name.value: self.gather_spreads(node)
14-
for node in context.get_ast().definitions
15-
if isinstance(node, ast.FragmentDefinition)
16-
}
17-
self.known_to_lead_to_cycle = set()
12+
self.errors = []
13+
self.visited_frags = set()
14+
self.spread_path = []
15+
self.spread_path_index_by_name = {}
1816

19-
def enter_FragmentDefinition(self, node, key, parent, path, ancestors):
20-
errors = []
21-
initial_name = node.name.value
22-
spread_path = []
23-
24-
def detect_cycle_recursive(fragment_name):
25-
spread_nodes = self.spreads_in_fragment.get(fragment_name)
26-
if not spread_nodes:
27-
return
28-
29-
for spread_node in spread_nodes:
30-
if spread_node in self.known_to_lead_to_cycle:
31-
continue
32-
33-
if spread_node.name.value == initial_name:
34-
cycle_path = spread_path + [spread_node]
35-
self.known_to_lead_to_cycle |= set(cycle_path)
36-
37-
errors.append(GraphQLError(
38-
self.cycle_error_message(initial_name, [s.name.value for s in spread_path]),
39-
cycle_path
40-
))
41-
continue
42-
43-
if any(spread is spread_node for spread in spread_path):
44-
continue
17+
def leave_Document(self, node, key, parent, path, ancestors):
18+
if self.errors:
19+
return self.errors
4520

46-
spread_path.append(spread_node)
47-
detect_cycle_recursive(spread_node.name.value)
48-
spread_path.pop()
21+
def enter_OperationDefinition(self, node, key, parent, path, ancestors):
22+
return False
4923

50-
detect_cycle_recursive(initial_name)
51-
if errors:
52-
return errors
24+
def enter_FragmentDefinition(self, node, key, parent, path, ancestors):
25+
if node.name.value not in self.visited_frags:
26+
self.detect_cycle_recursive(node)
27+
return False
28+
29+
def detect_cycle_recursive(self, fragment):
30+
fragment_name = fragment.name.value
31+
self.visited_frags.add(fragment_name)
32+
33+
spread_nodes = []
34+
self.gather_spreads(spread_nodes, fragment.selection_set)
35+
if not spread_nodes:
36+
return
37+
38+
self.spread_path_index_by_name[fragment_name] = len(self.spread_path)
39+
40+
for spread_node in spread_nodes:
41+
spread_name = spread_node.name.value
42+
cycle_index = self.spread_path_index_by_name.get(spread_name)
43+
44+
if cycle_index is None:
45+
self.spread_path.append(spread_node)
46+
if spread_name not in self.visited_frags:
47+
spread_fragment = self.context.get_fragment(spread_name)
48+
if spread_fragment:
49+
self.detect_cycle_recursive(spread_fragment)
50+
self.spread_path.pop()
51+
else:
52+
cycle_path = self.spread_path[cycle_index:]
53+
self.errors.append(GraphQLError(
54+
self.cycle_error_message(
55+
spread_name,
56+
[s.name.value for s in cycle_path]
57+
),
58+
cycle_path+[spread_node]
59+
))
60+
61+
self.spread_path_index_by_name[fragment_name] = None
5362

5463
@staticmethod
5564
def cycle_error_message(fragment_name, spread_names):
5665
via = ' via {}'.format(', '.join(spread_names)) if spread_names else ''
5766
return 'Cannot spread fragment "{}" within itself{}.'.format(fragment_name, via)
5867

5968
@classmethod
60-
def gather_spreads(cls, node):
61-
visitor = cls.CollectFragmentSpreadNodesVisitor()
62-
visit(node, visitor)
63-
return visitor.collect_fragment_spread_nodes()
64-
65-
class CollectFragmentSpreadNodesVisitor(Visitor):
66-
__slots__ = 'spread_nodes',
67-
68-
def __init__(self):
69-
self.spread_nodes = []
70-
71-
def enter_FragmentSpread(self, node, key, parent, path, ancestors):
72-
self.spread_nodes.append(node)
73-
74-
def collect_fragment_spread_nodes(self):
75-
return self.spread_nodes
69+
def gather_spreads(cls, spreads, node):
70+
for selection in node.selections:
71+
if isinstance(selection, ast.FragmentSpread):
72+
spreads.append(selection)
73+
elif selection.selection_set:
74+
cls.gather_spreads(spreads, selection.selection_set)

tests/core_validation/test_no_fragment_cycles.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,15 @@ def test_no_spreading_itself_deeply():
124124
fragment fragX on Dog { ...fragY }
125125
fragment fragY on Dog { ...fragZ }
126126
fragment fragZ on Dog { ...fragO }
127-
fragment fragO on Dog { ...fragA, ...fragX }
127+
fragment fragO on Dog { ...fragP }
128+
fragment fragP on Dog { ...fragA, ...fragX }
128129
''', [
129-
cycle_error_message('fragA', ['fragB', 'fragC', 'fragO'], L(2, 29), L(3, 29), L(4, 29), L(8, 29)),
130-
cycle_error_message('fragX', ['fragY', 'fragZ', 'fragO'], L(5, 29), L(6, 29), L(7, 29), L(8, 39))
130+
cycle_error_message('fragA', ['fragB', 'fragC', 'fragO', 'fragP'], L(2, 29), L(3, 29), L(4, 29), L(8, 29), L(9, 29)),
131+
cycle_error_message('fragO', ['fragP', 'fragX', 'fragY', 'fragZ'], L(8, 29), L(9, 39), L(5, 29), L(6, 29), L(7, 29))
131132
])
132133

133134

134-
def test_no_spreading_itself_deeply_two_paths(): # -- new rule
135+
def test_no_spreading_itself_deeply_two_paths():
135136
expect_fails_rule(NoFragmentCycles, '''
136137
fragment fragA on Dog { ...fragB, ...fragC }
137138
fragment fragB on Dog { ...fragA }
@@ -140,3 +141,26 @@ def test_no_spreading_itself_deeply_two_paths(): # -- new rule
140141
cycle_error_message('fragA', ['fragB'], L(2, 29), L(3, 29)),
141142
cycle_error_message('fragA', ['fragC'], L(2, 39), L(4, 29))
142143
])
144+
145+
146+
def test_no_spreading_itself_deeply_two_paths_alt_reverse_order():
147+
expect_fails_rule(NoFragmentCycles, '''
148+
fragment fragA on Dog { ...fragC }
149+
fragment fragB on Dog { ...fragC }
150+
fragment fragC on Dog { ...fragA, ...fragB }
151+
''', [
152+
cycle_error_message('fragA', ['fragC'], L(2, 29), L(4, 29)),
153+
cycle_error_message('fragC', ['fragB'], L(4, 39), L(3, 29))
154+
])
155+
156+
157+
def test_no_spreading_itself_deeply_and_immediately():
158+
expect_fails_rule(NoFragmentCycles, '''
159+
fragment fragA on Dog { ...fragB }
160+
fragment fragB on Dog { ...fragB, ...fragC }
161+
fragment fragC on Dog { ...fragA, ...fragB }
162+
''', [
163+
cycle_error_message('fragB', [], L(3, 29)),
164+
cycle_error_message('fragA', ['fragB', 'fragC'], L(2, 29), L(3, 39), L(4, 29)),
165+
cycle_error_message('fragB', ['fragC'], L(3, 39), L(4, 39))
166+
])

0 commit comments

Comments
 (0)