apollo_compiler/validation/
variable.rs1use crate::ast;
2use crate::collections::HashMap;
3use crate::collections::HashSet;
4use crate::executable;
5use crate::validation::diagnostics::DiagnosticData;
6use crate::validation::value::value_of_correct_type;
7use crate::validation::DepthCounter;
8use crate::validation::DepthGuard;
9use crate::validation::DiagnosticList;
10use crate::validation::RecursionLimitError;
11use crate::validation::SourceSpan;
12use crate::ExecutableDocument;
13use crate::Name;
14use crate::Node;
15use std::collections::hash_map::Entry;
16
17pub(crate) fn validate_variable_definitions(
18 diagnostics: &mut DiagnosticList,
19 schema: Option<&crate::Schema>,
20 variables: &[Node<ast::VariableDefinition>],
21) {
22 let mut seen: HashMap<Name, &Node<ast::VariableDefinition>> = HashMap::default();
23 for variable in variables.iter() {
24 super::directive::validate_directives(
25 diagnostics,
26 schema,
27 variable.directives.iter(),
28 ast::DirectiveLocation::VariableDefinition,
29 Default::default(),
32 );
33
34 if let Some(schema) = &schema {
35 let ty = &variable.ty;
36 let type_definition = schema.types.get(ty.inner_named_type());
37
38 match type_definition {
39 Some(type_definition) if type_definition.is_input_type() => {
40 if let Some(default) = &variable.default_value {
41 let var_defs_in_scope = &[];
43 value_of_correct_type(diagnostics, schema, ty, default, var_defs_in_scope);
44 }
45 }
46 Some(type_definition) => {
47 diagnostics.push(
48 variable.location(),
49 DiagnosticData::VariableInputType {
50 name: variable.name.clone(),
51 ty: ty.clone(),
52 describe_type: type_definition.describe(),
53 },
54 );
55 }
56 None => diagnostics.push(
57 variable.location(),
58 DiagnosticData::UndefinedDefinition {
59 name: ty.inner_named_type().clone(),
60 },
61 ),
62 }
63 }
64
65 match seen.entry(variable.name.clone()) {
66 Entry::Occupied(original) => {
67 let original_definition = original.get().location();
68 let redefined_definition = variable.location();
69 diagnostics.push(
70 redefined_definition,
71 DiagnosticData::UniqueVariable {
72 name: variable.name.clone(),
73 original_definition,
74 redefined_definition,
75 },
76 );
77 }
78 Entry::Vacant(entry) => {
79 entry.insert(variable);
80 }
81 }
82 }
83}
84
85pub(super) fn walk_selections_with_deduped_fragments<'doc>(
94 document: &'doc ExecutableDocument,
95 selections: &'doc executable::SelectionSet,
96 mut f: impl FnMut(&'doc executable::Selection),
97) -> Result<(), RecursionLimitError> {
98 fn walk_selections_inner<'doc>(
99 document: &'doc ExecutableDocument,
100 selection_set: &'doc executable::SelectionSet,
101 seen: &mut HashSet<&'doc Name>,
102 mut guard: DepthGuard<'_>,
103 f: &mut dyn FnMut(&'doc executable::Selection),
104 ) -> Result<(), RecursionLimitError> {
105 for selection in &selection_set.selections {
106 f(selection);
107 match selection {
108 executable::Selection::Field(field) => {
109 walk_selections_inner(
110 document,
111 &field.selection_set,
112 seen,
113 guard.increment()?,
114 f,
115 )?;
116 }
117 executable::Selection::FragmentSpread(fragment) => {
118 let new = seen.insert(&fragment.fragment_name);
119 if !new {
120 continue;
121 }
122
123 if let Some(fragment_definition) =
124 document.fragments.get(&fragment.fragment_name)
125 {
126 walk_selections_inner(
127 document,
128 &fragment_definition.selection_set,
129 seen,
130 guard.increment()?,
131 f,
132 )?;
133 }
134 }
135 executable::Selection::InlineFragment(fragment) => {
136 walk_selections_inner(
137 document,
138 &fragment.selection_set,
139 seen,
140 guard.increment()?,
141 f,
142 )?;
143 }
144 }
145 }
146 Ok(())
147 }
148
149 let mut depth = DepthCounter::new().with_limit(500);
154 walk_selections_inner(
155 document,
156 selections,
157 &mut HashSet::default(),
158 depth.guard(),
159 &mut f,
160 )
161}
162
163fn variables_in_value(value: &ast::Value) -> impl Iterator<Item = &Name> + '_ {
164 let mut value_stack = vec![value];
165 std::iter::from_fn(move || {
166 while let Some(value) = value_stack.pop() {
167 match value {
168 ast::Value::Variable(variable) => return Some(variable),
169 ast::Value::List(list) => value_stack.extend(list.iter().map(|value| &**value)),
170 ast::Value::Object(fields) => {
171 value_stack.extend(fields.iter().map(|(_, value)| &**value))
172 }
173 _ => (),
174 }
175 }
176 None
177 })
178}
179
180fn variables_in_arguments(args: &[Node<ast::Argument>]) -> impl Iterator<Item = &Name> + '_ {
181 args.iter().flat_map(|arg| variables_in_value(&arg.value))
182}
183
184fn variables_in_directives(
185 directives: &[Node<ast::Directive>],
186) -> impl Iterator<Item = &Name> + '_ {
187 directives
188 .iter()
189 .flat_map(|directive| variables_in_arguments(&directive.arguments))
190}
191
192pub(crate) fn validate_unused_variables(
199 diagnostics: &mut DiagnosticList,
200 document: &ExecutableDocument,
201 operation: &executable::Operation,
202) {
203 let mut unused_vars: HashMap<_, _> = operation
205 .variables
206 .iter()
207 .map(|var| {
208 (
209 &var.name,
210 SourceSpan::recompose(var.location(), var.name.location()),
211 )
212 })
213 .collect();
214
215 for used in variables_in_directives(&operation.directives) {
217 unused_vars.remove(used);
218 }
219
220 let walked =
221 walk_selections_with_deduped_fragments(document, &operation.selection_set, |selection| {
222 match selection {
223 executable::Selection::Field(field) => {
224 for used in variables_in_directives(&field.directives) {
225 unused_vars.remove(used);
226 }
227 for used in variables_in_arguments(&field.arguments) {
228 unused_vars.remove(used);
229 }
230 }
231 executable::Selection::FragmentSpread(fragment) => {
232 if let Some(fragment_def) = document.fragments.get(&fragment.fragment_name) {
233 for used in variables_in_directives(&fragment_def.directives) {
234 unused_vars.remove(used);
235 }
236 }
237 for used in variables_in_directives(&fragment.directives) {
238 unused_vars.remove(used);
239 }
240 }
241 executable::Selection::InlineFragment(fragment) => {
242 for used in variables_in_directives(&fragment.directives) {
243 unused_vars.remove(used);
244 }
245 }
246 }
247 });
248 if walked.is_err() {
249 diagnostics.push(None, DiagnosticData::RecursionError {});
250 return;
251 }
252
253 for (unused_var, location) in unused_vars {
254 diagnostics.push(
255 location,
256 DiagnosticData::UnusedVariable {
257 name: unused_var.clone(),
258 },
259 )
260 }
261}
262
263pub(crate) fn validate_variable_usage(
264 diagnostics: &mut DiagnosticList,
265 var_usage: &Node<ast::InputValueDefinition>,
266 var_defs: &[Node<ast::VariableDefinition>],
267 argument: &Node<ast::Argument>,
268) -> Result<(), ()> {
269 if let ast::Value::Variable(var_name) = &*argument.value {
270 let var_def = var_defs.iter().find(|v| v.name == *var_name);
273 if let Some(var_def) = var_def {
274 let is_allowed = is_variable_usage_allowed(var_def, var_usage);
275 if !is_allowed {
276 diagnostics.push(
277 argument.location(),
278 DiagnosticData::DisallowedVariableUsage {
279 variable: var_def.name.clone(),
280 variable_type: (*var_def.ty).clone(),
281 variable_location: var_def.location(),
282 argument: argument.name.clone(),
283 argument_type: (*var_usage.ty).clone(),
284 argument_location: argument.location(),
285 },
286 );
287 return Err(());
288 }
289 } else {
290 }
292 }
293
294 Ok(())
295}
296
297fn is_variable_usage_allowed(
298 variable_def: &ast::VariableDefinition,
299 variable_usage: &ast::InputValueDefinition,
300) -> bool {
301 let variable_ty = &variable_def.ty;
303 let location_ty = &variable_usage.ty;
307 if location_ty.is_non_null() && !variable_ty.is_non_null() {
310 let has_non_null_default_value = variable_def.default_value.is_some();
314 let has_location_default_value = variable_usage.default_value.is_some();
318 if !has_non_null_default_value && !has_location_default_value {
322 return false;
323 }
324
325 return variable_ty.is_assignable_to(&location_ty.as_ref().clone().nullable());
328 }
329
330 variable_ty.is_assignable_to(location_ty)
331}