Skip to main content

graphql_tools/validation/rules/
no_unused_variables.rs

1use std::collections::{HashMap, HashSet};
2
3use super::ValidationRule;
4use crate::ast::{AstNodeWithName, OperationVisitor, OperationVisitorContext};
5use crate::static_graphql::query::{self, OperationDefinition};
6use crate::validation::utils::{ValidationError, ValidationErrorContext};
7
8/// No unused fragments
9///
10/// A GraphQL operation is only valid if all variables defined by an operation
11/// are used, either directly or within a spread fragment.
12///
13/// See https://spec.graphql.org/draft/#sec-All-Variables-Used
14pub struct NoUnusedVariables<'doc> {
15    current_scope: Option<NoUnusedVariablesScope<'doc>>,
16    defined_variables: HashMap<Option<&'doc str>, HashSet<&'doc str>>,
17    used_variables: HashMap<NoUnusedVariablesScope<'doc>, Vec<&'doc str>>,
18    spreads: HashMap<NoUnusedVariablesScope<'doc>, Vec<&'doc str>>,
19}
20
21impl Default for NoUnusedVariables<'_> {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl NoUnusedVariables<'_> {
28    pub fn new() -> Self {
29        Self {
30            current_scope: None,
31            defined_variables: HashMap::new(),
32            used_variables: HashMap::new(),
33            spreads: HashMap::new(),
34        }
35    }
36}
37
38impl<'doc> NoUnusedVariables<'doc> {
39    fn find_used_vars(
40        &self,
41        from: &NoUnusedVariablesScope<'doc>,
42        defined: &HashSet<&str>,
43        used: &mut HashSet<&'doc str>,
44        visited: &mut HashSet<NoUnusedVariablesScope<'doc>>,
45    ) {
46        if visited.contains(from) {
47            return;
48        }
49
50        visited.insert(from.clone());
51
52        if let Some(used_vars) = self.used_variables.get(from) {
53            for var in used_vars {
54                if defined.contains(var) {
55                    used.insert(var);
56                }
57            }
58        }
59
60        if let Some(spreads) = self.spreads.get(from) {
61            for spread in spreads {
62                self.find_used_vars(
63                    &NoUnusedVariablesScope::Fragment(spread),
64                    defined,
65                    used,
66                    visited,
67                );
68            }
69        }
70    }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Hash)]
74pub enum NoUnusedVariablesScope<'doc> {
75    Operation(Option<&'doc str>),
76    Fragment(&'doc str),
77}
78
79impl<'doc> OperationVisitor<'doc, ValidationErrorContext> for NoUnusedVariables<'doc> {
80    fn enter_operation_definition(
81        &mut self,
82        _: &mut OperationVisitorContext,
83        _: &mut ValidationErrorContext,
84        operation_definition: &'doc OperationDefinition,
85    ) {
86        let op_name = operation_definition.node_name();
87        self.current_scope = Some(NoUnusedVariablesScope::Operation(op_name));
88        self.defined_variables.insert(op_name, HashSet::new());
89    }
90
91    fn enter_fragment_definition(
92        &mut self,
93        _: &mut OperationVisitorContext,
94        _: &mut ValidationErrorContext,
95        fragment_definition: &'doc query::FragmentDefinition,
96    ) {
97        self.current_scope = Some(NoUnusedVariablesScope::Fragment(&fragment_definition.name));
98    }
99
100    fn enter_fragment_spread(
101        &mut self,
102        _: &mut OperationVisitorContext,
103        _: &mut ValidationErrorContext,
104        fragment_spread: &'doc query::FragmentSpread,
105    ) {
106        if let Some(scope) = &self.current_scope {
107            self.spreads
108                .entry(scope.clone())
109                .or_default()
110                .push(&fragment_spread.fragment_name);
111        }
112    }
113
114    fn enter_variable_definition(
115        &mut self,
116        _: &mut OperationVisitorContext,
117        _: &mut ValidationErrorContext,
118        variable_definition: &'doc query::VariableDefinition,
119    ) {
120        if let Some(NoUnusedVariablesScope::Operation(ref name)) = self.current_scope {
121            if let Some(vars) = self.defined_variables.get_mut(name) {
122                vars.insert(&variable_definition.name);
123            }
124        }
125    }
126
127    fn enter_argument(
128        &mut self,
129        _: &mut OperationVisitorContext,
130        _: &mut ValidationErrorContext,
131        (_arg_name, arg_value): &'doc (String, query::Value),
132    ) {
133        if let Some(ref scope) = self.current_scope {
134            self.used_variables
135                .entry(scope.clone())
136                .or_default()
137                .append(&mut arg_value.variables_in_use());
138        }
139    }
140
141    fn leave_document(
142        &mut self,
143        _: &mut OperationVisitorContext,
144        user_context: &mut ValidationErrorContext,
145        _: &query::Document,
146    ) {
147        for (op_name, def_vars) in &self.defined_variables {
148            let mut used = HashSet::new();
149            let mut visited = HashSet::new();
150
151            self.find_used_vars(
152                &NoUnusedVariablesScope::Operation(*op_name),
153                def_vars,
154                &mut used,
155                &mut visited,
156            );
157
158            def_vars
159                .iter()
160                .filter(|var| !used.contains(*var))
161                .for_each(|var| {
162                    user_context.report_error(ValidationError {
163                        error_code: self.error_code(),
164                        message: error_message(var, op_name),
165                        locations: vec![],
166                    })
167                })
168        }
169    }
170}
171
172fn error_message(var_name: &str, op_name: &Option<&str>) -> String {
173    if let Some(op_name) = op_name {
174        format!(
175            r#"Variable "${}" is never used in operation "{}"."#,
176            var_name, op_name
177        )
178    } else {
179        format!(r#"Variable "${}" is never used."#, var_name)
180    }
181}
182
183impl ValidationRule for NoUnusedVariables<'_> {
184    fn error_code(&self) -> &'static str {
185        "NoUnusedVariables"
186    }
187
188    fn visitor<'doc>(&self) -> super::ValidationVisitor<'doc> {
189        Box::new(NoUnusedVariables::new())
190    }
191}
192
193#[test]
194fn use_all_variables() {
195    use crate::validation::test_utils::*;
196
197    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
198    let errors = test_operation_with_schema(
199        "query ($a: String, $b: String, $c: String) {
200        field(a: $a, b: $b, c: $c)
201      }",
202        TEST_SCHEMA,
203        &mut plan,
204    );
205
206    assert_eq!(get_messages(&errors).len(), 0);
207}
208
209#[test]
210fn use_all_variables_deeply() {
211    use crate::validation::test_utils::*;
212
213    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
214    let errors = test_operation_with_schema(
215        "query Foo($a: String, $b: String, $c: String) {
216      field(a: $a) {
217        field(b: $b) {
218          field(c: $c)
219        }
220      }
221    }
222  ",
223        TEST_SCHEMA,
224        &mut plan,
225    );
226
227    assert_eq!(get_messages(&errors).len(), 0);
228}
229
230#[test]
231fn use_all_variables_deeply_in_inline_fragments() {
232    use crate::validation::test_utils::*;
233
234    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
235    let errors = test_operation_with_schema(
236        " query Foo($a: String, $b: String, $c: String) {
237      ... on Type {
238        field(a: $a) {
239          field(b: $b) {
240            ... on Type {
241              field(c: $c)
242            }
243          }
244        }
245      }
246    }
247  ",
248        TEST_SCHEMA,
249        &mut plan,
250    );
251
252    assert_eq!(get_messages(&errors).len(), 0);
253}
254
255#[test]
256fn use_all_variables_in_fragments() {
257    use crate::validation::test_utils::*;
258
259    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
260    let errors = test_operation_with_schema(
261        "query Foo($a: String, $b: String, $c: String) {
262      ...FragA
263    }
264    fragment FragA on Type {
265      field(a: $a) {
266        ...FragB
267      }
268    }
269    fragment FragB on Type {
270      field(b: $b) {
271        ...FragC
272      }
273    }
274    fragment FragC on Type {
275      field(c: $c)
276    }",
277        TEST_SCHEMA,
278        &mut plan,
279    );
280
281    assert_eq!(get_messages(&errors).len(), 0);
282}
283
284#[test]
285fn variables_used_by_fragment_in_multiple_operations() {
286    use crate::validation::test_utils::*;
287
288    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
289    let errors = test_operation_with_schema(
290        "query Foo($a: String) {
291      ...FragA
292    }
293    query Bar($b: String) {
294      ...FragB
295    }
296    fragment FragA on Type {
297      field(a: $a)
298    }
299    fragment FragB on Type {
300      field(b: $b)
301    }",
302        TEST_SCHEMA,
303        &mut plan,
304    );
305
306    assert_eq!(get_messages(&errors).len(), 0);
307}
308
309#[test]
310fn variables_used_by_recursive_fragment() {
311    use crate::validation::test_utils::*;
312
313    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
314    let errors = test_operation_with_schema(
315        "query Foo($a: String) {
316      ...FragA
317    }
318    fragment FragA on Type {
319      field(a: $a) {
320        ...FragA
321      }
322    }",
323        TEST_SCHEMA,
324        &mut plan,
325    );
326
327    assert_eq!(get_messages(&errors).len(), 0);
328}
329
330#[test]
331fn variables_not_used() {
332    use crate::validation::test_utils::*;
333
334    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
335    let errors = test_operation_with_schema(
336        "query ($a: String, $b: String, $c: String) {
337          field(a: $a, b: $b)
338        }",
339        TEST_SCHEMA,
340        &mut plan,
341    );
342
343    let messages = get_messages(&errors);
344
345    assert_eq!(messages.len(), 1);
346    assert!(messages.contains(&&"Variable \"$c\" is never used.".to_owned()));
347}
348
349#[test]
350fn multiple_variables_not_used() {
351    use crate::validation::test_utils::*;
352
353    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
354    let errors = test_operation_with_schema(
355        "query Foo($a: String, $b: String, $c: String) {
356          field(b: $b)
357        }",
358        TEST_SCHEMA,
359        &mut plan,
360    );
361
362    let messages = get_messages(&errors);
363
364    assert_eq!(messages.len(), 2);
365    assert!(messages.contains(&&"Variable \"$a\" is never used in operation \"Foo\".".to_owned()));
366    assert!(messages.contains(&&"Variable \"$c\" is never used in operation \"Foo\".".to_owned()));
367}
368
369#[test]
370fn variables_not_used_in_fragments() {
371    use crate::validation::test_utils::*;
372
373    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
374    let errors = test_operation_with_schema(
375        "query Foo($a: String, $b: String, $c: String) {
376          ...FragA
377        }
378        fragment FragA on Type {
379          field(a: $a) {
380            ...FragB
381          }
382        }
383        fragment FragB on Type {
384          field(b: $b) {
385            ...FragC
386          }
387        }
388        fragment FragC on Type {
389          field
390        }",
391        TEST_SCHEMA,
392        &mut plan,
393    );
394
395    let messages = get_messages(&errors);
396
397    assert_eq!(messages.len(), 1);
398    assert!(messages.contains(&&"Variable \"$c\" is never used in operation \"Foo\".".to_owned()));
399}
400
401#[test]
402fn multiple_variables_not_used_in_fragments() {
403    use crate::validation::test_utils::*;
404
405    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
406    let errors = test_operation_with_schema(
407        "query Foo($a: String, $b: String, $c: String) {
408          ...FragA
409        }
410        fragment FragA on Type {
411          field {
412            ...FragB
413          }
414        }
415        fragment FragB on Type {
416          field(b: $b) {
417            ...FragC
418          }
419        }
420        fragment FragC on Type {
421          field
422        }",
423        TEST_SCHEMA,
424        &mut plan,
425    );
426
427    let messages = get_messages(&errors);
428
429    assert_eq!(messages.len(), 2);
430    assert!(messages.contains(&&"Variable \"$a\" is never used in operation \"Foo\".".to_owned()));
431    assert!(messages.contains(&&"Variable \"$c\" is never used in operation \"Foo\".".to_owned()));
432}
433
434#[test]
435fn variables_not_used_by_unreferences_fragment() {
436    use crate::validation::test_utils::*;
437
438    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
439    let errors = test_operation_with_schema(
440        "query Foo($b: String) {
441          ...FragA
442        }
443        fragment FragA on Type {
444          field(a: $a)
445        }
446        fragment FragB on Type {
447          field(b: $b)
448        }",
449        TEST_SCHEMA,
450        &mut plan,
451    );
452
453    let messages = get_messages(&errors);
454
455    assert_eq!(messages.len(), 1);
456    assert!(messages.contains(&&"Variable \"$b\" is never used in operation \"Foo\".".to_owned()));
457}
458
459#[test]
460fn variables_not_used_by_fragment_used_by_other_operation() {
461    use crate::validation::test_utils::*;
462
463    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
464    let errors = test_operation_with_schema(
465        "query Foo($b: String) {
466          ...FragA
467        }
468        query Bar($a: String) {
469          ...FragB
470        }
471        fragment FragA on Type {
472          field(a: $a)
473        }
474        fragment FragB on Type {
475          field(b: $b)
476        }",
477        TEST_SCHEMA,
478        &mut plan,
479    );
480
481    let messages = get_messages(&errors);
482
483    assert_eq!(messages.len(), 2);
484    assert!(messages.contains(&&"Variable \"$b\" is never used in operation \"Foo\".".to_owned()));
485    assert!(messages.contains(&&"Variable \"$a\" is never used in operation \"Bar\".".to_owned()));
486}
487
488#[test]
489fn should_also_check_directives_usage() {
490    use crate::validation::test_utils::*;
491
492    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
493    let errors = test_operation_with_schema(
494        "query foo($skip: Boolean!) {
495          field @skip(if: $skip)
496        }
497        ",
498        TEST_SCHEMA,
499        &mut plan,
500    );
501
502    let messages = get_messages(&errors);
503    assert_eq!(messages.len(), 0);
504}
505
506#[test]
507fn nested_variable_should_work_as_well() {
508    use crate::validation::test_utils::*;
509
510    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
511    let errors = test_operation_with_schema(
512        "query foo($t: Boolean!) {
513          field(boop: { test: $t})
514        }
515        ",
516        TEST_SCHEMA,
517        &mut plan,
518    );
519
520    let messages = get_messages(&errors);
521    assert_eq!(messages.len(), 0);
522}