async_graphql/validation/rules/
no_fragment_cycles.rs

1use 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}