Skip to main content

graphql_tools/validation/rules/
variables_in_allowed_position.rs

1use std::collections::{HashMap, HashSet};
2
3use crate::{
4    ast::{visit_document, AstNodeWithName, OperationVisitor, OperationVisitorContext},
5    static_graphql::query::{Type, Value, VariableDefinition},
6    validation::utils::{ValidationError, ValidationErrorContext},
7};
8
9use super::ValidationRule;
10
11/// Variables in allowed position
12///
13/// Variable usages must be compatible with the arguments they are passed to.
14///
15/// See https://spec.graphql.org/draft/#sec-All-Variable-Usages-are-Allowed
16#[derive(Default)]
17pub struct VariablesInAllowedPosition<'a> {
18    spreads: HashMap<Scope<'a>, HashSet<&'a str>>,
19    variable_usages: HashMap<Scope<'a>, Vec<(&'a str, &'a Type)>>,
20    variable_defs: HashMap<Scope<'a>, Vec<&'a VariableDefinition>>,
21    current_scope: Option<Scope<'a>>,
22}
23
24impl<'a> VariablesInAllowedPosition<'a> {
25    pub fn new() -> Self {
26        VariablesInAllowedPosition {
27            spreads: HashMap::new(),
28            variable_usages: HashMap::new(),
29            variable_defs: HashMap::new(),
30            current_scope: None,
31        }
32    }
33
34    fn collect_incorrect_usages(
35        &self,
36        from: &Scope<'a>,
37        var_defs: &Vec<&VariableDefinition>,
38        visitor_context: &mut OperationVisitorContext,
39        user_context: &mut ValidationErrorContext,
40        visited: &mut HashSet<Scope<'a>>,
41    ) {
42        if visited.contains(from) {
43            return;
44        }
45
46        visited.insert(from.clone());
47
48        if let Some(usages) = self.variable_usages.get(from) {
49            for (var_name, var_type) in usages {
50                if let Some(var_def) = var_defs.iter().find(|var_def| var_def.name == *var_name) {
51                    // Per https://spec.graphql.org/draft/#sec-All-Variable-Usages-are-Allowed,
52                    // a non-null default value makes the variable usable in a non-null position
53                    // because the default fills in when the variable is omitted. Model this by
54                    // promoting the variable's effective type to NonNull(var_type) for the
55                    // subtype check. An explicit `null` default does not get this promotion.
56                    let expected_type = match (&var_def.default_value, &var_def.var_type) {
57                        (_, t @ Type::NonNullType(_)) => t.clone(),
58                        (Some(default_value), t) if !matches!(default_value, Value::Null) => {
59                            Type::NonNullType(Box::new(t.clone()))
60                        }
61                        (_, t) => t.clone(),
62                    };
63
64                    if !visitor_context.schema.is_subtype(&expected_type, var_type) {
65                        user_context.report_error(ValidationError {
66                          error_code: self.error_code(),
67                            message: format!("Variable \"${}\" of type \"{}\" used in position expecting type \"{}\".",
68                                var_name,
69                                expected_type,
70                                var_type,
71                            ),
72                            locations: vec![var_def.position],
73                        });
74                    }
75                }
76            }
77        }
78
79        if let Some(spreads) = self.spreads.get(from) {
80            for spread in spreads {
81                self.collect_incorrect_usages(
82                    &Scope::Fragment(spread),
83                    var_defs,
84                    visitor_context,
85                    user_context,
86                    visited,
87                );
88            }
89        }
90    }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Hash)]
94pub enum Scope<'a> {
95    Operation(Option<&'a str>),
96    Fragment(&'a str),
97}
98
99impl<'a> OperationVisitor<'a, ValidationErrorContext> for VariablesInAllowedPosition<'a> {
100    fn leave_document(
101        &mut self,
102        visitor_context: &mut OperationVisitorContext<'a>,
103        user_context: &mut ValidationErrorContext,
104        _: &crate::static_graphql::query::Document,
105    ) {
106        for (op_scope, var_defs) in &self.variable_defs {
107            self.collect_incorrect_usages(
108                op_scope,
109                var_defs,
110                visitor_context,
111                user_context,
112                &mut HashSet::new(),
113            );
114        }
115    }
116
117    fn enter_fragment_definition(
118        &mut self,
119        _: &mut OperationVisitorContext<'a>,
120        _: &mut ValidationErrorContext,
121        fragment_definition: &'a crate::static_graphql::query::FragmentDefinition,
122    ) {
123        self.current_scope = Some(Scope::Fragment(&fragment_definition.name));
124    }
125
126    fn enter_operation_definition(
127        &mut self,
128        _: &mut OperationVisitorContext<'a>,
129        _: &mut ValidationErrorContext,
130        operation_definition: &'a crate::static_graphql::query::OperationDefinition,
131    ) {
132        self.current_scope = Some(Scope::Operation(operation_definition.node_name()));
133    }
134
135    fn enter_fragment_spread(
136        &mut self,
137        _: &mut OperationVisitorContext<'a>,
138        _: &mut ValidationErrorContext,
139        fragment_spread: &'a crate::static_graphql::query::FragmentSpread,
140    ) {
141        if let Some(scope) = &self.current_scope {
142            self.spreads
143                .entry(scope.clone())
144                .or_default()
145                .insert(&fragment_spread.fragment_name);
146        }
147    }
148
149    fn enter_variable_definition(
150        &mut self,
151        _: &mut OperationVisitorContext<'a>,
152        _: &mut ValidationErrorContext,
153        variable_definition: &'a VariableDefinition,
154    ) {
155        if let Some(ref scope) = self.current_scope {
156            self.variable_defs
157                .entry(scope.clone())
158                .or_default()
159                .push(variable_definition);
160        }
161    }
162
163    fn enter_variable_value(
164        &mut self,
165        visitor_context: &mut OperationVisitorContext<'a>,
166        _: &mut ValidationErrorContext,
167        variable_name: &'a str,
168    ) {
169        if let (Some(scope), Some(input_type)) = (
170            &self.current_scope,
171            visitor_context.current_input_type_literal(),
172        ) {
173            self.variable_usages
174                .entry(scope.clone())
175                .or_default()
176                .push((variable_name, input_type));
177        }
178    }
179}
180
181impl<'v> ValidationRule for VariablesInAllowedPosition<'v> {
182    fn error_code<'a>(&self) -> &'a str {
183        "VariablesInAllowedPosition"
184    }
185
186    fn validate(
187        &self,
188        ctx: &mut OperationVisitorContext,
189        error_collector: &mut ValidationErrorContext,
190    ) {
191        visit_document(
192            &mut VariablesInAllowedPosition::new(),
193            ctx.operation,
194            ctx,
195            error_collector,
196        );
197    }
198}
199
200#[test]
201fn boolean_to_boolean() {
202    use crate::validation::test_utils::*;
203
204    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
205    let errors = test_operation_with_schema(
206        "query Query($booleanArg: Boolean)
207        {
208          complicatedArgs {
209            booleanArgField(booleanArg: $booleanArg)
210          }
211        }",
212        TEST_SCHEMA,
213        &mut plan,
214    );
215
216    assert_eq!(get_messages(&errors).len(), 0);
217}
218
219#[test]
220fn boolean_to_boolean_within_fragment() {
221    use crate::validation::test_utils::*;
222
223    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
224    let errors = test_operation_with_schema(
225        "fragment booleanArgFrag on ComplicatedArgs {
226          booleanArgField(booleanArg: $booleanArg)
227        }
228        query Query($booleanArg: Boolean)
229        {
230          complicatedArgs {
231            ...booleanArgFrag
232          }
233        }",
234        TEST_SCHEMA,
235        &mut plan,
236    );
237
238    assert_eq!(get_messages(&errors).len(), 0);
239
240    let errors = test_operation_with_schema(
241        "query Query($booleanArg: Boolean)
242      {
243        complicatedArgs {
244          ...booleanArgFrag
245        }
246      }
247      fragment booleanArgFrag on ComplicatedArgs {
248        booleanArgField(booleanArg: $booleanArg)
249      }",
250        TEST_SCHEMA,
251        &mut plan,
252    );
253
254    assert_eq!(get_messages(&errors).len(), 0);
255}
256
257#[test]
258fn boolean_nonnull_to_boolean() {
259    use crate::validation::test_utils::*;
260
261    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
262    let errors = test_operation_with_schema(
263        "query Query($nonNullBooleanArg: Boolean!)
264        {
265          complicatedArgs {
266            booleanArgField(booleanArg: $nonNullBooleanArg)
267          }
268        }",
269        TEST_SCHEMA,
270        &mut plan,
271    );
272
273    assert_eq!(get_messages(&errors).len(), 0);
274}
275
276#[test]
277fn string_list_to_string_list() {
278    use crate::validation::test_utils::*;
279
280    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
281    let errors = test_operation_with_schema(
282        "query Query($stringListVar: [String])
283        {
284          complicatedArgs {
285            stringListArgField(stringListArg: $stringListVar)
286          }
287        }",
288        TEST_SCHEMA,
289        &mut plan,
290    );
291
292    assert_eq!(get_messages(&errors).len(), 0);
293}
294
295#[test]
296fn string_list_nonnull_to_string_list() {
297    use crate::validation::test_utils::*;
298
299    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
300    let errors = test_operation_with_schema(
301        "query Query($stringListVar: [String!])
302        {
303          complicatedArgs {
304            stringListArgField(stringListArg: $stringListVar)
305          }
306        }",
307        TEST_SCHEMA,
308        &mut plan,
309    );
310
311    assert_eq!(get_messages(&errors).len(), 0);
312}
313
314#[test]
315fn string_to_string_list_in_item_position() {
316    use crate::validation::test_utils::*;
317
318    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
319    let errors = test_operation_with_schema(
320        "query Query($stringVar: String)
321        {
322          complicatedArgs {
323            stringListArgField(stringListArg: [$stringVar])
324          }
325        }",
326        TEST_SCHEMA,
327        &mut plan,
328    );
329
330    assert_eq!(get_messages(&errors).len(), 0);
331}
332
333#[test]
334fn string_nonnull_to_string_list_in_item_position() {
335    use crate::validation::test_utils::*;
336
337    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
338    let errors = test_operation_with_schema(
339        "query Query($stringVar: String!)
340        {
341          complicatedArgs {
342            stringListArgField(stringListArg: [$stringVar])
343          }
344        }",
345        TEST_SCHEMA,
346        &mut plan,
347    );
348
349    assert_eq!(get_messages(&errors).len(), 0);
350}
351
352#[test]
353fn complexinput_to_complexinput() {
354    use crate::validation::test_utils::*;
355
356    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
357    let errors = test_operation_with_schema(
358        "query Query($complexVar: ComplexInput)
359        {
360          complicatedArgs {
361            complexArgField(complexArg: $complexVar)
362          }
363        }",
364        TEST_SCHEMA,
365        &mut plan,
366    );
367
368    assert_eq!(get_messages(&errors).len(), 0);
369}
370
371#[test]
372fn complexinput_to_complexinput_in_field_position() {
373    use crate::validation::test_utils::*;
374
375    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
376    let errors = test_operation_with_schema(
377        "query Query($boolVar: Boolean = false)
378        {
379          complicatedArgs {
380            complexArgField(complexArg: { requiredArg: $boolVar })
381          }
382        }",
383        TEST_SCHEMA,
384        &mut plan,
385    );
386
387    let messages = get_messages(&errors);
388    assert_eq!(messages.len(), 0);
389}
390
391#[test]
392fn boolean_nonnull_to_boolean_nonnull_in_directive() {
393    use crate::validation::test_utils::*;
394
395    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
396    let errors = test_operation_with_schema(
397        "query Query($boolVar: Boolean!)
398        {
399          dog @include(if: $boolVar)
400        }",
401        TEST_SCHEMA,
402        &mut plan,
403    );
404
405    assert_eq!(get_messages(&errors).len(), 0);
406}
407
408#[test]
409fn int_to_int_nonnull() {
410    use crate::validation::test_utils::*;
411
412    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
413    let errors = test_operation_with_schema(
414        "query Query($intArg: Int) {
415          complicatedArgs {
416            nonNullIntArgField(nonNullIntArg: $intArg)
417          }
418        }",
419        TEST_SCHEMA,
420        &mut plan,
421    );
422
423    let messages = get_messages(&errors);
424    assert_eq!(messages.len(), 1);
425    assert_eq!(
426        messages,
427        vec!["Variable \"$intArg\" of type \"Int\" used in position expecting type \"Int!\"."]
428    )
429}
430
431#[test]
432fn int_to_int_nonnull_within_fragment() {
433    use crate::validation::test_utils::*;
434
435    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
436    let errors = test_operation_with_schema(
437        "fragment nonNullIntArgFieldFrag on ComplicatedArgs {
438          nonNullIntArgField(nonNullIntArg: $intArg)
439        }
440        query Query($intArg: Int) {
441          complicatedArgs {
442            ...nonNullIntArgFieldFrag
443          }
444        }",
445        TEST_SCHEMA,
446        &mut plan,
447    );
448
449    let messages = get_messages(&errors);
450    assert_eq!(messages.len(), 1);
451    assert_eq!(
452        messages,
453        vec!["Variable \"$intArg\" of type \"Int\" used in position expecting type \"Int!\"."]
454    )
455}
456
457#[test]
458fn int_to_int_nonnull_within_nested_fragment() {
459    use crate::validation::test_utils::*;
460
461    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
462    let errors = test_operation_with_schema(
463        "fragment outerFrag on ComplicatedArgs {
464          ...nonNullIntArgFieldFrag
465        }
466        fragment nonNullIntArgFieldFrag on ComplicatedArgs {
467          nonNullIntArgField(nonNullIntArg: $intArg)
468        }
469        query Query($intArg: Int) {
470          complicatedArgs {
471            ...outerFrag
472          }
473        }",
474        TEST_SCHEMA,
475        &mut plan,
476    );
477
478    let messages = get_messages(&errors);
479    assert_eq!(messages.len(), 1);
480    assert_eq!(
481        messages,
482        vec!["Variable \"$intArg\" of type \"Int\" used in position expecting type \"Int!\"."]
483    )
484}
485
486#[test]
487fn string_over_boolean() {
488    use crate::validation::test_utils::*;
489
490    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
491    let errors = test_operation_with_schema(
492        "query Query($stringVar: String) {
493          complicatedArgs {
494            booleanArgField(booleanArg: $stringVar)
495          }
496        }",
497        TEST_SCHEMA,
498        &mut plan,
499    );
500
501    let messages = get_messages(&errors);
502    assert_eq!(messages.len(), 1);
503    assert_eq!(
504        messages,
505        vec![
506      "Variable \"$stringVar\" of type \"String\" used in position expecting type \"Boolean\"."
507    ]
508    )
509}
510
511#[test]
512fn string_over_string_list() {
513    use crate::validation::test_utils::*;
514
515    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
516    let errors = test_operation_with_schema(
517        "query Query($stringVar: String) {
518          complicatedArgs {
519            stringListArgField(stringListArg: $stringVar)
520          }
521        }",
522        TEST_SCHEMA,
523        &mut plan,
524    );
525
526    let messages = get_messages(&errors);
527    assert_eq!(messages.len(), 1);
528    assert_eq!(
529        messages,
530        vec![
531      "Variable \"$stringVar\" of type \"String\" used in position expecting type \"[String]\"."
532    ]
533    )
534}
535
536#[test]
537fn boolean_to_boolean_nonnull_in_directive() {
538    use crate::validation::test_utils::*;
539
540    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
541    let errors = test_operation_with_schema(
542        "query Query($boolVar: Boolean) {
543          dog @include(if: $boolVar)
544        }",
545        TEST_SCHEMA,
546        &mut plan,
547    );
548
549    let messages = get_messages(&errors);
550    assert_eq!(messages.len(), 1);
551    assert_eq!(
552        messages,
553        vec![
554      "Variable \"$boolVar\" of type \"Boolean\" used in position expecting type \"Boolean!\"."
555    ]
556    )
557}
558
559#[test]
560fn string_to_boolean_nonnull_in_directive() {
561    use crate::validation::test_utils::*;
562
563    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
564    let errors = test_operation_with_schema(
565        "query Query($stringVar: String) {
566          dog @include(if: $stringVar)
567        }",
568        TEST_SCHEMA,
569        &mut plan,
570    );
571
572    let messages = get_messages(&errors);
573    assert_eq!(messages.len(), 1);
574    assert_eq!(
575        messages,
576        vec![
577      "Variable \"$stringVar\" of type \"String\" used in position expecting type \"Boolean!\"."
578    ]
579    )
580}
581
582#[test]
583fn string_list_to_string_nonnull_list() {
584    use crate::validation::test_utils::*;
585
586    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
587    let errors = test_operation_with_schema(
588        "query Query($stringListVar: [String])
589        {
590          complicatedArgs {
591            stringListNonNullArgField(stringListNonNullArg: $stringListVar)
592          }
593        }",
594        TEST_SCHEMA,
595        &mut plan,
596    );
597
598    let messages = get_messages(&errors);
599    assert_eq!(messages.len(), 1);
600    assert_eq!(messages, vec![
601      "Variable \"$stringListVar\" of type \"[String]\" used in position expecting type \"[String!]\"."
602    ])
603}
604
605#[test]
606fn int_to_int_non_null_with_null_default_value() {
607    use crate::validation::test_utils::*;
608
609    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
610    let errors = test_operation_with_schema(
611        "query Query($intVar: Int = null) {
612          complicatedArgs {
613            nonNullIntArgField(nonNullIntArg: $intVar)
614          }
615        }",
616        TEST_SCHEMA,
617        &mut plan,
618    );
619
620    let messages = get_messages(&errors);
621    assert_eq!(messages.len(), 1);
622    assert_eq!(
623        messages,
624        vec!["Variable \"$intVar\" of type \"Int\" used in position expecting type \"Int!\"."]
625    )
626}
627
628#[test]
629fn int_to_int_non_null_with_default_value() {
630    use crate::validation::test_utils::*;
631
632    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
633    let errors = test_operation_with_schema(
634        "query Query($intVar: Int = 1) {
635          complicatedArgs {
636            nonNullIntArgField(nonNullIntArg: $intVar)
637          }
638        }",
639        TEST_SCHEMA,
640        &mut plan,
641    );
642
643    let messages = get_messages(&errors);
644    assert_eq!(messages.len(), 0);
645}
646
647#[test]
648fn int_to_int_non_null_where_argument_with_default_value() {
649    use crate::validation::test_utils::*;
650
651    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
652    let errors = test_operation_with_schema(
653        "query Query($intVar: Int) {
654          complicatedArgs {
655            nonNullFieldWithDefault(nonNullIntArg: $intVar)
656          }
657        }",
658        TEST_SCHEMA,
659        &mut plan,
660    );
661
662    let messages = get_messages(&errors);
663    assert_eq!(messages.len(), 0);
664}
665
666#[test]
667fn list_of_non_null_enum_with_single_enum_default_value() {
668    use crate::validation::test_utils::*;
669
670    // A single-value default on a list-typed variable is valid per
671    // GraphQL spec (single values coerce to a one-item list). The router used to
672    // reject this with the malformed type "T!!" because the expected-type
673    // computation dropped the list wrapper and double-wrapped NonNull.
674    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
675    let errors = test_operation_with_schema(
676        "query Query($enumListVar: [FurColor!] = BROWN) {
677          complicatedArgs {
678            enumListArgField(enumListArg: $enumListVar)
679          }
680        }",
681        // Augment the schema with a list-of-non-null-enum argument.
682        &(TEST_SCHEMA.replace(
683            "enumArgField(enumArg: FurColor): String",
684            "enumArgField(enumArg: FurColor): String\n  enumListArgField(enumListArg: [FurColor!]): String",
685        )),
686        &mut plan,
687    );
688
689    let messages = get_messages(&errors);
690    assert_eq!(messages, Vec::<&String>::new());
691}
692
693#[test]
694fn list_of_non_null_with_default_value_used_in_non_null_list_position() {
695    use crate::validation::test_utils::*;
696
697    // A list variable with a non-null default value should be usable in a
698    // non-null list position. Before the fix, the expected type for the
699    // subtype check was "T!!" (invalid) instead of "[T!]!".
700    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
701    let errors = test_operation_with_schema(
702        "query Query($enumListVar: [FurColor!] = BROWN) {
703          complicatedArgs {
704            requiredEnumListArgField(enumListArg: $enumListVar)
705          }
706        }",
707        &(TEST_SCHEMA.replace(
708            "enumArgField(enumArg: FurColor): String",
709            "enumArgField(enumArg: FurColor): String\n  requiredEnumListArgField(enumListArg: [FurColor!]!): String",
710        )),
711        &mut plan,
712    );
713
714    let messages = get_messages(&errors);
715    assert_eq!(messages, Vec::<&String>::new());
716}
717
718#[test]
719fn boolean_to_boolean_non_null_with_default_value() {
720    use crate::validation::test_utils::*;
721
722    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
723    let errors = test_operation_with_schema(
724        "query Query($boolVar: Boolean = false) {
725          dog @include(if: $boolVar)
726        }",
727        TEST_SCHEMA,
728        &mut plan,
729    );
730
731    let messages = get_messages(&errors);
732    assert_eq!(messages.len(), 0);
733}