async_graphql/validation/rules/
no_fragment_cycles.rs1use std::collections::{HashMap, HashSet};
2
3use crate::{
4 Name, Pos, Positioned,
5 parser::types::{ExecutableDocument, FragmentDefinition, FragmentSpread},
6 validation::visitor::{RuleError, Visitor, VisitorContext},
7};
8
9struct CycleDetector<'a> {
10 visited: HashSet<&'a str>,
11 spreads: &'a HashMap<&'a str, Vec<(&'a str, Pos)>>,
12 path_indices: HashMap<&'a str, usize>,
13 errors: Vec<RuleError>,
14}
15
16impl<'a> CycleDetector<'a> {
17 fn detect_from(&mut self, from: &'a str, path: &mut Vec<(&'a str, Pos)>) {
18 self.visited.insert(from);
19
20 if !self.spreads.contains_key(from) {
21 return;
22 }
23
24 self.path_indices.insert(from, path.len());
25
26 for (name, pos) in &self.spreads[from] {
27 let index = self.path_indices.get(name).cloned();
28
29 if let Some(index) = index {
30 let err_pos = if index < path.len() {
31 path[index].1
32 } else {
33 *pos
34 };
35
36 self.errors.push(RuleError::new(
37 vec![err_pos],
38 format!("Cannot spread fragment \"{}\"", name),
39 ));
40 } else if !self.visited.contains(name) {
41 path.push((name, *pos));
42 self.detect_from(name, path);
43 path.pop();
44 }
45 }
46
47 self.path_indices.remove(from);
48 }
49}
50
51#[derive(Default)]
52pub struct NoFragmentCycles<'a> {
53 current_fragment: Option<&'a str>,
54 spreads: HashMap<&'a str, Vec<(&'a str, Pos)>>,
55 fragment_order: Vec<&'a str>,
56}
57
58impl<'a> Visitor<'a> for NoFragmentCycles<'a> {
59 fn exit_document(&mut self, ctx: &mut VisitorContext<'a>, _doc: &'a ExecutableDocument) {
60 let mut detector = CycleDetector {
61 visited: HashSet::new(),
62 spreads: &self.spreads,
63 path_indices: HashMap::new(),
64 errors: Vec::new(),
65 };
66
67 for frag in &self.fragment_order {
68 if !detector.visited.contains(frag) {
69 let mut path = Vec::new();
70 detector.detect_from(frag, &mut path);
71 }
72 }
73
74 ctx.append_errors(detector.errors);
75 }
76
77 fn enter_fragment_definition(
78 &mut self,
79 _ctx: &mut VisitorContext<'a>,
80 name: &'a Name,
81 _fragment_definition: &'a Positioned<FragmentDefinition>,
82 ) {
83 self.current_fragment = Some(name);
84 self.fragment_order.push(name);
85 }
86
87 fn exit_fragment_definition(
88 &mut self,
89 _ctx: &mut VisitorContext<'a>,
90 _name: &'a Name,
91 _fragment_definition: &'a Positioned<FragmentDefinition>,
92 ) {
93 self.current_fragment = None;
94 }
95
96 fn enter_fragment_spread(
97 &mut self,
98 _ctx: &mut VisitorContext<'a>,
99 fragment_spread: &'a Positioned<FragmentSpread>,
100 ) {
101 if let Some(current_fragment) = self.current_fragment {
102 self.spreads.entry(current_fragment).or_default().push((
103 &fragment_spread.node.fragment_name.node,
104 fragment_spread.pos,
105 ));
106 }
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 pub fn factory<'a>() -> NoFragmentCycles<'a> {
115 NoFragmentCycles::default()
116 }
117
118 #[test]
119 fn single_reference_is_valid() {
120 expect_passes_rule!(
121 factory,
122 r#"
123 fragment fragA on Dog { ...fragB }
124 fragment fragB on Dog { name }
125 { __typename }
126 "#,
127 );
128 }
129
130 #[test]
131 fn spreading_twice_is_not_circular() {
132 expect_passes_rule!(
133 factory,
134 r#"
135 fragment fragA on Dog { ...fragB, ...fragB }
136 fragment fragB on Dog { name }
137 { __typename }
138 "#,
139 );
140 }
141
142 #[test]
143 fn spreading_twice_indirectly_is_not_circular() {
144 expect_passes_rule!(
145 factory,
146 r#"
147 fragment fragA on Dog { ...fragB, ...fragC }
148 fragment fragB on Dog { ...fragC }
149 fragment fragC on Dog { name }
150 { __typename }
151 "#,
152 );
153 }
154
155 #[test]
156 fn double_spread_within_abstract_types() {
157 expect_passes_rule!(
158 factory,
159 r#"
160 fragment nameFragment on Pet {
161 ... on Dog { name }
162 ... on Cat { name }
163 }
164 fragment spreadsInAnon on Pet {
165 ... on Dog { ...nameFragment }
166 ... on Cat { ...nameFragment }
167 }
168 { __typename }
169 "#,
170 );
171 }
172
173 #[test]
174 fn does_not_false_positive_on_unknown_fragment() {
175 expect_passes_rule!(
176 factory,
177 r#"
178 fragment nameFragment on Pet {
179 ...UnknownFragment
180 }
181 { __typename }
182 "#,
183 );
184 }
185
186 #[test]
187 fn spreading_recursively_within_field_fails() {
188 expect_fails_rule!(
189 factory,
190 r#"
191 fragment fragA on Human { relatives { ...fragA } },
192 { __typename }
193 "#,
194 );
195 }
196
197 #[test]
198 fn no_spreading_itself_directly() {
199 expect_fails_rule!(
200 factory,
201 r#"
202 fragment fragA on Dog { ...fragA }
203 { __typename }
204 "#,
205 );
206 }
207
208 #[test]
209 fn no_spreading_itself_directly_within_inline_fragment() {
210 expect_fails_rule!(
211 factory,
212 r#"
213 fragment fragA on Pet {
214 ... on Dog {
215 ...fragA
216 }
217 }
218 { __typename }
219 "#,
220 );
221 }
222
223 #[test]
224 fn no_spreading_itself_indirectly() {
225 expect_fails_rule!(
226 factory,
227 r#"
228 fragment fragA on Dog { ...fragB }
229 fragment fragB on Dog { ...fragA }
230 { __typename }
231 "#,
232 );
233 }
234
235 #[test]
236 fn no_spreading_itself_indirectly_reports_opposite_order() {
237 expect_fails_rule!(
238 factory,
239 r#"
240 fragment fragB on Dog { ...fragA }
241 fragment fragA on Dog { ...fragB }
242 { __typename }
243 "#,
244 );
245 }
246
247 #[test]
248 fn no_spreading_itself_indirectly_within_inline_fragment() {
249 expect_fails_rule!(
250 factory,
251 r#"
252 fragment fragA on Pet {
253 ... on Dog {
254 ...fragB
255 }
256 }
257 fragment fragB on Pet {
258 ... on Dog {
259 ...fragA
260 }
261 }
262 { __typename }
263 "#,
264 );
265 }
266
267 #[test]
268 fn no_spreading_itself_deeply() {
269 expect_fails_rule!(
270 factory,
271 r#"
272 fragment fragA on Dog { ...fragB }
273 fragment fragB on Dog { ...fragC }
274 fragment fragC on Dog { ...fragO }
275 fragment fragX on Dog { ...fragY }
276 fragment fragY on Dog { ...fragZ }
277 fragment fragZ on Dog { ...fragO }
278 fragment fragO on Dog { ...fragP }
279 fragment fragP on Dog { ...fragA, ...fragX }
280 { __typename }
281 "#,
282 );
283 }
284
285 #[test]
286 fn no_spreading_itself_deeply_two_paths() {
287 expect_fails_rule!(
288 factory,
289 r#"
290 fragment fragA on Dog { ...fragB, ...fragC }
291 fragment fragB on Dog { ...fragA }
292 fragment fragC on Dog { ...fragA }
293 { __typename }
294 "#,
295 );
296 }
297
298 #[test]
299 fn no_spreading_itself_deeply_two_paths_alt_traversal_order() {
300 expect_fails_rule!(
301 factory,
302 r#"
303 fragment fragA on Dog { ...fragC }
304 fragment fragB on Dog { ...fragC }
305 fragment fragC on Dog { ...fragA, ...fragB }
306 { __typename }
307 "#,
308 );
309 }
310
311 #[test]
312 fn no_spreading_itself_deeply_and_immediately() {
313 expect_fails_rule!(
314 factory,
315 r#"
316 fragment fragA on Dog { ...fragB }
317 fragment fragB on Dog { ...fragB, ...fragC }
318 fragment fragC on Dog { ...fragA, ...fragB }
319 { __typename }
320 "#,
321 );
322 }
323}