Skip to main content

graphql_tools/validation/rules/
no_unused_variables.rs

1use std::collections::{HashMap, HashSet};
2
3use super::ValidationRule;
4use crate::ast::{visit_document, 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<'a> {
15    current_scope: Option<NoUnusedVariablesScope<'a>>,
16    defined_variables: HashMap<Option<&'a str>, HashSet<&'a str>>,
17    used_variables: HashMap<NoUnusedVariablesScope<'a>, Vec<&'a str>>,
18    spreads: HashMap<NoUnusedVariablesScope<'a>, Vec<&'a str>>,
19}
20
21impl<'a> Default for NoUnusedVariables<'a> {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl<'a> NoUnusedVariables<'a> {
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<'a> NoUnusedVariables<'a> {
39    fn find_used_vars(
40        &self,
41        from: &NoUnusedVariablesScope<'a>,
42        defined: &HashSet<&str>,
43        used: &mut HashSet<&'a str>,
44        visited: &mut HashSet<NoUnusedVariablesScope<'a>>,
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<'a> {
75    Operation(Option<&'a str>),
76    Fragment(&'a str),
77}
78
79impl<'a> OperationVisitor<'a, ValidationErrorContext> for NoUnusedVariables<'a> {
80    fn enter_operation_definition(
81        &mut self,
82        _: &mut OperationVisitorContext,
83        _: &mut ValidationErrorContext,
84        operation_definition: &'a 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: &'a 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: &'a 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: &'a 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): &'a (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<'n> ValidationRule for NoUnusedVariables<'n> {
184    fn error_code<'a>(&self) -> &'a str {
185        "NoUnusedVariables"
186    }
187
188    fn validate(
189        &self,
190        ctx: &mut OperationVisitorContext,
191        error_collector: &mut ValidationErrorContext,
192    ) {
193        visit_document(
194            &mut NoUnusedVariables::new(),
195            ctx.operation,
196            ctx,
197            error_collector,
198        );
199    }
200}
201
202#[test]
203fn use_all_variables() {
204    use crate::validation::test_utils::*;
205
206    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
207    let errors = test_operation_with_schema(
208        "query ($a: String, $b: String, $c: String) {
209        field(a: $a, b: $b, c: $c)
210      }",
211        TEST_SCHEMA,
212        &mut plan,
213    );
214
215    assert_eq!(get_messages(&errors).len(), 0);
216}
217
218#[test]
219fn use_all_variables_deeply() {
220    use crate::validation::test_utils::*;
221
222    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
223    let errors = test_operation_with_schema(
224        "query Foo($a: String, $b: String, $c: String) {
225      field(a: $a) {
226        field(b: $b) {
227          field(c: $c)
228        }
229      }
230    }
231  ",
232        TEST_SCHEMA,
233        &mut plan,
234    );
235
236    assert_eq!(get_messages(&errors).len(), 0);
237}
238
239#[test]
240fn use_all_variables_deeply_in_inline_fragments() {
241    use crate::validation::test_utils::*;
242
243    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
244    let errors = test_operation_with_schema(
245        " query Foo($a: String, $b: String, $c: String) {
246      ... on Type {
247        field(a: $a) {
248          field(b: $b) {
249            ... on Type {
250              field(c: $c)
251            }
252          }
253        }
254      }
255    }
256  ",
257        TEST_SCHEMA,
258        &mut plan,
259    );
260
261    assert_eq!(get_messages(&errors).len(), 0);
262}
263
264#[test]
265fn use_all_variables_in_fragments() {
266    use crate::validation::test_utils::*;
267
268    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
269    let errors = test_operation_with_schema(
270        "query Foo($a: String, $b: String, $c: String) {
271      ...FragA
272    }
273    fragment FragA on Type {
274      field(a: $a) {
275        ...FragB
276      }
277    }
278    fragment FragB on Type {
279      field(b: $b) {
280        ...FragC
281      }
282    }
283    fragment FragC on Type {
284      field(c: $c)
285    }",
286        TEST_SCHEMA,
287        &mut plan,
288    );
289
290    assert_eq!(get_messages(&errors).len(), 0);
291}
292
293#[test]
294fn variables_used_by_fragment_in_multiple_operations() {
295    use crate::validation::test_utils::*;
296
297    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
298    let errors = test_operation_with_schema(
299        "query Foo($a: String) {
300      ...FragA
301    }
302    query Bar($b: String) {
303      ...FragB
304    }
305    fragment FragA on Type {
306      field(a: $a)
307    }
308    fragment FragB on Type {
309      field(b: $b)
310    }",
311        TEST_SCHEMA,
312        &mut plan,
313    );
314
315    assert_eq!(get_messages(&errors).len(), 0);
316}
317
318#[test]
319fn variables_used_by_recursive_fragment() {
320    use crate::validation::test_utils::*;
321
322    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
323    let errors = test_operation_with_schema(
324        "query Foo($a: String) {
325      ...FragA
326    }
327    fragment FragA on Type {
328      field(a: $a) {
329        ...FragA
330      }
331    }",
332        TEST_SCHEMA,
333        &mut plan,
334    );
335
336    assert_eq!(get_messages(&errors).len(), 0);
337}
338
339#[test]
340fn variables_not_used() {
341    use crate::validation::test_utils::*;
342
343    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
344    let errors = test_operation_with_schema(
345        "query ($a: String, $b: String, $c: String) {
346          field(a: $a, b: $b)
347        }",
348        TEST_SCHEMA,
349        &mut plan,
350    );
351
352    let messages = get_messages(&errors);
353
354    assert_eq!(messages.len(), 1);
355    assert!(messages.contains(&&"Variable \"$c\" is never used.".to_owned()));
356}
357
358#[test]
359fn multiple_variables_not_used() {
360    use crate::validation::test_utils::*;
361
362    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
363    let errors = test_operation_with_schema(
364        "query Foo($a: String, $b: String, $c: String) {
365          field(b: $b)
366        }",
367        TEST_SCHEMA,
368        &mut plan,
369    );
370
371    let messages = get_messages(&errors);
372
373    assert_eq!(messages.len(), 2);
374    assert!(messages.contains(&&"Variable \"$a\" is never used in operation \"Foo\".".to_owned()));
375    assert!(messages.contains(&&"Variable \"$c\" is never used in operation \"Foo\".".to_owned()));
376}
377
378#[test]
379fn variables_not_used_in_fragments() {
380    use crate::validation::test_utils::*;
381
382    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
383    let errors = test_operation_with_schema(
384        "query Foo($a: String, $b: String, $c: String) {
385          ...FragA
386        }
387        fragment FragA on Type {
388          field(a: $a) {
389            ...FragB
390          }
391        }
392        fragment FragB on Type {
393          field(b: $b) {
394            ...FragC
395          }
396        }
397        fragment FragC on Type {
398          field
399        }",
400        TEST_SCHEMA,
401        &mut plan,
402    );
403
404    let messages = get_messages(&errors);
405
406    assert_eq!(messages.len(), 1);
407    assert!(messages.contains(&&"Variable \"$c\" is never used in operation \"Foo\".".to_owned()));
408}
409
410#[test]
411fn multiple_variables_not_used_in_fragments() {
412    use crate::validation::test_utils::*;
413
414    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
415    let errors = test_operation_with_schema(
416        "query Foo($a: String, $b: String, $c: String) {
417          ...FragA
418        }
419        fragment FragA on Type {
420          field {
421            ...FragB
422          }
423        }
424        fragment FragB on Type {
425          field(b: $b) {
426            ...FragC
427          }
428        }
429        fragment FragC on Type {
430          field
431        }",
432        TEST_SCHEMA,
433        &mut plan,
434    );
435
436    let messages = get_messages(&errors);
437
438    assert_eq!(messages.len(), 2);
439    assert!(messages.contains(&&"Variable \"$a\" is never used in operation \"Foo\".".to_owned()));
440    assert!(messages.contains(&&"Variable \"$c\" is never used in operation \"Foo\".".to_owned()));
441}
442
443#[test]
444fn variables_not_used_by_unreferences_fragment() {
445    use crate::validation::test_utils::*;
446
447    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
448    let errors = test_operation_with_schema(
449        "query Foo($b: String) {
450          ...FragA
451        }
452        fragment FragA on Type {
453          field(a: $a)
454        }
455        fragment FragB on Type {
456          field(b: $b)
457        }",
458        TEST_SCHEMA,
459        &mut plan,
460    );
461
462    let messages = get_messages(&errors);
463
464    assert_eq!(messages.len(), 1);
465    assert!(messages.contains(&&"Variable \"$b\" is never used in operation \"Foo\".".to_owned()));
466}
467
468#[test]
469fn variables_not_used_by_fragment_used_by_other_operation() {
470    use crate::validation::test_utils::*;
471
472    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
473    let errors = test_operation_with_schema(
474        "query Foo($b: String) {
475          ...FragA
476        }
477        query Bar($a: String) {
478          ...FragB
479        }
480        fragment FragA on Type {
481          field(a: $a)
482        }
483        fragment FragB on Type {
484          field(b: $b)
485        }",
486        TEST_SCHEMA,
487        &mut plan,
488    );
489
490    let messages = get_messages(&errors);
491
492    assert_eq!(messages.len(), 2);
493    assert!(messages.contains(&&"Variable \"$b\" is never used in operation \"Foo\".".to_owned()));
494    assert!(messages.contains(&&"Variable \"$a\" is never used in operation \"Bar\".".to_owned()));
495}
496
497#[test]
498fn should_also_check_directives_usage() {
499    use crate::validation::test_utils::*;
500
501    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
502    let errors = test_operation_with_schema(
503        "query foo($skip: Boolean!) {
504          field @skip(if: $skip)
505        }
506        ",
507        TEST_SCHEMA,
508        &mut plan,
509    );
510
511    let messages = get_messages(&errors);
512    assert_eq!(messages.len(), 0);
513}
514
515#[test]
516fn nested_variable_should_work_as_well() {
517    use crate::validation::test_utils::*;
518
519    let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
520    let errors = test_operation_with_schema(
521        "query foo($t: Boolean!) {
522          field(boop: { test: $t})
523        }
524        ",
525        TEST_SCHEMA,
526        &mut plan,
527    );
528
529    let messages = get_messages(&errors);
530    assert_eq!(messages.len(), 0);
531}