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                    let expected_type = match (&var_def.default_value, &var_def.var_type) {
52                        (Some(_), Type::ListType(inner)) => Type::NonNullType(inner.clone()),
53                        (Some(default_value), Type::NamedType(_)) => {
54                            if let Value::Null = default_value {
55                                var_def.var_type.clone()
56                            } else {
57                                Type::NonNullType(Box::new(var_def.var_type.clone()))
58                            }
59                        }
60                        (_, t) => t.clone(),
61                    };
62
63                    if !visitor_context.schema.is_subtype(&expected_type, var_type) {
64                        user_context.report_error(ValidationError {
65                          error_code: self.error_code(),
66                            message: format!("Variable \"${}\" of type \"{}\" used in position expecting type \"{}\".",
67                                var_name,
68                                expected_type,
69                                var_type,
70                            ),
71                            locations: vec![var_def.position],
72                        });
73                    }
74                }
75            }
76        }
77
78        if let Some(spreads) = self.spreads.get(from) {
79            for spread in spreads {
80                self.collect_incorrect_usages(
81                    &Scope::Fragment(spread),
82                    var_defs,
83                    visitor_context,
84                    user_context,
85                    visited,
86                );
87            }
88        }
89    }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Hash)]
93pub enum Scope<'a> {
94    Operation(Option<&'a str>),
95    Fragment(&'a str),
96}
97
98impl<'a> OperationVisitor<'a, ValidationErrorContext> for VariablesInAllowedPosition<'a> {
99    fn leave_document(
100        &mut self,
101        visitor_context: &mut OperationVisitorContext<'a>,
102        user_context: &mut ValidationErrorContext,
103        _: &crate::static_graphql::query::Document,
104    ) {
105        for (op_scope, var_defs) in &self.variable_defs {
106            self.collect_incorrect_usages(
107                op_scope,
108                var_defs,
109                visitor_context,
110                user_context,
111                &mut HashSet::new(),
112            );
113        }
114    }
115
116    fn enter_fragment_definition(
117        &mut self,
118        _: &mut OperationVisitorContext<'a>,
119        _: &mut ValidationErrorContext,
120        fragment_definition: &'a crate::static_graphql::query::FragmentDefinition,
121    ) {
122        self.current_scope = Some(Scope::Fragment(&fragment_definition.name));
123    }
124
125    fn enter_operation_definition(
126        &mut self,
127        _: &mut OperationVisitorContext<'a>,
128        _: &mut ValidationErrorContext,
129        operation_definition: &'a crate::static_graphql::query::OperationDefinition,
130    ) {
131        self.current_scope = Some(Scope::Operation(operation_definition.node_name()));
132    }
133
134    fn enter_fragment_spread(
135        &mut self,
136        _: &mut OperationVisitorContext<'a>,
137        _: &mut ValidationErrorContext,
138        fragment_spread: &'a crate::static_graphql::query::FragmentSpread,
139    ) {
140        if let Some(scope) = &self.current_scope {
141            self.spreads
142                .entry(scope.clone())
143                .or_default()
144                .insert(&fragment_spread.fragment_name);
145        }
146    }
147
148    fn enter_variable_definition(
149        &mut self,
150        _: &mut OperationVisitorContext<'a>,
151        _: &mut ValidationErrorContext,
152        variable_definition: &'a VariableDefinition,
153    ) {
154        if let Some(ref scope) = self.current_scope {
155            self.variable_defs
156                .entry(scope.clone())
157                .or_default()
158                .push(variable_definition);
159        }
160    }
161
162    fn enter_variable_value(
163        &mut self,
164        visitor_context: &mut OperationVisitorContext<'a>,
165        _: &mut ValidationErrorContext,
166        variable_name: &'a str,
167    ) {
168        if let (Some(scope), Some(input_type)) = (
169            &self.current_scope,
170            visitor_context.current_input_type_literal(),
171        ) {
172            self.variable_usages
173                .entry(scope.clone())
174                .or_default()
175                .push((variable_name, input_type));
176        }
177    }
178}
179
180impl<'v> ValidationRule for VariablesInAllowedPosition<'v> {
181    fn error_code<'a>(&self) -> &'a str {
182        "VariablesInAllowedPosition"
183    }
184
185    fn validate(
186        &self,
187        ctx: &mut OperationVisitorContext,
188        error_collector: &mut ValidationErrorContext,
189    ) {
190        visit_document(
191            &mut VariablesInAllowedPosition::new(),
192            ctx.operation,
193            ctx,
194            error_collector,
195        );
196    }
197}
198
199#[test]
200fn boolean_to_boolean() {
201    use crate::validation::test_utils::*;
202
203    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
204    let errors = test_operation_with_schema(
205        "query Query($booleanArg: Boolean)
206        {
207          complicatedArgs {
208            booleanArgField(booleanArg: $booleanArg)
209          }
210        }",
211        TEST_SCHEMA,
212        &mut plan,
213    );
214
215    assert_eq!(get_messages(&errors).len(), 0);
216}
217
218#[test]
219fn boolean_to_boolean_within_fragment() {
220    use crate::validation::test_utils::*;
221
222    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
223    let errors = test_operation_with_schema(
224        "fragment booleanArgFrag on ComplicatedArgs {
225          booleanArgField(booleanArg: $booleanArg)
226        }
227        query Query($booleanArg: Boolean)
228        {
229          complicatedArgs {
230            ...booleanArgFrag
231          }
232        }",
233        TEST_SCHEMA,
234        &mut plan,
235    );
236
237    assert_eq!(get_messages(&errors).len(), 0);
238
239    let errors = test_operation_with_schema(
240        "query Query($booleanArg: Boolean)
241      {
242        complicatedArgs {
243          ...booleanArgFrag
244        }
245      }
246      fragment booleanArgFrag on ComplicatedArgs {
247        booleanArgField(booleanArg: $booleanArg)
248      }",
249        TEST_SCHEMA,
250        &mut plan,
251    );
252
253    assert_eq!(get_messages(&errors).len(), 0);
254}
255
256#[test]
257fn boolean_nonnull_to_boolean() {
258    use crate::validation::test_utils::*;
259
260    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
261    let errors = test_operation_with_schema(
262        "query Query($nonNullBooleanArg: Boolean!)
263        {
264          complicatedArgs {
265            booleanArgField(booleanArg: $nonNullBooleanArg)
266          }
267        }",
268        TEST_SCHEMA,
269        &mut plan,
270    );
271
272    assert_eq!(get_messages(&errors).len(), 0);
273}
274
275#[test]
276fn string_list_to_string_list() {
277    use crate::validation::test_utils::*;
278
279    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
280    let errors = test_operation_with_schema(
281        "query Query($stringListVar: [String])
282        {
283          complicatedArgs {
284            stringListArgField(stringListArg: $stringListVar)
285          }
286        }",
287        TEST_SCHEMA,
288        &mut plan,
289    );
290
291    assert_eq!(get_messages(&errors).len(), 0);
292}
293
294#[test]
295fn string_list_nonnull_to_string_list() {
296    use crate::validation::test_utils::*;
297
298    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
299    let errors = test_operation_with_schema(
300        "query Query($stringListVar: [String!])
301        {
302          complicatedArgs {
303            stringListArgField(stringListArg: $stringListVar)
304          }
305        }",
306        TEST_SCHEMA,
307        &mut plan,
308    );
309
310    assert_eq!(get_messages(&errors).len(), 0);
311}
312
313#[test]
314fn string_to_string_list_in_item_position() {
315    use crate::validation::test_utils::*;
316
317    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
318    let errors = test_operation_with_schema(
319        "query Query($stringVar: String)
320        {
321          complicatedArgs {
322            stringListArgField(stringListArg: [$stringVar])
323          }
324        }",
325        TEST_SCHEMA,
326        &mut plan,
327    );
328
329    assert_eq!(get_messages(&errors).len(), 0);
330}
331
332#[test]
333fn string_nonnull_to_string_list_in_item_position() {
334    use crate::validation::test_utils::*;
335
336    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
337    let errors = test_operation_with_schema(
338        "query Query($stringVar: String!)
339        {
340          complicatedArgs {
341            stringListArgField(stringListArg: [$stringVar])
342          }
343        }",
344        TEST_SCHEMA,
345        &mut plan,
346    );
347
348    assert_eq!(get_messages(&errors).len(), 0);
349}
350
351#[test]
352fn complexinput_to_complexinput() {
353    use crate::validation::test_utils::*;
354
355    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
356    let errors = test_operation_with_schema(
357        "query Query($complexVar: ComplexInput)
358        {
359          complicatedArgs {
360            complexArgField(complexArg: $complexVar)
361          }
362        }",
363        TEST_SCHEMA,
364        &mut plan,
365    );
366
367    assert_eq!(get_messages(&errors).len(), 0);
368}
369
370#[test]
371fn complexinput_to_complexinput_in_field_position() {
372    use crate::validation::test_utils::*;
373
374    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
375    let errors = test_operation_with_schema(
376        "query Query($boolVar: Boolean = false)
377        {
378          complicatedArgs {
379            complexArgField(complexArg: { requiredArg: $boolVar })
380          }
381        }",
382        TEST_SCHEMA,
383        &mut plan,
384    );
385
386    let messages = get_messages(&errors);
387    assert_eq!(messages.len(), 0);
388}
389
390#[test]
391fn boolean_nonnull_to_boolean_nonnull_in_directive() {
392    use crate::validation::test_utils::*;
393
394    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
395    let errors = test_operation_with_schema(
396        "query Query($boolVar: Boolean!)
397        {
398          dog @include(if: $boolVar)
399        }",
400        TEST_SCHEMA,
401        &mut plan,
402    );
403
404    assert_eq!(get_messages(&errors).len(), 0);
405}
406
407#[test]
408fn int_to_int_nonnull() {
409    use crate::validation::test_utils::*;
410
411    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
412    let errors = test_operation_with_schema(
413        "query Query($intArg: Int) {
414          complicatedArgs {
415            nonNullIntArgField(nonNullIntArg: $intArg)
416          }
417        }",
418        TEST_SCHEMA,
419        &mut plan,
420    );
421
422    let messages = get_messages(&errors);
423    assert_eq!(messages.len(), 1);
424    assert_eq!(
425        messages,
426        vec!["Variable \"$intArg\" of type \"Int\" used in position expecting type \"Int!\"."]
427    )
428}
429
430#[test]
431fn int_to_int_nonnull_within_fragment() {
432    use crate::validation::test_utils::*;
433
434    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
435    let errors = test_operation_with_schema(
436        "fragment nonNullIntArgFieldFrag on ComplicatedArgs {
437          nonNullIntArgField(nonNullIntArg: $intArg)
438        }
439        query Query($intArg: Int) {
440          complicatedArgs {
441            ...nonNullIntArgFieldFrag
442          }
443        }",
444        TEST_SCHEMA,
445        &mut plan,
446    );
447
448    let messages = get_messages(&errors);
449    assert_eq!(messages.len(), 1);
450    assert_eq!(
451        messages,
452        vec!["Variable \"$intArg\" of type \"Int\" used in position expecting type \"Int!\"."]
453    )
454}
455
456#[test]
457fn int_to_int_nonnull_within_nested_fragment() {
458    use crate::validation::test_utils::*;
459
460    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
461    let errors = test_operation_with_schema(
462        "fragment outerFrag on ComplicatedArgs {
463          ...nonNullIntArgFieldFrag
464        }
465        fragment nonNullIntArgFieldFrag on ComplicatedArgs {
466          nonNullIntArgField(nonNullIntArg: $intArg)
467        }
468        query Query($intArg: Int) {
469          complicatedArgs {
470            ...outerFrag
471          }
472        }",
473        TEST_SCHEMA,
474        &mut plan,
475    );
476
477    let messages = get_messages(&errors);
478    assert_eq!(messages.len(), 1);
479    assert_eq!(
480        messages,
481        vec!["Variable \"$intArg\" of type \"Int\" used in position expecting type \"Int!\"."]
482    )
483}
484
485#[test]
486fn string_over_boolean() {
487    use crate::validation::test_utils::*;
488
489    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
490    let errors = test_operation_with_schema(
491        "query Query($stringVar: String) {
492          complicatedArgs {
493            booleanArgField(booleanArg: $stringVar)
494          }
495        }",
496        TEST_SCHEMA,
497        &mut plan,
498    );
499
500    let messages = get_messages(&errors);
501    assert_eq!(messages.len(), 1);
502    assert_eq!(
503        messages,
504        vec![
505      "Variable \"$stringVar\" of type \"String\" used in position expecting type \"Boolean\"."
506    ]
507    )
508}
509
510#[test]
511fn string_over_string_list() {
512    use crate::validation::test_utils::*;
513
514    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
515    let errors = test_operation_with_schema(
516        "query Query($stringVar: String) {
517          complicatedArgs {
518            stringListArgField(stringListArg: $stringVar)
519          }
520        }",
521        TEST_SCHEMA,
522        &mut plan,
523    );
524
525    let messages = get_messages(&errors);
526    assert_eq!(messages.len(), 1);
527    assert_eq!(
528        messages,
529        vec![
530      "Variable \"$stringVar\" of type \"String\" used in position expecting type \"[String]\"."
531    ]
532    )
533}
534
535#[test]
536fn boolean_to_boolean_nonnull_in_directive() {
537    use crate::validation::test_utils::*;
538
539    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
540    let errors = test_operation_with_schema(
541        "query Query($boolVar: Boolean) {
542          dog @include(if: $boolVar)
543        }",
544        TEST_SCHEMA,
545        &mut plan,
546    );
547
548    let messages = get_messages(&errors);
549    assert_eq!(messages.len(), 1);
550    assert_eq!(
551        messages,
552        vec![
553      "Variable \"$boolVar\" of type \"Boolean\" used in position expecting type \"Boolean!\"."
554    ]
555    )
556}
557
558#[test]
559fn string_to_boolean_nonnull_in_directive() {
560    use crate::validation::test_utils::*;
561
562    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
563    let errors = test_operation_with_schema(
564        "query Query($stringVar: String) {
565          dog @include(if: $stringVar)
566        }",
567        TEST_SCHEMA,
568        &mut plan,
569    );
570
571    let messages = get_messages(&errors);
572    assert_eq!(messages.len(), 1);
573    assert_eq!(
574        messages,
575        vec![
576      "Variable \"$stringVar\" of type \"String\" used in position expecting type \"Boolean!\"."
577    ]
578    )
579}
580
581#[test]
582fn string_list_to_string_nonnull_list() {
583    use crate::validation::test_utils::*;
584
585    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
586    let errors = test_operation_with_schema(
587        "query Query($stringListVar: [String])
588        {
589          complicatedArgs {
590            stringListNonNullArgField(stringListNonNullArg: $stringListVar)
591          }
592        }",
593        TEST_SCHEMA,
594        &mut plan,
595    );
596
597    let messages = get_messages(&errors);
598    assert_eq!(messages.len(), 1);
599    assert_eq!(messages, vec![
600      "Variable \"$stringListVar\" of type \"[String]\" used in position expecting type \"[String!]\"."
601    ])
602}
603
604#[test]
605fn int_to_int_non_null_with_null_default_value() {
606    use crate::validation::test_utils::*;
607
608    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
609    let errors = test_operation_with_schema(
610        "query Query($intVar: Int = null) {
611          complicatedArgs {
612            nonNullIntArgField(nonNullIntArg: $intVar)
613          }
614        }",
615        TEST_SCHEMA,
616        &mut plan,
617    );
618
619    let messages = get_messages(&errors);
620    assert_eq!(messages.len(), 1);
621    assert_eq!(
622        messages,
623        vec!["Variable \"$intVar\" of type \"Int\" used in position expecting type \"Int!\"."]
624    )
625}
626
627#[test]
628fn int_to_int_non_null_with_default_value() {
629    use crate::validation::test_utils::*;
630
631    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
632    let errors = test_operation_with_schema(
633        "query Query($intVar: Int = 1) {
634          complicatedArgs {
635            nonNullIntArgField(nonNullIntArg: $intVar)
636          }
637        }",
638        TEST_SCHEMA,
639        &mut plan,
640    );
641
642    let messages = get_messages(&errors);
643    assert_eq!(messages.len(), 0);
644}
645
646#[test]
647fn int_to_int_non_null_where_argument_with_default_value() {
648    use crate::validation::test_utils::*;
649
650    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
651    let errors = test_operation_with_schema(
652        "query Query($intVar: Int) {
653          complicatedArgs {
654            nonNullFieldWithDefault(nonNullIntArg: $intVar)
655          }
656        }",
657        TEST_SCHEMA,
658        &mut plan,
659    );
660
661    let messages = get_messages(&errors);
662    assert_eq!(messages.len(), 0);
663}
664
665#[test]
666fn boolean_to_boolean_non_null_with_default_value() {
667    use crate::validation::test_utils::*;
668
669    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
670    let errors = test_operation_with_schema(
671        "query Query($boolVar: Boolean = false) {
672          dog @include(if: $boolVar)
673        }",
674        TEST_SCHEMA,
675        &mut plan,
676    );
677
678    let messages = get_messages(&errors);
679    assert_eq!(messages.len(), 0);
680}