juniper/validation/rules/
no_undefined_variables.rs

1use crate::{
2    ast::{Document, Fragment, FragmentSpread, InputValue, Operation, VariableDefinition},
3    parser::{SourcePosition, Spanning},
4    validation::{RuleError, ValidatorContext, Visitor},
5    value::ScalarValue,
6    Span,
7};
8use std::collections::{HashMap, HashSet};
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub enum Scope<'a> {
12    Operation(Option<&'a str>),
13    Fragment(&'a str),
14}
15
16pub fn factory<'a>() -> NoUndefinedVariables<'a> {
17    NoUndefinedVariables {
18        defined_variables: HashMap::new(),
19        used_variables: HashMap::new(),
20        current_scope: None,
21        spreads: HashMap::new(),
22    }
23}
24
25type BorrowedSpanning<'a, T> = Spanning<&'a T, &'a Span>;
26
27pub struct NoUndefinedVariables<'a> {
28    defined_variables: HashMap<Option<&'a str>, (SourcePosition, HashSet<&'a str>)>,
29    used_variables: HashMap<Scope<'a>, Vec<BorrowedSpanning<'a, str>>>,
30    current_scope: Option<Scope<'a>>,
31    spreads: HashMap<Scope<'a>, Vec<&'a str>>,
32}
33
34impl<'a> NoUndefinedVariables<'a> {
35    fn find_undef_vars(
36        &'a self,
37        scope: &Scope<'a>,
38        defined: &HashSet<&'a str>,
39        unused: &mut Vec<BorrowedSpanning<'a, str>>,
40        visited: &mut HashSet<Scope<'a>>,
41    ) {
42        let mut to_visit = Vec::new();
43        if let Some(spreads) = self.find_undef_vars_inner(scope, defined, unused, visited) {
44            to_visit.push(spreads);
45        }
46        while let Some(spreads) = to_visit.pop() {
47            for spread in spreads {
48                if let Some(spreads) =
49                    self.find_undef_vars_inner(&Scope::Fragment(spread), defined, unused, visited)
50                {
51                    to_visit.push(spreads);
52                }
53            }
54        }
55    }
56
57    /// This function should be called only inside [`Self::find_undef_vars()`],
58    /// as it's a recursive function using heap instead of a stack. So, instead
59    /// of the recursive call, we return a [`Vec`] that is visited inside
60    /// [`Self::find_undef_vars()`].
61    fn find_undef_vars_inner(
62        &'a self,
63        scope: &Scope<'a>,
64        defined: &HashSet<&'a str>,
65        unused: &mut Vec<BorrowedSpanning<'a, str>>,
66        visited: &mut HashSet<Scope<'a>>,
67    ) -> Option<&'a Vec<&'a str>> {
68        if visited.contains(scope) {
69            return None;
70        }
71
72        visited.insert(scope.clone());
73
74        if let Some(used_vars) = self.used_variables.get(scope) {
75            for var in used_vars {
76                if !defined.contains(&var.item) {
77                    unused.push(*var);
78                }
79            }
80        }
81
82        self.spreads.get(scope)
83    }
84}
85
86impl<'a, S> Visitor<'a, S> for NoUndefinedVariables<'a>
87where
88    S: ScalarValue,
89{
90    fn exit_document(&mut self, ctx: &mut ValidatorContext<'a, S>, _: &'a Document<S>) {
91        for (op_name, (pos, def_vars)) in &self.defined_variables {
92            let mut unused = Vec::new();
93            let mut visited = HashSet::new();
94            self.find_undef_vars(
95                &Scope::Operation(*op_name),
96                def_vars,
97                &mut unused,
98                &mut visited,
99            );
100
101            ctx.append_errors(
102                unused
103                    .into_iter()
104                    .map(|var| {
105                        RuleError::new(&error_message(var.item, *op_name), &[var.span.start, *pos])
106                    })
107                    .collect(),
108            );
109        }
110    }
111
112    fn enter_operation_definition(
113        &mut self,
114        _: &mut ValidatorContext<'a, S>,
115        op: &'a Spanning<Operation<S>>,
116    ) {
117        let op_name = op.item.name.as_ref().map(|s| s.item);
118        self.current_scope = Some(Scope::Operation(op_name));
119        self.defined_variables
120            .insert(op_name, (op.span.start, HashSet::new()));
121    }
122
123    fn enter_fragment_definition(
124        &mut self,
125        _: &mut ValidatorContext<'a, S>,
126        f: &'a Spanning<Fragment<S>>,
127    ) {
128        self.current_scope = Some(Scope::Fragment(f.item.name.item));
129    }
130
131    fn enter_fragment_spread(
132        &mut self,
133        _: &mut ValidatorContext<'a, S>,
134        spread: &'a Spanning<FragmentSpread<S>>,
135    ) {
136        if let Some(ref scope) = self.current_scope {
137            self.spreads
138                .entry(scope.clone())
139                .or_default()
140                .push(spread.item.name.item);
141        }
142    }
143
144    fn enter_variable_definition(
145        &mut self,
146        _: &mut ValidatorContext<'a, S>,
147        (var_name, _): &'a (Spanning<&'a str>, VariableDefinition<S>),
148    ) {
149        if let Some(Scope::Operation(ref name)) = self.current_scope {
150            if let Some(&mut (_, ref mut vars)) = self.defined_variables.get_mut(name) {
151                vars.insert(var_name.item);
152            }
153        }
154    }
155
156    fn enter_argument(
157        &mut self,
158        _: &mut ValidatorContext<'a, S>,
159        (_, value): &'a (Spanning<&'a str>, Spanning<InputValue<S>>),
160    ) {
161        if let Some(ref scope) = self.current_scope {
162            self.used_variables
163                .entry(scope.clone())
164                .or_default()
165                .append(
166                    &mut value
167                        .item
168                        .referenced_variables()
169                        .iter()
170                        .map(|&var_name| BorrowedSpanning {
171                            span: &value.span,
172                            item: var_name,
173                        })
174                        .collect(),
175                );
176        }
177    }
178}
179
180fn error_message(var_name: &str, op_name: Option<&str>) -> String {
181    if let Some(op_name) = op_name {
182        format!(r#"Variable "${var_name}" is not defined by operation "{op_name}""#)
183    } else {
184        format!(r#"Variable "${var_name}" is not defined"#)
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::{error_message, factory};
191
192    use crate::{
193        parser::SourcePosition,
194        validation::{expect_fails_rule, expect_passes_rule, RuleError},
195        value::DefaultScalarValue,
196    };
197
198    #[test]
199    fn all_variables_defined() {
200        expect_passes_rule::<_, _, DefaultScalarValue>(
201            factory,
202            r#"
203          query Foo($a: String, $b: String, $c: String) {
204            field(a: $a, b: $b, c: $c)
205          }
206        "#,
207        );
208    }
209
210    #[test]
211    fn all_variables_deeply_defined() {
212        expect_passes_rule::<_, _, DefaultScalarValue>(
213            factory,
214            r#"
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        );
224    }
225
226    #[test]
227    fn all_variables_deeply_defined_in_inline_fragments_defined() {
228        expect_passes_rule::<_, _, DefaultScalarValue>(
229            factory,
230            r#"
231          query Foo($a: String, $b: String, $c: String) {
232            ... on Type {
233              field(a: $a) {
234                field(b: $b) {
235                  ... on Type {
236                    field(c: $c)
237                  }
238                }
239              }
240            }
241          }
242        "#,
243        );
244    }
245
246    #[test]
247    fn all_variables_in_fragments_deeply_defined() {
248        expect_passes_rule::<_, _, DefaultScalarValue>(
249            factory,
250            r#"
251          query Foo($a: String, $b: String, $c: String) {
252            ...FragA
253          }
254          fragment FragA on Type {
255            field(a: $a) {
256              ...FragB
257            }
258          }
259          fragment FragB on Type {
260            field(b: $b) {
261              ...FragC
262            }
263          }
264          fragment FragC on Type {
265            field(c: $c)
266          }
267        "#,
268        );
269    }
270
271    #[test]
272    fn variable_within_single_fragment_defined_in_multiple_operations() {
273        expect_passes_rule::<_, _, DefaultScalarValue>(
274            factory,
275            r#"
276          query Foo($a: String) {
277            ...FragA
278          }
279          query Bar($a: String) {
280            ...FragA
281          }
282          fragment FragA on Type {
283            field(a: $a)
284          }
285        "#,
286        );
287    }
288
289    #[test]
290    fn variable_within_fragments_defined_in_operations() {
291        expect_passes_rule::<_, _, DefaultScalarValue>(
292            factory,
293            r#"
294          query Foo($a: String) {
295            ...FragA
296          }
297          query Bar($b: String) {
298            ...FragB
299          }
300          fragment FragA on Type {
301            field(a: $a)
302          }
303          fragment FragB on Type {
304            field(b: $b)
305          }
306        "#,
307        );
308    }
309
310    #[test]
311    fn variable_within_recursive_fragment_defined() {
312        expect_passes_rule::<_, _, DefaultScalarValue>(
313            factory,
314            r#"
315          query Foo($a: String) {
316            ...FragA
317          }
318          fragment FragA on Type {
319            field(a: $a) {
320              ...FragA
321            }
322          }
323        "#,
324        );
325    }
326
327    #[test]
328    fn variable_not_defined() {
329        expect_fails_rule::<_, _, DefaultScalarValue>(
330            factory,
331            r#"
332          query Foo($a: String, $b: String, $c: String) {
333            field(a: $a, b: $b, c: $c, d: $d)
334          }
335        "#,
336            &[RuleError::new(
337                &error_message("d", Some("Foo")),
338                &[
339                    SourcePosition::new(101, 2, 42),
340                    SourcePosition::new(11, 1, 10),
341                ],
342            )],
343        );
344    }
345
346    #[test]
347    fn variable_not_defined_by_unnamed_query() {
348        expect_fails_rule::<_, _, DefaultScalarValue>(
349            factory,
350            r#"
351          {
352            field(a: $a)
353          }
354        "#,
355            &[RuleError::new(
356                &error_message("a", None),
357                &[
358                    SourcePosition::new(34, 2, 21),
359                    SourcePosition::new(11, 1, 10),
360                ],
361            )],
362        );
363    }
364
365    #[test]
366    fn multiple_variables_not_defined() {
367        expect_fails_rule::<_, _, DefaultScalarValue>(
368            factory,
369            r#"
370          query Foo($b: String) {
371            field(a: $a, b: $b, c: $c)
372          }
373        "#,
374            &[
375                RuleError::new(
376                    &error_message("a", Some("Foo")),
377                    &[
378                        SourcePosition::new(56, 2, 21),
379                        SourcePosition::new(11, 1, 10),
380                    ],
381                ),
382                RuleError::new(
383                    &error_message("c", Some("Foo")),
384                    &[
385                        SourcePosition::new(70, 2, 35),
386                        SourcePosition::new(11, 1, 10),
387                    ],
388                ),
389            ],
390        );
391    }
392
393    #[test]
394    fn variable_in_fragment_not_defined_by_unnamed_query() {
395        expect_fails_rule::<_, _, DefaultScalarValue>(
396            factory,
397            r#"
398          {
399            ...FragA
400          }
401          fragment FragA on Type {
402            field(a: $a)
403          }
404        "#,
405            &[RuleError::new(
406                &error_message("a", None),
407                &[
408                    SourcePosition::new(102, 5, 21),
409                    SourcePosition::new(11, 1, 10),
410                ],
411            )],
412        );
413    }
414
415    #[test]
416    fn variable_in_fragment_not_defined_by_operation() {
417        expect_fails_rule::<_, _, DefaultScalarValue>(
418            factory,
419            r#"
420          query Foo($a: String, $b: String) {
421            ...FragA
422          }
423          fragment FragA on Type {
424            field(a: $a) {
425              ...FragB
426            }
427          }
428          fragment FragB on Type {
429            field(b: $b) {
430              ...FragC
431            }
432          }
433          fragment FragC on Type {
434            field(c: $c)
435          }
436        "#,
437            &[RuleError::new(
438                &error_message("c", Some("Foo")),
439                &[
440                    SourcePosition::new(358, 15, 21),
441                    SourcePosition::new(11, 1, 10),
442                ],
443            )],
444        );
445    }
446
447    #[test]
448    fn multiple_variables_in_fragments_not_defined() {
449        expect_fails_rule::<_, _, DefaultScalarValue>(
450            factory,
451            r#"
452          query Foo($b: String) {
453            ...FragA
454          }
455          fragment FragA on Type {
456            field(a: $a) {
457              ...FragB
458            }
459          }
460          fragment FragB on Type {
461            field(b: $b) {
462              ...FragC
463            }
464          }
465          fragment FragC on Type {
466            field(c: $c)
467          }
468        "#,
469            &[
470                RuleError::new(
471                    &error_message("a", Some("Foo")),
472                    &[
473                        SourcePosition::new(124, 5, 21),
474                        SourcePosition::new(11, 1, 10),
475                    ],
476                ),
477                RuleError::new(
478                    &error_message("c", Some("Foo")),
479                    &[
480                        SourcePosition::new(346, 15, 21),
481                        SourcePosition::new(11, 1, 10),
482                    ],
483                ),
484            ],
485        );
486    }
487
488    #[test]
489    fn single_variable_in_fragment_not_defined_by_multiple_operations() {
490        expect_fails_rule::<_, _, DefaultScalarValue>(
491            factory,
492            r#"
493          query Foo($a: String) {
494            ...FragAB
495          }
496          query Bar($a: String) {
497            ...FragAB
498          }
499          fragment FragAB on Type {
500            field(a: $a, b: $b)
501          }
502        "#,
503            &[
504                RuleError::new(
505                    &error_message("b", Some("Foo")),
506                    &[
507                        SourcePosition::new(201, 8, 28),
508                        SourcePosition::new(11, 1, 10),
509                    ],
510                ),
511                RuleError::new(
512                    &error_message("b", Some("Bar")),
513                    &[
514                        SourcePosition::new(201, 8, 28),
515                        SourcePosition::new(79, 4, 10),
516                    ],
517                ),
518            ],
519        );
520    }
521
522    #[test]
523    fn variables_in_fragment_not_defined_by_multiple_operations() {
524        expect_fails_rule::<_, _, DefaultScalarValue>(
525            factory,
526            r#"
527          query Foo($b: String) {
528            ...FragAB
529          }
530          query Bar($a: String) {
531            ...FragAB
532          }
533          fragment FragAB on Type {
534            field(a: $a, b: $b)
535          }
536        "#,
537            &[
538                RuleError::new(
539                    &error_message("a", Some("Foo")),
540                    &[
541                        SourcePosition::new(194, 8, 21),
542                        SourcePosition::new(11, 1, 10),
543                    ],
544                ),
545                RuleError::new(
546                    &error_message("b", Some("Bar")),
547                    &[
548                        SourcePosition::new(201, 8, 28),
549                        SourcePosition::new(79, 4, 10),
550                    ],
551                ),
552            ],
553        );
554    }
555
556    #[test]
557    fn variable_in_fragment_used_by_other_operation() {
558        expect_fails_rule::<_, _, DefaultScalarValue>(
559            factory,
560            r#"
561          query Foo($b: String) {
562            ...FragA
563          }
564          query Bar($a: String) {
565            ...FragB
566          }
567          fragment FragA on Type {
568            field(a: $a)
569          }
570          fragment FragB on Type {
571            field(b: $b)
572          }
573        "#,
574            &[
575                RuleError::new(
576                    &error_message("a", Some("Foo")),
577                    &[
578                        SourcePosition::new(191, 8, 21),
579                        SourcePosition::new(11, 1, 10),
580                    ],
581                ),
582                RuleError::new(
583                    &error_message("b", Some("Bar")),
584                    &[
585                        SourcePosition::new(263, 11, 21),
586                        SourcePosition::new(78, 4, 10),
587                    ],
588                ),
589            ],
590        );
591    }
592
593    #[test]
594    fn multiple_undefined_variables_produce_multiple_errors() {
595        expect_fails_rule::<_, _, DefaultScalarValue>(
596            factory,
597            r#"
598          query Foo($b: String) {
599            ...FragAB
600          }
601          query Bar($a: String) {
602            ...FragAB
603          }
604          fragment FragAB on Type {
605            field1(a: $a, b: $b)
606            ...FragC
607            field3(a: $a, b: $b)
608          }
609          fragment FragC on Type {
610            field2(c: $c)
611          }
612        "#,
613            &[
614                RuleError::new(
615                    &error_message("a", Some("Foo")),
616                    &[
617                        SourcePosition::new(195, 8, 22),
618                        SourcePosition::new(11, 1, 10),
619                    ],
620                ),
621                RuleError::new(
622                    &error_message("b", Some("Bar")),
623                    &[
624                        SourcePosition::new(202, 8, 29),
625                        SourcePosition::new(79, 4, 10),
626                    ],
627                ),
628                RuleError::new(
629                    &error_message("a", Some("Foo")),
630                    &[
631                        SourcePosition::new(249, 10, 22),
632                        SourcePosition::new(11, 1, 10),
633                    ],
634                ),
635                RuleError::new(
636                    &error_message("b", Some("Bar")),
637                    &[
638                        SourcePosition::new(256, 10, 29),
639                        SourcePosition::new(79, 4, 10),
640                    ],
641                ),
642                RuleError::new(
643                    &error_message("c", Some("Foo")),
644                    &[
645                        SourcePosition::new(329, 13, 22),
646                        SourcePosition::new(11, 1, 10),
647                    ],
648                ),
649                RuleError::new(
650                    &error_message("c", Some("Bar")),
651                    &[
652                        SourcePosition::new(329, 13, 22),
653                        SourcePosition::new(79, 4, 10),
654                    ],
655                ),
656            ],
657        );
658    }
659}