Skip to content
This repository was archived by the owner on Jul 25, 2024. It is now read-only.

Commit e1b951e

Browse files
author
Josh Price
committed
Merge pull request #81 from freshtonic/validations/no-fragment-cycles
NoFragmentCycles validation
2 parents 8e1a6c8 + 48d8fb9 commit e1b951e

File tree

5 files changed

+330
-3
lines changed

5 files changed

+330
-3
lines changed

lib/graphql/util/stack.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,6 @@ defmodule GraphQL.Util.Stack do
2525
[] -> nil
2626
end
2727
end
28+
29+
def length(stack), do: Kernel.length(stack.elements)
2830
end

lib/graphql/validation/rules.ex

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22
defmodule GraphQL.Validation.Rules do
33

44
# All of the known validation rules.
5-
# TODO: it would be great if the rules auto-registered themselves.
6-
# A task for a rainy day.
75
@rules [
86
%GraphQL.Validation.Rules.UniqueOperationNames{},
97
%GraphQL.Validation.Rules.FieldsOnCorrectType{},
10-
%GraphQL.Validation.Rules.ProvidedNonNullArguments{}
8+
%GraphQL.Validation.Rules.ProvidedNonNullArguments{},
9+
%GraphQL.Validation.Rules.NoFragmentCycles{}
1110
]
1211

1312
def all(), do: @rules
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
defmodule GraphQL.Validation.Rules.NoFragmentCycles do
2+
3+
alias GraphQL.Lang.AST.{Visitor, InitialisingVisitor}
4+
alias GraphQL.Util.Stack
5+
import GraphQL.Validation
6+
7+
defstruct name: "NoFragmentCycles"
8+
9+
defimpl InitialisingVisitor do
10+
def init(_visitor, acc) do
11+
Map.merge(acc, %{
12+
visited_fragments: %{},
13+
spread_path: %Stack{},
14+
spread_path_indices: %{}
15+
})
16+
end
17+
end
18+
19+
defimpl Visitor do
20+
def enter(_visitor, %{kind: :FragmentDefinition} = node, acc) do
21+
if !visited?(acc, node) do
22+
{:continue, detect_cycles(acc, node)}
23+
else
24+
{:continue, acc}
25+
end
26+
end
27+
28+
def enter(_visitor, _node, acc) do
29+
{:continue, acc}
30+
end
31+
32+
def leave(_visitor, _node, acc) do
33+
{:continue, acc}
34+
end
35+
36+
defp detect_cycles(acc, fragment_def) do
37+
acc
38+
|> mark_visited(fragment_def)
39+
|> detect_cycles_via_spread_nodes(fragment_def, spread_nodes_of_fragment(fragment_def))
40+
end
41+
42+
defp detect_cycles_via_spread_nodes(acc, _, []), do: acc
43+
defp detect_cycles_via_spread_nodes(acc, fragment_def, spread_nodes) do
44+
frag_name = fragment_def.name.value
45+
acc = %{ acc | spread_path_indices:
46+
Map.merge(acc[:spread_path_indices], %{frag_name => Stack.length(acc[:spread_path])})}
47+
48+
acc = process_spread_nodes(acc, spread_nodes)
49+
50+
%{ acc | spread_path_indices: Map.delete(acc[:spread_path_indices], frag_name)}
51+
end
52+
53+
defp process_spread_nodes(acc, []), do: acc
54+
defp process_spread_nodes(acc, [spread_node|rest]) do
55+
spread_name = spread_node.name.value
56+
cycle_index = Map.get(acc[:spread_path_indices], spread_name, nil)
57+
process_spread_nodes(process_one_node(acc, spread_node, cycle_index), rest)
58+
end
59+
60+
defp process_one_node(acc, spread_node, cycle_index) when is_integer(cycle_index) do
61+
cycle_path = Enum.slice(
62+
Enum.reverse(acc[:spread_path].elements),
63+
cycle_index,
64+
Stack.length(acc[:spread_path])
65+
)
66+
report_error(acc, cycle_error_message(spread_node.name.value, Enum.map(cycle_path, fn(s) -> s.name.value end)))
67+
end
68+
69+
defp process_one_node(acc, spread_node, _) do
70+
acc = %{ acc | spread_path: Stack.push(acc[:spread_path], spread_node)}
71+
if !visited?(acc, spread_node) do
72+
spread_fragment = get_fragment(acc[:document], spread_node.name.value)
73+
if spread_fragment do
74+
acc = detect_cycles(acc, spread_fragment)
75+
end
76+
end
77+
%{ acc | spread_path: Stack.pop(acc[:spread_path])}
78+
end
79+
80+
defp visited?(acc, node) do
81+
Map.has_key?(acc[:visited_fragments], node.name.value)
82+
end
83+
84+
defp mark_visited(acc, node) do
85+
%{acc | visited_fragments:
86+
Map.merge(acc[:visited_fragments], %{node.name.value => true})}
87+
end
88+
89+
defp cycle_error_message(frag_name, spread_names) do
90+
via = case length(spread_names) do
91+
0 -> ""
92+
_ -> " via #{Enum.join(spread_names, ", ")}"
93+
end
94+
"Cannot spread fragment #{frag_name} within itself#{via}."
95+
end
96+
97+
defp get_fragment(document, fragment_name) do
98+
Enum.find(document.definitions, fn(definition) ->
99+
definition.kind == :FragmentDefinition && definition.name.value == fragment_name
100+
end)
101+
end
102+
103+
defp spread_nodes_of_fragment(fragment_def) do
104+
spread_nodes_of_selection_sets([fragment_def.selectionSet])
105+
end
106+
107+
defp spread_nodes_of_selection_sets([]), do: []
108+
defp spread_nodes_of_selection_sets([selection_set|rest]) do
109+
spread_nodes_of_selections(selection_set.selections) ++ spread_nodes_of_selection_sets(rest)
110+
end
111+
112+
defp spread_nodes_of_selections([]), do: []
113+
defp spread_nodes_of_selections([%{kind: :FragmentSpread} = selection|rest]) do
114+
[selection] ++ spread_nodes_of_selections(rest)
115+
end
116+
defp spread_nodes_of_selections([%{selectionSet: selection_set}|rest]) do
117+
spread_nodes_of_selection_sets([selection_set]) ++ spread_nodes_of_selections(rest)
118+
end
119+
defp spread_nodes_of_selections([_|rest]) do
120+
spread_nodes_of_selections(rest)
121+
end
122+
end
123+
end

lib/graphql/validation/validator.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ defmodule GraphQL.Validation.Validator do
2828
])
2929
result = Reducer.reduce(document, validation_pipeline, %{
3030
type_info: %TypeInfo{schema: schema},
31+
document: document,
3132
validation_errors: []
3233
})
3334
errors = result[:validation_errors]
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
2+
Code.require_file "../../../support/validations.exs", __DIR__
3+
4+
defmodule GraphQL.Validation.Rules.NoFragmentCyclesTest do
5+
use ExUnit.Case, async: true
6+
7+
import ValidationsSupport
8+
9+
alias GraphQL.Validation.Rules.NoFragmentCycles, as: Rule
10+
11+
test "single reference is valid" do
12+
assert_passes_rule(
13+
"""
14+
fragment fragA on Dog { ...fragB }
15+
fragment fragB on Dog { name }
16+
""",
17+
%Rule{}
18+
)
19+
end
20+
21+
test "spreading twice is not circular" do
22+
assert_passes_rule(
23+
"""
24+
fragment fragA on Dog { ...fragB, ...fragB }
25+
fragment fragB on Dog { name }
26+
""",
27+
%Rule{}
28+
)
29+
end
30+
31+
test "spreading twice indirectly is not circular" do
32+
assert_passes_rule(
33+
"""
34+
fragment fragA on Dog { ...fragB, ...fragC }
35+
fragment fragB on Dog { ...fragC }
36+
fragment fragC on Dog { name }
37+
""",
38+
%Rule{}
39+
)
40+
end
41+
42+
test "double spread within abstract types" do
43+
assert_passes_rule(
44+
"""
45+
fragment nameFragment on Pet {
46+
... on Dog { name }
47+
... on Cat { name }
48+
}
49+
50+
fragment spreadsInAnon on Pet {
51+
... on Dog { ...nameFragment }
52+
... on Cat { ...nameFragment }
53+
}
54+
""",
55+
%Rule{}
56+
)
57+
end
58+
59+
test "does not false positive on unknown fragment" do
60+
assert_passes_rule(
61+
"""
62+
fragment nameFragment on Pet {
63+
...UnknownFragment
64+
}
65+
""",
66+
%Rule{}
67+
)
68+
end
69+
70+
test "spreading recursively within field fails" do
71+
assert_fails_rule(
72+
"""
73+
fragment fragA on Human { relatives { ...fragA } }
74+
""",
75+
%Rule{},
76+
["Cannot spread fragment fragA within itself."]
77+
)
78+
end
79+
80+
test "no spreading itself directly" do
81+
assert_fails_rule(
82+
"""
83+
fragment fragA on Dog { ...fragA }
84+
""",
85+
%Rule{},
86+
["Cannot spread fragment fragA within itself."]
87+
)
88+
end
89+
90+
test "no spreading itself directly within inline fragment" do
91+
assert_fails_rule(
92+
"""
93+
fragment fragA on Pet {
94+
... on Dog {
95+
...fragA
96+
}
97+
}
98+
""",
99+
%Rule{},
100+
["Cannot spread fragment fragA within itself."]
101+
)
102+
end
103+
104+
test "no spreading itself indirectly" do
105+
assert_fails_rule(
106+
"""
107+
fragment fragA on Dog { ...fragB }
108+
fragment fragB on Dog { ...fragA }
109+
""",
110+
%Rule{},
111+
["Cannot spread fragment fragA within itself via fragB."]
112+
)
113+
end
114+
115+
test "no spreading itself indirectly reports opposite order" do
116+
assert_fails_rule(
117+
"""
118+
fragment fragB on Dog { ...fragA }
119+
fragment fragA on Dog { ...fragB }
120+
""",
121+
%Rule{},
122+
["Cannot spread fragment fragB within itself via fragA."]
123+
)
124+
end
125+
126+
test "no spreading itself indirectly within inline fragment" do
127+
assert_fails_rule(
128+
"""
129+
fragment fragA on Pet {
130+
... on Dog {
131+
...fragB
132+
}
133+
}
134+
fragment fragB on Pet {
135+
... on Dog {
136+
...fragA
137+
}
138+
}
139+
""",
140+
%Rule{},
141+
["Cannot spread fragment fragA within itself via fragB."]
142+
)
143+
end
144+
145+
test "no spreading itself deeply" do
146+
assert_fails_rule(
147+
"""
148+
fragment fragA on Dog { ...fragB }
149+
fragment fragB on Dog { ...fragC }
150+
fragment fragC on Dog { ...fragO }
151+
fragment fragX on Dog { ...fragY }
152+
fragment fragY on Dog { ...fragZ }
153+
fragment fragZ on Dog { ...fragO }
154+
fragment fragO on Dog { ...fragP }
155+
fragment fragP on Dog { ...fragA, ...fragX }
156+
""",
157+
%Rule{},
158+
["Cannot spread fragment fragA within itself via fragB, fragC, fragO, fragP.",
159+
"Cannot spread fragment fragO within itself via fragP, fragX, fragY, fragZ."]
160+
)
161+
end
162+
163+
test "no spreading itself deeply two paths" do
164+
assert_fails_rule(
165+
"""
166+
fragment fragA on Dog { ...fragB, ...fragC }
167+
fragment fragB on Dog { ...fragA }
168+
fragment fragC on Dog { ...fragA }
169+
""",
170+
%Rule{},
171+
["Cannot spread fragment fragA within itself via fragB.",
172+
"Cannot spread fragment fragA within itself via fragC."]
173+
)
174+
end
175+
176+
test "no spreading itself deeply two paths -- alt traverse order" do
177+
assert_fails_rule(
178+
"""
179+
fragment fragA on Dog { ...fragC }
180+
fragment fragB on Dog { ...fragC }
181+
fragment fragC on Dog { ...fragA, ...fragB }
182+
""",
183+
%Rule{},
184+
["Cannot spread fragment fragA within itself via fragC.",
185+
"Cannot spread fragment fragC within itself via fragB."]
186+
)
187+
end
188+
189+
test "no spreading itself deeply and immediately" do
190+
assert_fails_rule(
191+
"""
192+
fragment fragA on Dog { ...fragB }
193+
fragment fragB on Dog { ...fragB, ...fragC }
194+
fragment fragC on Dog { ...fragA, ...fragB }
195+
""",
196+
%Rule{},
197+
["Cannot spread fragment fragB within itself.",
198+
"Cannot spread fragment fragA within itself via fragB, fragC.",
199+
"Cannot spread fragment fragB within itself via fragC."]
200+
)
201+
end
202+
end

0 commit comments

Comments
 (0)