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