Skip to main content

graphql_tools/validation/rules/
no_unused_fragments.rs

1use std::collections::{HashMap, HashSet, VecDeque};
2
3use super::ValidationRule;
4use crate::ast::{OperationVisitor, OperationVisitorContext};
5use crate::static_graphql::query::*;
6use crate::validation::utils::{ValidationError, ValidationErrorContext};
7
8/// No unused fragments
9///
10/// A GraphQL document is only valid if all fragment definitions are spread
11/// within operations, or spread within other fragments spread within operations.
12///
13/// See https://spec.graphql.org/draft/#sec-Fragments-Must-Be-Used
14pub struct NoUnusedFragments<'doc> {
15    fragments_in_use: Vec<&'doc str>,
16    current_fragment_spreads: Vec<&'doc str>,
17    current_fragment: Option<&'doc str>,
18    fragment_spreads: HashMap<&'doc str, Vec<&'doc str>>,
19}
20
21impl<'doc> OperationVisitor<'doc, ValidationErrorContext> for NoUnusedFragments<'doc> {
22    fn enter_fragment_definition(
23        &mut self,
24        _: &mut OperationVisitorContext,
25        _: &mut ValidationErrorContext,
26        fragment: &'doc FragmentDefinition,
27    ) {
28        self.current_fragment = Some(fragment.name.as_str());
29        self.current_fragment_spreads = Vec::new();
30    }
31
32    fn leave_fragment_definition(
33        &mut self,
34        _: &mut OperationVisitorContext,
35        _: &mut ValidationErrorContext,
36        _: &FragmentDefinition,
37    ) {
38        if let Some(name) = self.current_fragment.take() {
39            self.fragment_spreads
40                .insert(name, std::mem::take(&mut self.current_fragment_spreads));
41        }
42    }
43
44    fn enter_fragment_spread(
45        &mut self,
46        _: &mut OperationVisitorContext,
47        _: &mut ValidationErrorContext,
48        fragment_spread: &'doc FragmentSpread,
49    ) {
50        let name = fragment_spread.fragment_name.as_str();
51        if self.current_fragment.is_some() {
52            self.current_fragment_spreads.push(name);
53        } else {
54            self.fragments_in_use.push(name);
55        }
56    }
57
58    fn leave_document(
59        &mut self,
60        visitor_context: &mut OperationVisitorContext,
61        user_context: &mut ValidationErrorContext,
62        _document: &Document,
63    ) {
64        let mut reachable: HashSet<&str> = HashSet::new();
65        let mut queue: VecDeque<&str> = self.fragments_in_use.iter().copied().collect();
66
67        while let Some(frag) = queue.pop_front() {
68            if !reachable.insert(frag) {
69                continue;
70            }
71
72            let Some(spreads) = self.fragment_spreads.get(frag) else {
73                continue;
74            };
75
76            for spread in spreads {
77                if !reachable.contains(spread) {
78                    queue.push_back(spread);
79                }
80            }
81        }
82
83        visitor_context
84            .known_fragments
85            .keys()
86            .filter(|fragment_name| !reachable.contains(*fragment_name))
87            .for_each(|unused_fragment_name| {
88                user_context.report_error(ValidationError {
89                    error_code: self.error_code(),
90                    locations: vec![],
91                    message: format!("Fragment \"{}\" is never used.", unused_fragment_name),
92                });
93            });
94    }
95}
96
97impl Default for NoUnusedFragments<'_> {
98    fn default() -> Self {
99        Self::new()
100    }
101}
102
103impl NoUnusedFragments<'_> {
104    pub fn new() -> Self {
105        NoUnusedFragments {
106            fragments_in_use: Vec::new(),
107            current_fragment_spreads: Vec::new(),
108            current_fragment: None,
109            fragment_spreads: HashMap::new(),
110        }
111    }
112}
113
114impl ValidationRule for NoUnusedFragments<'_> {
115    fn error_code(&self) -> &'static str {
116        "NoUnusedFragments"
117    }
118
119    fn visitor<'doc>(&self) -> super::ValidationVisitor<'doc> {
120        Box::new(NoUnusedFragments::new())
121    }
122}
123
124#[test]
125fn all_fragment_names_are_used() {
126    use crate::validation::test_utils::*;
127
128    let mut plan = create_plan_from_rule(Box::new(NoUnusedFragments::new()));
129    let errors = test_operation_with_schema(
130        "{
131          human(id: 4) {
132            ...HumanFields1
133            ... on Human {
134              ...HumanFields2
135            }
136          }
137        }
138        fragment HumanFields1 on Human {
139          name
140          ...HumanFields3
141        }
142        fragment HumanFields2 on Human {
143          name
144        }
145        fragment HumanFields3 on Human {
146          name
147        }",
148        TEST_SCHEMA,
149        &mut plan,
150    );
151
152    assert_eq!(get_messages(&errors).len(), 0);
153}
154
155#[test]
156fn all_fragment_names_are_used_by_multiple_operations() {
157    use crate::validation::test_utils::*;
158
159    let mut plan = create_plan_from_rule(Box::new(NoUnusedFragments::new()));
160    let errors = test_operation_with_schema(
161        "query Foo {
162          human(id: 4) {
163            ...HumanFields1
164          }
165        }
166        query Bar {
167          human(id: 4) {
168            ...HumanFields2
169          }
170        }
171        fragment HumanFields1 on Human {
172          name
173          ...HumanFields3
174        }
175        fragment HumanFields2 on Human {
176          name
177        }
178        fragment HumanFields3 on Human {
179          name
180        }
181  ",
182        TEST_SCHEMA,
183        &mut plan,
184    );
185
186    assert_eq!(get_messages(&errors).len(), 0);
187}
188
189#[test]
190fn contains_unknown_fragments() {
191    use crate::validation::test_utils::*;
192
193    let mut plan = create_plan_from_rule(Box::new(NoUnusedFragments::new()));
194    let errors = test_operation_with_schema(
195        "query Foo {
196          human(id: 4) {
197            ...HumanFields1
198          }
199        }
200        query Bar {
201          human(id: 4) {
202            ...HumanFields2
203          }
204        }
205        fragment HumanFields1 on Human {
206          name
207          ...HumanFields3
208        }
209        fragment HumanFields2 on Human {
210          name
211        }
212        fragment HumanFields3 on Human {
213          name
214        }
215        fragment Unused1 on Human {
216          name
217        }
218        fragment Unused2 on Human {
219          name
220        }
221  ",
222        TEST_SCHEMA,
223        &mut plan,
224    );
225
226    let messages = get_messages(&errors);
227    assert_eq!(messages.len(), 2);
228}
229
230#[test]
231fn contains_unknown_fragments_with_ref_cycle() {
232    use crate::validation::test_utils::*;
233
234    let mut plan = create_plan_from_rule(Box::new(NoUnusedFragments::new()));
235    let errors = test_operation_with_schema(
236        "query Foo {
237          human(id: 4) {
238            ...HumanFields1
239          }
240        }
241        query Bar {
242          human(id: 4) {
243            ...HumanFields2
244          }
245        }
246        fragment HumanFields1 on Human {
247          name
248          ...HumanFields3
249        }
250        fragment HumanFields2 on Human {
251          name
252        }
253        fragment HumanFields3 on Human {
254          name
255        }
256        fragment Unused1 on Human {
257          name
258          ...Unused2
259        }
260        fragment Unused2 on Human {
261          name
262          ...Unused1
263        }
264  ",
265        TEST_SCHEMA,
266        &mut plan,
267    );
268
269    let messages = get_messages(&errors);
270    assert_eq!(messages.len(), 2);
271    assert!(messages.contains(&&"Fragment \"Unused1\" is never used.".to_owned()));
272    assert!(messages.contains(&&"Fragment \"Unused2\" is never used.".to_owned()));
273}
274
275#[test]
276fn contains_unknown_and_undef_fragments() {
277    use crate::validation::test_utils::*;
278
279    let mut plan = create_plan_from_rule(Box::new(NoUnusedFragments::new()));
280    let errors = test_operation_with_schema(
281        "query Foo {
282          human(id: 4) {
283            ...bar
284          }
285        }
286        fragment foo on Human {
287          name
288        }
289  ",
290        TEST_SCHEMA,
291        &mut plan,
292    );
293
294    let messages = get_messages(&errors);
295    assert_eq!(messages.len(), 1);
296    assert_eq!(messages, vec!["Fragment \"foo\" is never used.",]);
297}