graphql_query/validate/rules/
no_undefined_variables.rs1use 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
19pub 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}