apollo_compiler/validation/
variable.rs

1use 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            // let's assume that variable definitions cannot reference other
30            // variables and provide them as arguments to directives
31            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                        // Default values are "const", not allowed to refer to other variables:
42                        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
85/// Call a function for every selection that is reachable from the given selection set.
86///
87/// This includes fields, fragment spreads, and inline fragments. For fragments, both the spread
88/// and the fragment's nested selections are reported. For fields, nested selections are also
89/// reported.
90///
91/// Named fragments are "deduplicated": only visited once even if spread multiple times *in
92/// different locations*. This is only appropriate for certain kinds of validations, so reuser beware.
93pub(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    // This has a much higher limit than comparable recursive walks, like the one in
150    // `validate_fragment_cycles`, despite doing similar work. This is because this limit
151    // was introduced later and should not break (reasonable) existing queries that are
152    // under that pre-existing limit. Luckily the existing limit was very conservative.
153    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
192// TODO add test:
193// should NOT report a unused variable warning
194// query ($var1: Boolean!, $var2: Boolean!) {
195//   a: field (arg: $var1)
196//   a: field (arg: $var2)
197// }
198pub(crate) fn validate_unused_variables(
199    diagnostics: &mut DiagnosticList,
200    document: &ExecutableDocument,
201    operation: &executable::Operation,
202) {
203    // Start off by considering all variables unused: names are removed from this as we find them.
204    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    // You're allowed to do `query($var: Int!) @dir(arg: $var) {}`
216    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 be the VariableDefinition named
271        // variable_name defined within operation.
272        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            // If the variable is not defined, we raise an error in `value.rs`
291        }
292    }
293
294    Ok(())
295}
296
297fn is_variable_usage_allowed(
298    variable_def: &ast::VariableDefinition,
299    variable_usage: &ast::InputValueDefinition,
300) -> bool {
301    // 1. Let variable_ty be the expected type of variable_def.
302    let variable_ty = &variable_def.ty;
303    // 2. Let location_ty be the expected type of the Argument,
304    // ObjectField, or ListValue entry where variableUsage is
305    // located.
306    let location_ty = &variable_usage.ty;
307    // 3. if location_ty is a non-null type AND variable_ty is
308    // NOT a non-null type:
309    if location_ty.is_non_null() && !variable_ty.is_non_null() {
310        // 3.a. let hasNonNullVariableDefaultValue be true
311        // if a default value exists for variableDefinition
312        // and is not the value null.
313        let has_non_null_default_value = variable_def.default_value.is_some();
314        // 3.b. Let hasLocationDefaultValue be true if a default
315        // value exists for the Argument or ObjectField where
316        // variableUsage is located.
317        let has_location_default_value = variable_usage.default_value.is_some();
318        // 3.c. If hasNonNullVariableDefaultValue is NOT true
319        // AND hasLocationDefaultValue is NOT true, return
320        // false.
321        if !has_non_null_default_value && !has_location_default_value {
322            return false;
323        }
324
325        // 3.d. Let nullable_location_ty be the unwrapped
326        // nullable type of location_ty.
327        return variable_ty.is_assignable_to(&location_ty.as_ref().clone().nullable());
328    }
329
330    variable_ty.is_assignable_to(location_ty)
331}