graphql_query/validate/rules/
no_undefined_variables.rs

1use hashbrown::HashMap;
2
3use super::super::{ValidationContext, ValidationRule};
4use crate::{ast::*, visit::*};
5
6#[derive(Default, Clone)]
7struct OperationEdge<'a> {
8    defined_vars: Vec<&'a str>,
9    used_fragments: Vec<&'a str>,
10}
11
12#[derive(Default, Clone)]
13struct FragmentEdge<'a> {
14    used_vars: Vec<&'a str>,
15    used_fragments: Vec<&'a str>,
16}
17
18/// Validate that a document defines all the variables it uses per operation
19///
20/// See [`ValidationRule`]
21/// [Reference](https://spec.graphql.org/October2021/#sec-All-Variable-Uses-Defined)
22#[derive(Default)]
23pub struct NoUndefinedVariables<'a> {
24    used_vars: Vec<&'a str>,
25    defined_vars: Vec<&'a str>,
26    used_fragments: Vec<&'a str>,
27    operation_edges: std::vec::Vec<OperationEdge<'a>>,
28    fragment_edges: HashMap<&'a str, FragmentEdge<'a>>,
29}
30
31impl<'a> ValidationRule<'a> for NoUndefinedVariables<'a> {}
32
33impl<'a> Visitor<'a, ValidationContext<'a>> for NoUndefinedVariables<'a> {
34    fn enter_variable_definition(
35        &mut self,
36        _ctx: &mut ValidationContext<'a>,
37        var_def: &'a VariableDefinition<'a>,
38        _info: &VisitInfo,
39    ) -> VisitFlow {
40        self.defined_vars.push(var_def.variable.name);
41        VisitFlow::Skip
42    }
43
44    fn enter_argument(
45        &mut self,
46        _ctx: &mut ValidationContext<'a>,
47        argument: &'a Argument,
48        _info: &VisitInfo,
49    ) -> VisitFlow {
50        if let Value::Variable(var) = argument.value {
51            self.used_vars.push(var.name);
52        }
53        VisitFlow::Skip
54    }
55
56    fn leave_operation(
57        &mut self,
58        ctx: &mut ValidationContext<'a>,
59        _operation: &'a OperationDefinition<'a>,
60        _info: &VisitInfo,
61    ) -> VisitFlow {
62        for var in self.used_vars.iter() {
63            if !self.defined_vars.contains(var) {
64                ctx.add_error(
65                    "All variables used within operations must be defined on the operation",
66                );
67                return VisitFlow::Break;
68            }
69        }
70        self.operation_edges.push(OperationEdge {
71            defined_vars: self.defined_vars.clone(),
72            used_fragments: self.used_fragments.clone(),
73        });
74        self.used_fragments.clear();
75        self.used_vars.clear();
76        self.defined_vars.clear();
77        VisitFlow::Next
78    }
79
80    fn leave_fragment(
81        &mut self,
82        _ctx: &mut ValidationContext<'a>,
83        fragment: &'a FragmentDefinition<'a>,
84        _info: &VisitInfo,
85    ) -> VisitFlow {
86        let name = fragment.name.name;
87        self.fragment_edges.insert(
88            name,
89            FragmentEdge {
90                used_vars: self.used_vars.clone(),
91                used_fragments: self.used_fragments.clone(),
92            },
93        );
94        self.used_fragments.clear();
95        self.used_vars.clear();
96        VisitFlow::Next
97    }
98
99    fn enter_fragment_spread(
100        &mut self,
101        _ctx: &mut ValidationContext<'a>,
102        spread: &'a FragmentSpread<'a>,
103        _info: &VisitInfo,
104    ) -> VisitFlow {
105        self.used_fragments.push(spread.name.name);
106        VisitFlow::Skip
107    }
108
109    fn leave_document(
110        &mut self,
111        ctx: &mut ValidationContext<'a>,
112        _document: &'a Document<'a>,
113        _info: &VisitInfo,
114    ) -> VisitFlow {
115        let mut visited: Vec<&'a str> = Vec::default();
116        for operation_edge in self.operation_edges.iter() {
117            if references_undefined_var(
118                &mut visited,
119                &self.fragment_edges,
120                &operation_edge.defined_vars,
121                &operation_edge.used_fragments,
122            ) {
123                ctx.add_error("All variables within fragments must be defined on the operation they're used in");
124                return VisitFlow::Break;
125            }
126            visited.clear();
127        }
128        VisitFlow::Next
129    }
130}
131
132fn references_undefined_var<'a>(
133    visited: &mut Vec<&'a str>,
134    fragment_edges: &HashMap<&'a str, FragmentEdge<'a>>,
135    defined_vars: &Vec<&'a str>,
136    used_fragments: &Vec<&'a str>,
137) -> bool {
138    for fragment_name in used_fragments {
139        if !visited.contains(fragment_name) {
140            visited.push(fragment_name);
141            if let Some(edge) = fragment_edges.get(fragment_name) {
142                for var in edge.used_vars.iter() {
143                    if !defined_vars.contains(var) {
144                        return true;
145                    }
146                }
147                if references_undefined_var(
148                    visited,
149                    fragment_edges,
150                    defined_vars,
151                    &edge.used_fragments,
152                ) {
153                    return true;
154                }
155            }
156        }
157    }
158    false
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn defined_vars() {
167        let ctx = ASTContext::new();
168        let document = Document::parse(&ctx, "query($var: Int) { field(x: $var), ...Frag } fragment Frag on Query { field(x: $var) } ").unwrap();
169        NoUndefinedVariables::validate(&ctx, document).unwrap();
170    }
171
172    #[test]
173    fn undefined_vars_on_operation() {
174        let ctx = ASTContext::new();
175        let document = Document::parse(&ctx, "query { field(x: $var) }").unwrap();
176        NoUndefinedVariables::validate(&ctx, document).unwrap_err();
177    }
178
179    #[test]
180    fn undefined_vars_on_fragments() {
181        let ctx = ASTContext::new();
182        let document = Document::parse(
183            &ctx,
184            "query { ...Frag } fragment Frag on Query { field(x: $var) } ",
185        )
186        .unwrap();
187        NoUndefinedVariables::validate(&ctx, document).unwrap_err();
188        let document = Document::parse(
189            &ctx,
190            "query { ...A } fragment A on A { ...B } fragment B on B { field(x: $var) } ",
191        )
192        .unwrap();
193        NoUndefinedVariables::validate(&ctx, document).unwrap_err();
194    }
195}