Skip to main content

graphql_tools/validation/rules/
no_undefined_variables.rs

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