rusty_gql/validation/rules/
no_unused_fragments.rs

1use std::collections::{HashMap, HashSet};
2
3use graphql_parser::{
4    query::{Definition, FragmentDefinition, FragmentSpread, OperationDefinition},
5    Pos,
6};
7
8use crate::validation::{
9    utils::Scope,
10    visitor::{ValidationContext, Visitor},
11};
12
13#[derive(Default)]
14pub struct NoUnusedFragment<'a> {
15    current_scope: Option<Scope<'a>>,
16    fragment_spreads: HashMap<Scope<'a>, Vec<&'a str>>,
17    fragment_definitions: HashSet<(&'a str, Pos)>,
18}
19
20impl<'a> NoUnusedFragment<'a> {
21    fn get_reachable_fragments(&self, from: &Scope<'a>, result: &mut HashSet<&'a str>) {
22        if let Scope::Fragment(name) = from {
23            if result.contains(name) {
24                return;
25            } else {
26                result.insert(name);
27            }
28        }
29
30        if let Some(spreads) = self.fragment_spreads.get(from) {
31            for spread in spreads {
32                self.get_reachable_fragments(&Scope::Fragment(spread), result)
33            }
34        }
35    }
36}
37
38impl<'a> Visitor<'a> for NoUnusedFragment<'a> {
39    fn exit_document(
40        &mut self,
41        ctx: &mut ValidationContext<'a>,
42        doc: &'a graphql_parser::query::Document<'a, String>,
43    ) {
44        let mut reachable = HashSet::new();
45
46        for definition in &doc.definitions {
47            if let Definition::Operation(_) = definition {
48                self.get_reachable_fragments(&Scope::Operation(None), &mut reachable)
49            }
50        }
51
52        for (name, pos) in &self.fragment_definitions {
53            if !reachable.contains(name) {
54                ctx.add_error(format!("{} is unused fragment.", name), vec![*pos])
55            }
56        }
57    }
58
59    fn enter_operation_definition(
60        &mut self,
61        _ctx: &mut ValidationContext<'a>,
62        name: Option<&'a str>,
63        _operation_definition: &'a OperationDefinition<'a, String>,
64    ) {
65        self.current_scope = Some(Scope::Operation(name));
66    }
67
68    fn enter_fragment_definition(
69        &mut self,
70        _ctx: &mut ValidationContext,
71        name: &'a str,
72        fragment_definition: &'a FragmentDefinition<'a, String>,
73    ) {
74        self.current_scope = Some(Scope::Fragment(name));
75        self.fragment_definitions
76            .insert((name, fragment_definition.position));
77    }
78
79    fn enter_fragment_spread(
80        &mut self,
81        _ctx: &mut ValidationContext,
82        fragment_spread: &'a FragmentSpread<'a, String>,
83    ) {
84        if let Some(scope) = &self.current_scope {
85            self.fragment_spreads
86                .entry(*scope)
87                .or_insert_with(Vec::new)
88                .push(&fragment_spread.fragment_name)
89        }
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use crate::{check_fails_rule, check_passes_rule};
96
97    use super::*;
98
99    fn factory<'a>() -> NoUnusedFragment<'a> {
100        NoUnusedFragment::default()
101    }
102
103    #[test]
104    fn all_fragment_used() {
105        let query_doc = r#"
106        {
107            hero {
108                ...Frag1
109                ... on Human {
110                    ...Frag2
111                }
112
113            }
114        }
115        fragment Frag1 on Human {
116            name
117            ...Frag3
118        }
119        fragment Frag2 on Human {
120            name
121        }
122        fragment Frag3 on Human {
123            name
124        }
125        "#;
126        check_passes_rule!(query_doc, factory);
127    }
128
129    #[test]
130    fn with_unused_fragment() {
131        let query_doc = r#"
132        {
133            hero {
134                ...Frag1
135                ... on Human {
136                    ...Frag2
137                }
138
139            }
140        }
141        fragment Frag1 on Human {
142            name
143            ...Frag3
144        }
145        fragment Frag2 on Human {
146            name
147        }
148        fragment Frag3 on Human {
149            name
150        }
151        fragment UnusedFrag1 on Human {
152            name
153        }
154        "#;
155        check_fails_rule!(query_doc, factory);
156    }
157
158    #[test]
159    fn with_unused_fragment_ref_cycle() {
160        let query_doc = r#"
161        {
162            hero {
163                ...Frag1
164                ... on Human {
165                    ...Frag2
166                }
167
168            }
169        }
170        fragment Frag1 on Human {
171            name
172            ...Frag3
173        }
174        fragment Frag2 on Human {
175            name
176        }
177        fragment Frag3 on Human {
178            name
179        }
180        fragment UnusedFrag1 on Human {
181            name
182            ...UnusedFrag2
183        }
184        fragment UnusedFrag2 on Human {
185            name
186            ...UnusedFrag1
187        }
188        "#;
189        check_fails_rule!(query_doc, factory);
190    }
191
192    #[test]
193    fn with_unknown_and_unused_fragments() {
194        let query_doc = r#"
195        {
196            hero {
197                ...Frag1
198
199            }
200        }
201        fragment UnusedFrag1 on Human {
202            name
203        }
204        "#;
205        check_fails_rule!(query_doc, factory);
206    }
207}