Skip to main content

graphql_query/validate/rules/
no_undefined_variables.rs

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