graphql_tools/validation/rules/
no_unused_variables.rs

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