Skip to main content

graphql_tools/validation/rules/
no_undefined_variables.rs

1use super::ValidationRule;
2use crate::ast::{visit_document, 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<'a> {
14    current_scope: Option<NoUndefinedVariablesScope<'a>>,
15    defined_variables: HashMap<Option<&'a str>, HashSet<&'a str>>,
16    used_variables: HashMap<NoUndefinedVariablesScope<'a>, Vec<&'a str>>,
17    spreads: HashMap<NoUndefinedVariablesScope<'a>, Vec<&'a str>>,
18}
19
20impl<'a> Default for NoUndefinedVariables<'a> {
21    fn default() -> Self {
22        Self::new()
23    }
24}
25
26impl<'a> NoUndefinedVariables<'a> {
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<'a> NoUndefinedVariables<'a> {
38    fn find_undefined_vars(
39        &self,
40        from: &NoUndefinedVariablesScope<'a>,
41        defined: &HashSet<&str>,
42        unused: &mut HashSet<&'a str>,
43        visited: &mut HashSet<NoUndefinedVariablesScope<'a>>,
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<'a> {
74    Operation(Option<&'a str>),
75    Fragment(&'a str),
76}
77
78impl<'a> OperationVisitor<'a, ValidationErrorContext> for NoUndefinedVariables<'a> {
79    fn enter_operation_definition(
80        &mut self,
81        _: &mut OperationVisitorContext,
82        _: &mut ValidationErrorContext,
83        operation_definition: &'a 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: &'a 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: &'a 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: &'a 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): &'a (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<'n> ValidationRule for NoUndefinedVariables<'n> {
182    fn error_code<'a>(&self) -> &'a str {
183        "NoUndefinedVariables"
184    }
185
186    fn validate(
187        &self,
188        ctx: &mut OperationVisitorContext,
189        error_collector: &mut ValidationErrorContext,
190    ) {
191        visit_document(
192            &mut NoUndefinedVariables::new(),
193            ctx.operation,
194            ctx,
195            error_collector,
196        );
197    }
198}
199
200#[test]
201fn all_variables_defined() {
202    use crate::validation::test_utils::*;
203
204    let mut plan = create_plan_from_rule(Box::new(NoUndefinedVariables::new()));
205    let errors = test_operation_with_schema(
206        "query Foo($a: String, $b: String, $c: String) {
207          field(a: $a, b: $b, c: $c)
208        }",
209        TEST_SCHEMA,
210        &mut plan,
211    );
212
213    let messages = get_messages(&errors);
214    assert_eq!(messages.len(), 0);
215}
216
217#[test]
218fn all_variables_deeply_defined() {
219    use crate::validation::test_utils::*;
220
221    let mut plan = create_plan_from_rule(Box::new(NoUndefinedVariables::new()));
222    let errors = test_operation_with_schema(
223        "query Foo($a: String, $b: String, $c: String) {
224          field(a: $a) {
225            field(b: $b) {
226              field(c: $c)
227            }
228          }
229        }",
230        TEST_SCHEMA,
231        &mut plan,
232    );
233
234    let messages = get_messages(&errors);
235    assert_eq!(messages.len(), 0);
236}
237
238#[test]
239fn all_variables_deeply_in_inline_fragments_defined() {
240    use crate::validation::test_utils::*;
241
242    let mut plan = create_plan_from_rule(Box::new(NoUndefinedVariables::new()));
243    let errors = test_operation_with_schema(
244        "query Foo($a: String, $b: String, $c: String) {
245          ... on Type {
246            field(a: $a) {
247              field(b: $b) {
248                ... on Type {
249                  field(c: $c)
250                }
251              }
252            }
253          }
254        }",
255        TEST_SCHEMA,
256        &mut plan,
257    );
258
259    let messages = get_messages(&errors);
260    assert_eq!(messages.len(), 0);
261}
262
263#[test]
264fn all_variables_in_fragments_deeply_defined() {
265    use crate::validation::test_utils::*;
266
267    let mut plan = create_plan_from_rule(Box::new(NoUndefinedVariables::new()));
268    let errors = test_operation_with_schema(
269        "query Foo($a: String, $b: String, $c: String) {
270          ...FragA
271        }
272        fragment FragA on Type {
273          field(a: $a) {
274            ...FragB
275          }
276        }
277        fragment FragB on Type {
278          field(b: $b) {
279            ...FragC
280          }
281        }
282        fragment FragC on Type {
283          field(c: $c)
284        }",
285        TEST_SCHEMA,
286        &mut plan,
287    );
288
289    let messages = get_messages(&errors);
290    assert_eq!(messages.len(), 0);
291}
292
293#[test]
294fn variable_within_single_fragment_defined_in_multiple_operations() {
295    use crate::validation::test_utils::*;
296
297    let mut plan = create_plan_from_rule(Box::new(NoUndefinedVariables::new()));
298    let errors = test_operation_with_schema(
299        "query Foo($a: String) {
300          ...FragA
301        }
302        query Bar($a: String) {
303          ...FragA
304        }
305        fragment FragA on Type {
306          field(a: $a)
307        }",
308        TEST_SCHEMA,
309        &mut plan,
310    );
311
312    let messages = get_messages(&errors);
313    assert_eq!(messages.len(), 0);
314}
315
316#[test]
317fn variable_within_fragments_defined_in_operations() {
318    use crate::validation::test_utils::*;
319
320    let mut plan = create_plan_from_rule(Box::new(NoUndefinedVariables::new()));
321    let errors = test_operation_with_schema(
322        "query Foo($a: String) {
323          ...FragA
324        }
325        query Bar($b: String) {
326          ...FragB
327        }
328        fragment FragA on Type {
329          field(a: $a)
330        }
331        fragment FragB on Type {
332          field(b: $b)
333        }",
334        TEST_SCHEMA,
335        &mut plan,
336    );
337
338    let messages = get_messages(&errors);
339    assert_eq!(messages.len(), 0);
340}
341
342#[test]
343fn variable_within_recursive_fragment_defined() {
344    use crate::validation::test_utils::*;
345
346    let mut plan = create_plan_from_rule(Box::new(NoUndefinedVariables::new()));
347    let errors = test_operation_with_schema(
348        "query Foo($a: String) {
349          ...FragA
350        }
351        fragment FragA on Type {
352          field(a: $a) {
353            ...FragA
354          }
355        }",
356        TEST_SCHEMA,
357        &mut plan,
358    );
359
360    let messages = get_messages(&errors);
361    assert_eq!(messages.len(), 0);
362}
363
364#[test]
365fn variable_not_defined() {
366    use crate::validation::test_utils::*;
367
368    let mut plan = create_plan_from_rule(Box::new(NoUndefinedVariables::new()));
369    let errors = test_operation_with_schema(
370        "query Foo($a: String, $b: String, $c: String) {
371          field(a: $a, b: $b, c: $c, d: $d)
372        }",
373        TEST_SCHEMA,
374        &mut plan,
375    );
376
377    let messages = get_messages(&errors);
378    assert_eq!(messages.len(), 1);
379    assert_eq!(
380        messages,
381        vec!["Variable \"$d\" is not defined by operation \"Foo\"."]
382    );
383}
384
385#[test]
386fn variable_not_defined_by_un_named_query() {
387    use crate::validation::test_utils::*;
388
389    let mut plan = create_plan_from_rule(Box::new(NoUndefinedVariables::new()));
390    let errors = test_operation_with_schema(
391        "{
392          field(a: $a)
393        }",
394        TEST_SCHEMA,
395        &mut plan,
396    );
397
398    let messages = get_messages(&errors);
399    assert_eq!(messages.len(), 1);
400    assert_eq!(messages, vec!["Variable \"$a\" is not defined."]);
401}
402
403#[test]
404fn multiple_variables_not_defined() {
405    use crate::validation::test_utils::*;
406
407    let mut plan = create_plan_from_rule(Box::new(NoUndefinedVariables::new()));
408    let errors = test_operation_with_schema(
409        "query Foo($b: String) {
410          field(a: $a, b: $b, c: $c)
411        }",
412        TEST_SCHEMA,
413        &mut plan,
414    );
415
416    let messages = get_messages(&errors);
417    assert_eq!(messages.len(), 2);
418    assert!(messages.contains(&&"Variable \"$a\" is not defined by operation \"Foo\".".to_owned()));
419    assert!(messages.contains(&&"Variable \"$c\" is not defined by operation \"Foo\".".to_owned()));
420}
421
422#[test]
423fn variable_in_fragment_not_defined_by_un_named_query() {
424    use crate::validation::test_utils::*;
425
426    let mut plan = create_plan_from_rule(Box::new(NoUndefinedVariables::new()));
427    let errors = test_operation_with_schema(
428        "{
429          ...FragA
430        }
431        fragment FragA on Type {
432          field(a: $a)
433        }",
434        TEST_SCHEMA,
435        &mut plan,
436    );
437
438    let messages = get_messages(&errors);
439    assert_eq!(messages.len(), 1);
440    assert_eq!(messages, vec!["Variable \"$a\" is not defined.",]);
441}
442
443#[test]
444fn variable_in_fragment_not_defined_by_operation() {
445    use crate::validation::test_utils::*;
446
447    let mut plan = create_plan_from_rule(Box::new(NoUndefinedVariables::new()));
448    let errors = test_operation_with_schema(
449        "query Foo($a: String, $b: String) {
450          ...FragA
451        }
452        fragment FragA on Type {
453          field(a: $a) {
454            ...FragB
455          }
456        }
457        fragment FragB on Type {
458          field(b: $b) {
459            ...FragC
460          }
461        }
462        fragment FragC on Type {
463          field(c: $c)
464        }",
465        TEST_SCHEMA,
466        &mut plan,
467    );
468
469    let messages = get_messages(&errors);
470    assert_eq!(messages.len(), 1);
471    assert_eq!(
472        messages,
473        vec!["Variable \"$c\" is not defined by operation \"Foo\"."]
474    );
475}
476
477#[test]
478fn multiple_variables_in_fragments_not_defined() {
479    use crate::validation::test_utils::*;
480
481    let mut plan = create_plan_from_rule(Box::new(NoUndefinedVariables::new()));
482    let errors = test_operation_with_schema(
483        "query Foo($b: String) {
484          ...FragA
485        }
486        fragment FragA on Type {
487          field(a: $a) {
488            ...FragB
489          }
490        }
491        fragment FragB on Type {
492          field(b: $b) {
493            ...FragC
494          }
495        }
496        fragment FragC on Type {
497          field(c: $c)
498        }",
499        TEST_SCHEMA,
500        &mut plan,
501    );
502
503    let messages = get_messages(&errors);
504    assert_eq!(messages.len(), 2);
505    assert!(messages.contains(&&"Variable \"$c\" is not defined by operation \"Foo\".".to_owned()));
506    assert!(messages.contains(&&"Variable \"$a\" is not defined by operation \"Foo\".".to_owned()));
507}
508
509#[test]
510fn single_variable_in_fragment_not_defined_by_multiple_operations() {
511    use crate::validation::test_utils::*;
512
513    let mut plan = create_plan_from_rule(Box::new(NoUndefinedVariables::new()));
514    let errors = test_operation_with_schema(
515        "query Foo($a: String) {
516          ...FragAB
517        }
518        query Bar($a: String) {
519          ...FragAB
520        }
521        fragment FragAB on Type {
522          field(a: $a, b: $b)
523        }",
524        TEST_SCHEMA,
525        &mut plan,
526    );
527
528    let messages = get_messages(&errors);
529    assert_eq!(messages.len(), 2);
530    assert!(messages.contains(&&"Variable \"$b\" is not defined by operation \"Bar\".".to_owned()));
531    assert!(messages.contains(&&"Variable \"$b\" is not defined by operation \"Foo\".".to_owned()));
532}
533
534#[test]
535fn variables_in_fragment_not_defined_by_multiple_operations() {
536    use crate::validation::test_utils::*;
537
538    let mut plan = create_plan_from_rule(Box::new(NoUndefinedVariables::new()));
539    let errors = test_operation_with_schema(
540        "query Foo($b: String) {
541          ...FragAB
542        }
543        query Bar($a: String) {
544          ...FragAB
545        }
546        fragment FragAB on Type {
547          field(a: $a, b: $b)
548        }",
549        TEST_SCHEMA,
550        &mut plan,
551    );
552
553    let messages = get_messages(&errors);
554    assert_eq!(messages.len(), 2);
555    assert!(messages.contains(&&"Variable \"$a\" is not defined by operation \"Foo\".".to_owned()));
556    assert!(messages.contains(&&"Variable \"$b\" is not defined by operation \"Bar\".".to_owned()));
557}
558
559#[test]
560fn variable_in_fragment_used_by_other_operation() {
561    use crate::validation::test_utils::*;
562
563    let mut plan = create_plan_from_rule(Box::new(NoUndefinedVariables::new()));
564    let errors = test_operation_with_schema(
565        "query Foo($b: String) {
566          ...FragA
567        }
568        query Bar($a: String) {
569          ...FragB
570        }
571        fragment FragA on Type {
572          field(a: $a)
573        }
574        fragment FragB on Type {
575          field(b: $b)
576        }",
577        TEST_SCHEMA,
578        &mut plan,
579    );
580
581    let messages = get_messages(&errors);
582    assert_eq!(messages.len(), 2);
583    assert!(messages.contains(&&"Variable \"$a\" is not defined by operation \"Foo\".".to_owned()));
584    assert!(messages.contains(&&"Variable \"$b\" is not defined by operation \"Bar\".".to_owned()));
585}
586#[test]
587fn multiple_undefined_variables_produce_multiple_errors() {
588    use crate::validation::test_utils::*;
589
590    let mut plan = create_plan_from_rule(Box::new(NoUndefinedVariables::new()));
591    let errors = test_operation_with_schema(
592        "query Foo($b: String) {
593          ...FragAB
594        }
595        query Bar($a: String) {
596          ...FragAB
597        }
598        fragment FragAB on Type {
599          field1(a: $a, b: $b)
600          ...FragC
601          field3(a: $a, b: $b)
602        }
603        fragment FragC on Type {
604          field2(c: $c)
605        }",
606        TEST_SCHEMA,
607        &mut plan,
608    );
609
610    let messages = get_messages(&errors);
611    assert_eq!(messages.len(), 4);
612    assert!(messages.contains(&&"Variable \"$c\" is not defined by operation \"Foo\".".to_owned()));
613    assert!(messages.contains(&&"Variable \"$a\" is not defined by operation \"Foo\".".to_owned()));
614    assert!(messages.contains(&&"Variable \"$b\" is not defined by operation \"Bar\".".to_owned()));
615    assert!(messages.contains(&&"Variable \"$c\" is not defined by operation \"Bar\".".to_owned()));
616}