Skip to main content

graphql_tools/validation/rules/
no_fragments_cycle.rs

1use super::ValidationRule;
2use crate::ast::ext::AstNodeWithName;
3use crate::ast::{OperationVisitor, OperationVisitorContext};
4use crate::static_graphql::query::{FragmentDefinition, FragmentSpread};
5use crate::validation::utils::{ValidationError, ValidationErrorContext};
6use std::collections::{HashMap, HashSet};
7
8/// No fragment cycles
9///
10/// The graph of fragment spreads must not form any cycles including spreading itself.
11/// Otherwise an operation could infinitely spread or infinitely execute on cycles in the underlying data.
12///
13/// https://spec.graphql.org/draft/#sec-Fragment-spreads-must-not-form-cycles
14pub struct NoFragmentsCycle {
15    visited_fragments: HashSet<String>,
16}
17
18impl Default for NoFragmentsCycle {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl NoFragmentsCycle {
25    pub fn new() -> Self {
26        Self {
27            visited_fragments: HashSet::new(),
28        }
29    }
30
31    /// This does a straight-forward DFS to find cycles.
32    /// It does not terminate when a cycle was found but continues to explore
33    /// the graph to find all possible cycles.
34    fn detect_cycles<'doc>(
35        &mut self,
36        root: &'doc FragmentDefinition,
37        spread_paths: &mut Vec<&'doc FragmentSpread>,
38        spread_path_index_by_name: &mut HashMap<&'doc str, usize>,
39        known_fragments: &'doc HashMap<&'doc str, &'doc FragmentDefinition>,
40        error_context: &mut ValidationErrorContext,
41    ) {
42        // iterative DFS to avoid stack overflow on deep acyclic chains.
43        // Frame::EnterSpread pushes one spread onto spread_paths then either recurses into the
44        // target fragment or reports a cycle, Frame::PopSpread undoes that push on the way back.
45        enum Frame<'doc> {
46            EnterFragment(&'doc FragmentDefinition),
47            ExitFragment(&'doc str), // removes fragment from spread_path_index_by_name
48            EnterSpread(&'doc FragmentSpread), // push spread onto spread_paths
49            PopSpread,               // pop spread from spread_paths
50        }
51
52        let mut stack: Vec<Frame<'doc>> = vec![Frame::EnterFragment(root)];
53
54        while let Some(frame) = stack.pop() {
55            match frame {
56                Frame::ExitFragment(name) => {
57                    spread_path_index_by_name.remove(name);
58                }
59                Frame::PopSpread => {
60                    spread_paths.pop();
61                }
62                Frame::EnterFragment(fragment) => {
63                    if self.visited_fragments.contains(&fragment.name) {
64                        continue;
65                    }
66                    self.visited_fragments.insert(fragment.name.clone());
67
68                    let spread_nodes = fragment.selection_set.get_recursive_fragment_spreads();
69                    if spread_nodes.is_empty() {
70                        continue;
71                    }
72
73                    spread_path_index_by_name.insert(fragment.name.as_str(), spread_paths.len());
74                    stack.push(Frame::ExitFragment(fragment.name.as_str()));
75
76                    // push spreads in reverse so they execute left-to-right
77                    for spread_node in spread_nodes.into_iter().rev() {
78                        stack.push(Frame::EnterSpread(spread_node));
79                    }
80                }
81                Frame::EnterSpread(spread_node) => {
82                    let spread_name = &spread_node.fragment_name;
83
84                    match spread_path_index_by_name.get(spread_name.as_str()) {
85                        None => {
86                            if let Some(spread_def) = known_fragments.get(spread_name.as_str()) {
87                                // descend into the target; PopSpread restores spread_paths once the
88                                // whole child subtree is explored.
89                                spread_paths.push(spread_node);
90                                stack.push(Frame::PopSpread);
91                                stack.push(Frame::EnterFragment(spread_def));
92                            }
93                        }
94                        Some(cycle_index) => {
95                            // include the closing spread so the reported path ends where it loops.
96                            spread_paths.push(spread_node);
97                            let cycle_path = &spread_paths[*cycle_index..];
98                            let via_path = match cycle_path.len() {
99                                0 => vec![],
100                                _ => cycle_path[0..cycle_path.len() - 1]
101                                    .iter()
102                                    .map(|s| {
103                                        format!(
104                                            "\"{}\"",
105                                            s.node_name()
106                                                .expect("fragment spread must have a name")
107                                        )
108                                    })
109                                    .collect::<Vec<String>>(),
110                            };
111
112                            error_context.report_error(ValidationError {
113                                error_code: self.error_code(),
114                                locations: cycle_path.iter().map(|f| f.position).collect(),
115                                message: match via_path.len() {
116                                    0 => format!(
117                                        "Cannot spread fragment \"{}\" within itself.",
118                                        spread_name
119                                    ),
120                                    _ => format!(
121                                        "Cannot spread fragment \"{}\" within itself via {}.",
122                                        spread_name,
123                                        via_path.join(", ")
124                                    ),
125                                },
126                            });
127                            spread_paths.pop();
128                        }
129                    }
130                }
131            }
132        }
133    }
134}
135
136impl<'doc> OperationVisitor<'doc, ValidationErrorContext> for NoFragmentsCycle {
137    fn enter_fragment_definition(
138        &mut self,
139        visitor_context: &mut OperationVisitorContext,
140        user_context: &mut ValidationErrorContext,
141        fragment: &FragmentDefinition,
142    ) {
143        let mut spread_paths: Vec<&FragmentSpread> = vec![];
144        let mut spread_path_index_by_name: HashMap<&str, usize> = HashMap::new();
145
146        self.detect_cycles(
147            fragment,
148            &mut spread_paths,
149            &mut spread_path_index_by_name,
150            &visitor_context.known_fragments,
151            user_context,
152        );
153    }
154}
155
156impl ValidationRule for NoFragmentsCycle {
157    fn error_code(&self) -> &'static str {
158        "NoFragmentsCycle"
159    }
160
161    fn visitor<'doc>(&self) -> super::ValidationVisitor<'doc> {
162        Box::new(NoFragmentsCycle::new())
163    }
164}
165
166#[test]
167fn single_reference_is_valid() {
168    use crate::validation::test_utils::*;
169
170    let mut plan = create_plan_from_rule(Box::new(NoFragmentsCycle::new()));
171    let errors = test_operation_with_schema(
172        "fragment fragA on Dog { ...fragB }
173		fragment fragB on Dog { name }",
174        TEST_SCHEMA,
175        &mut plan,
176    );
177
178    let mes = get_messages(&errors).len();
179    assert_eq!(mes, 0);
180}
181
182#[test]
183fn spreading_twice_is_not_circular() {
184    use crate::validation::test_utils::*;
185
186    let mut plan = create_plan_from_rule(Box::new(NoFragmentsCycle::new()));
187    let errors = test_operation_with_schema(
188        "fragment fragA on Dog { ...fragB, ...fragB }
189		fragment fragB on Dog { name }",
190        TEST_SCHEMA,
191        &mut plan,
192    );
193
194    let mes = get_messages(&errors).len();
195    assert_eq!(mes, 0);
196}
197
198#[test]
199fn spreading_twice_indirectly_is_not_circular() {
200    use crate::validation::test_utils::*;
201
202    let mut plan = create_plan_from_rule(Box::new(NoFragmentsCycle::new()));
203    let errors = test_operation_with_schema(
204        "fragment fragA on Dog { ...fragB, ...fragC }
205		fragment fragB on Dog { ...fragC }
206		fragment fragC on Dog { name }",
207        TEST_SCHEMA,
208        &mut plan,
209    );
210
211    let mes = get_messages(&errors).len();
212    assert_eq!(mes, 0);
213}
214
215#[test]
216fn double_spread_within_abstract_types() {
217    use crate::validation::test_utils::*;
218
219    let mut plan = create_plan_from_rule(Box::new(NoFragmentsCycle::new()));
220    let errors = test_operation_with_schema(
221        "fragment nameFragment on Pet {
222			... on Dog { name }
223			... on Cat { name }
224		      }
225
226		      fragment spreadsInAnon on Pet {
227			... on Dog { ...nameFragment }
228			... on Cat { ...nameFragment }
229		      }",
230        TEST_SCHEMA,
231        &mut plan,
232    );
233
234    let mes = get_messages(&errors).len();
235    assert_eq!(mes, 0);
236}
237
238#[test]
239fn does_not_false_positive_on_unknown_fragment() {
240    use crate::validation::test_utils::*;
241
242    let mut plan = create_plan_from_rule(Box::new(NoFragmentsCycle::new()));
243    let errors = test_operation_with_schema(
244        "fragment nameFragment on Pet {
245			...UnknownFragment
246		      }",
247        TEST_SCHEMA,
248        &mut plan,
249    );
250
251    let mes = get_messages(&errors).len();
252    assert_eq!(mes, 0);
253}
254
255#[test]
256fn spreading_recursively_within_field_fails() {
257    use crate::validation::test_utils::*;
258
259    let mut plan = create_plan_from_rule(Box::new(NoFragmentsCycle::new()));
260    let errors = test_operation_with_schema(
261        "fragment fragA on Human { relatives { ...fragA } }",
262        TEST_SCHEMA,
263        &mut plan,
264    );
265
266    let mes = get_messages(&errors);
267    assert_eq!(mes.len(), 1);
268    assert_eq!(mes, vec!["Cannot spread fragment \"fragA\" within itself."]);
269}
270
271#[test]
272fn no_spreading_itself_directly() {
273    use crate::validation::test_utils::*;
274
275    let mut plan = create_plan_from_rule(Box::new(NoFragmentsCycle::new()));
276    let errors = test_operation_with_schema(
277        "
278        fragment fragA on Dog { ...fragA }",
279        TEST_SCHEMA,
280        &mut plan,
281    );
282
283    let mes = get_messages(&errors);
284    assert_eq!(mes.len(), 1);
285    assert_eq!(mes, vec!["Cannot spread fragment \"fragA\" within itself."]);
286}
287
288#[test]
289fn no_spreading_itself_directly_within_inline_fragment() {
290    use crate::validation::test_utils::*;
291
292    let mut plan = create_plan_from_rule(Box::new(NoFragmentsCycle::new()));
293    let errors = test_operation_with_schema(
294        "fragment fragA on Pet {
295			... on Dog {
296			  ...fragA
297			}
298		      }",
299        TEST_SCHEMA,
300        &mut plan,
301    );
302
303    let mes = get_messages(&errors);
304    assert_eq!(mes.len(), 1);
305    assert_eq!(mes, vec!["Cannot spread fragment \"fragA\" within itself."]);
306}
307
308#[test]
309fn no_spreading_itself_indirectly() {
310    use crate::validation::test_utils::*;
311
312    let mut plan = create_plan_from_rule(Box::new(NoFragmentsCycle::new()));
313    let errors = test_operation_with_schema(
314        "fragment fragA on Dog { ...fragB }
315		fragment fragB on Dog { ...fragA }",
316        TEST_SCHEMA,
317        &mut plan,
318    );
319
320    let mes = get_messages(&errors);
321    assert_eq!(mes.len(), 1);
322    assert_eq!(
323        mes,
324        vec!["Cannot spread fragment \"fragA\" within itself via \"fragB\"."]
325    );
326}
327
328#[test]
329fn no_spreading_itself_indirectly_reports_opposite_order() {
330    use crate::validation::test_utils::*;
331
332    let mut plan = create_plan_from_rule(Box::new(NoFragmentsCycle::new()));
333    let errors = test_operation_with_schema(
334        "fragment fragB on Dog { ...fragA }
335		fragment fragA on Dog { ...fragB }",
336        TEST_SCHEMA,
337        &mut plan,
338    );
339
340    let mes = get_messages(&errors);
341    assert_eq!(mes.len(), 1);
342    assert_eq!(
343        mes,
344        vec!["Cannot spread fragment \"fragB\" within itself via \"fragA\"."]
345    );
346}
347
348#[test]
349fn no_spreading_itself_indirectly_within_inline_fragment() {
350    use crate::validation::test_utils::*;
351
352    let mut plan = create_plan_from_rule(Box::new(NoFragmentsCycle::new()));
353    let errors = test_operation_with_schema(
354        "fragment fragA on Pet {
355			... on Dog {
356			  ...fragB
357			}
358		      }
359		      fragment fragB on Pet {
360			... on Dog {
361			  ...fragA
362			}
363		      }",
364        TEST_SCHEMA,
365        &mut plan,
366    );
367
368    let mes = get_messages(&errors);
369    assert_eq!(mes.len(), 1);
370    assert_eq!(
371        mes,
372        vec!["Cannot spread fragment \"fragA\" within itself via \"fragB\"."]
373    );
374}
375
376#[test]
377fn no_spreading_itself_deeply() {
378    use crate::validation::test_utils::*;
379
380    let mut plan = create_plan_from_rule(Box::new(NoFragmentsCycle::new()));
381    let errors = test_operation_with_schema(
382        "fragment fragA on Dog { ...fragB }
383        fragment fragB on Dog { ...fragC }
384        fragment fragC on Dog { ...fragO }
385        fragment fragX on Dog { ...fragY }
386        fragment fragY on Dog { ...fragZ }
387        fragment fragZ on Dog { ...fragO }
388        fragment fragO on Dog { ...fragP }
389        fragment fragP on Dog { ...fragA, ...fragX }",
390        TEST_SCHEMA,
391        &mut plan,
392    );
393
394    let mes = get_messages(&errors);
395    assert_eq!(mes.len(), 2);
396    assert_eq!(
397        mes,
398        vec![
399            "Cannot spread fragment \"fragA\" within itself via \"fragB\", \"fragC\", \"fragO\", \"fragP\".",
400            "Cannot spread fragment \"fragO\" within itself via \"fragP\", \"fragX\", \"fragY\", \"fragZ\".",
401        ]
402    );
403}
404
405#[test]
406fn no_spreading_itself_deeply_two_paths() {
407    use crate::validation::test_utils::*;
408
409    let mut plan = create_plan_from_rule(Box::new(NoFragmentsCycle::new()));
410    let errors = test_operation_with_schema(
411        "fragment fragA on Dog { ...fragB, ...fragC }
412	fragment fragB on Dog { ...fragA }
413	fragment fragC on Dog { ...fragA }",
414        TEST_SCHEMA,
415        &mut plan,
416    );
417
418    let mes = get_messages(&errors);
419    assert_eq!(mes.len(), 2);
420    assert_eq!(
421        mes,
422        vec![
423            "Cannot spread fragment \"fragA\" within itself via \"fragB\".",
424            "Cannot spread fragment \"fragA\" within itself via \"fragC\".",
425        ]
426    );
427}
428
429#[test]
430fn no_spreading_itself_deeply_two_paths_alt_traverse_order() {
431    use crate::validation::test_utils::*;
432
433    let mut plan = create_plan_from_rule(Box::new(NoFragmentsCycle::new()));
434    let errors = test_operation_with_schema(
435        "
436        fragment fragA on Dog { ...fragC }
437        fragment fragB on Dog { ...fragC }
438        fragment fragC on Dog { ...fragA, ...fragB }
439        ",
440        TEST_SCHEMA,
441        &mut plan,
442    );
443
444    let mes = get_messages(&errors);
445    assert_eq!(mes.len(), 2);
446    assert_eq!(
447        mes,
448        vec![
449            "Cannot spread fragment \"fragA\" within itself via \"fragC\".",
450            "Cannot spread fragment \"fragC\" within itself via \"fragB\".",
451        ]
452    );
453}
454
455#[test]
456fn acyclic_fragment_chain_does_not_overflow_stack() {
457    use crate::validation::test_utils::*;
458
459    let mut plan = create_plan_from_rule(Box::new(NoFragmentsCycle::new()));
460
461    let n = 10_000;
462    let mut doc = String::from("");
463    for i in 1..n {
464        doc.push_str(&format!("fragment F{i} on Dog {{ ...F{} }}\n", i + 1));
465    }
466    doc.push_str(&format!("fragment F{n} on Dog {{ name }}\n"));
467
468    // must return without overflowing the stack
469    let errors = test_operation_with_schema(&doc, TEST_SCHEMA, &mut plan);
470    let mes = get_messages(&errors);
471    assert_eq!(mes.len(), 0);
472}
473
474#[test]
475fn no_spreading_itself_deeply_and_immediately() {
476    use crate::validation::test_utils::*;
477
478    let mut plan = create_plan_from_rule(Box::new(NoFragmentsCycle::new()));
479    let errors = test_operation_with_schema(
480        "
481          fragment fragA on Dog { ...fragB }
482		      fragment fragB on Dog { ...fragB, ...fragC }
483		      fragment fragC on Dog { ...fragA, ...fragB }
484        ",
485        TEST_SCHEMA,
486        &mut plan,
487    );
488
489    let mes = get_messages(&errors);
490    assert_eq!(mes.len(), 3);
491    assert_eq!(
492        mes,
493        vec![
494            "Cannot spread fragment \"fragB\" within itself.",
495            "Cannot spread fragment \"fragA\" within itself via \"fragB\", \"fragC\".",
496            "Cannot spread fragment \"fragB\" within itself via \"fragC\".",
497        ]
498    );
499}