Skip to main content

apollo_compiler/validation/
directive.rs

1use super::CycleError;
2use crate::ast;
3use crate::collections::HashMap;
4use crate::collections::HashSet;
5use crate::coordinate::DirectiveArgumentCoordinate;
6use crate::coordinate::DirectiveCoordinate;
7use crate::schema;
8use crate::schema::validation::BuiltInScalars;
9use crate::validation::diagnostics::DiagnosticData;
10use crate::validation::DiagnosticList;
11use crate::validation::RecursionGuard;
12use crate::validation::RecursionStack;
13use crate::validation::SourceSpan;
14use crate::Node;
15
16/// This struct just groups functions that are used to find self-referential directives.
17/// The way to use it is to call `FindRecursiveDirective::check`.
18struct FindRecursiveDirective<'s> {
19    schema: &'s schema::Schema,
20}
21
22impl FindRecursiveDirective<'_> {
23    fn type_definition(
24        &self,
25        directive_guard: &mut RecursionGuard<'_>,
26        type_guard: &mut RecursionGuard<'_>,
27        def: &schema::ExtendedType,
28    ) -> Result<(), CycleError<ast::Directive>> {
29        match def {
30            schema::ExtendedType::Scalar(scalar_type_definition) => {
31                self.directives(
32                    directive_guard,
33                    type_guard,
34                    &scalar_type_definition.directives,
35                )?;
36            }
37            schema::ExtendedType::Object(object_type_definition) => {
38                self.directives(
39                    directive_guard,
40                    type_guard,
41                    &object_type_definition.directives,
42                )?;
43            }
44            schema::ExtendedType::Interface(interface_type_definition) => {
45                self.directives(
46                    directive_guard,
47                    type_guard,
48                    &interface_type_definition.directives,
49                )?;
50            }
51            schema::ExtendedType::Union(union_type_definition) => {
52                self.directives(
53                    directive_guard,
54                    type_guard,
55                    &union_type_definition.directives,
56                )?;
57            }
58            schema::ExtendedType::Enum(enum_type_definition) => {
59                self.directives(
60                    directive_guard,
61                    type_guard,
62                    &enum_type_definition.directives,
63                )?;
64                for enum_value in enum_type_definition.values.values() {
65                    self.enum_value(directive_guard, type_guard, enum_value)?;
66                }
67            }
68            schema::ExtendedType::InputObject(input_type_definition) => {
69                self.directives(
70                    directive_guard,
71                    type_guard,
72                    &input_type_definition.directives,
73                )?;
74                for input_value in input_type_definition.fields.values() {
75                    self.input_value(directive_guard, type_guard, input_value)?;
76                }
77            }
78        }
79
80        Ok(())
81    }
82
83    fn input_value(
84        &self,
85        directive_guard: &mut RecursionGuard<'_>,
86        type_guard: &mut RecursionGuard<'_>,
87        input_value: &Node<ast::InputValueDefinition>,
88    ) -> Result<(), CycleError<ast::Directive>> {
89        for directive in &input_value.directives {
90            self.directive(directive_guard, type_guard, directive)?;
91        }
92
93        let type_name = input_value.ty.inner_named_type();
94        if let Some(type_def) = self.schema.types.get(type_name) {
95            if type_guard.contains(type_def.name()) {
96                // input type was already processed
97                return Ok(());
98            }
99            if !type_def.is_built_in() {
100                let mut new_type_guard = type_guard.push(type_def.name())?;
101                self.type_definition(directive_guard, &mut new_type_guard, type_def)?;
102            } else {
103                self.type_definition(directive_guard, type_guard, type_def)?;
104            }
105        }
106
107        Ok(())
108    }
109
110    fn enum_value(
111        &self,
112        directive_guard: &mut RecursionGuard<'_>,
113        type_guard: &mut RecursionGuard<'_>,
114        enum_value: &Node<ast::EnumValueDefinition>,
115    ) -> Result<(), CycleError<ast::Directive>> {
116        for directive in &enum_value.directives {
117            self.directive(directive_guard, type_guard, directive)?;
118        }
119
120        Ok(())
121    }
122
123    fn directives(
124        &self,
125        directive_guard: &mut RecursionGuard<'_>,
126        type_guard: &mut RecursionGuard<'_>,
127        directives: &[schema::Component<ast::Directive>],
128    ) -> Result<(), CycleError<ast::Directive>> {
129        for directive in directives {
130            self.directive(directive_guard, type_guard, directive)?;
131        }
132        Ok(())
133    }
134
135    fn directive(
136        &self,
137        directive_guard: &mut RecursionGuard<'_>,
138        type_guard: &mut RecursionGuard<'_>,
139        directive: &Node<ast::Directive>,
140    ) -> Result<(), CycleError<ast::Directive>> {
141        if !directive_guard.contains(&directive.name) {
142            if let Some(def) = self.schema.directive_definitions.get(&directive.name) {
143                let mut new_directive_guard = directive_guard.push(&directive.name)?;
144                self.directive_definition(&mut new_directive_guard, type_guard, def)
145                    .map_err(|error| error.trace(directive))?;
146            }
147        } else if directive_guard.first() == Some(&directive.name) {
148            // Only report an error & bail out early if this is the *initial* directive.
149            // This prevents raising confusing errors when a directive `@b` which is not
150            // self-referential uses a directive `@a` that *is*. The error with `@a` should
151            // only be reported on its definition, not on `@b`'s.
152            return Err(CycleError::Recursed(vec![directive.clone()]));
153        }
154
155        Ok(())
156    }
157
158    fn directive_definition(
159        &self,
160        directive_guard: &mut RecursionGuard<'_>,
161        type_guard: &mut RecursionGuard<'_>,
162        def: &Node<ast::DirectiveDefinition>,
163    ) -> Result<(), CycleError<ast::Directive>> {
164        for input_value in &def.arguments {
165            self.input_value(directive_guard, type_guard, input_value)?;
166        }
167
168        Ok(())
169    }
170
171    fn check(
172        schema: &schema::Schema,
173        directive_def: &Node<ast::DirectiveDefinition>,
174    ) -> Result<(), CycleError<ast::Directive>> {
175        let mut directive_stack = RecursionStack::with_root(directive_def.name.clone());
176        let mut directive_guard = directive_stack.guard();
177        let mut type_stack = RecursionStack::new();
178        let mut type_guard = type_stack.guard();
179        FindRecursiveDirective { schema }.directive_definition(
180            &mut directive_guard,
181            &mut type_guard,
182            directive_def,
183        )
184    }
185}
186
187pub(crate) fn validate_directive_definition(
188    diagnostics: &mut DiagnosticList,
189    schema: &crate::Schema,
190    built_in_scalars: &mut BuiltInScalars,
191    def: &Node<ast::DirectiveDefinition>,
192) {
193    schema::validation::validate_type_system_name(diagnostics, &def.name, "a directive definition");
194    super::input_object::validate_argument_definitions(
195        diagnostics,
196        schema,
197        built_in_scalars,
198        &def.arguments,
199        ast::DirectiveLocation::ArgumentDefinition,
200    );
201
202    let head_location = SourceSpan::recompose(def.location(), def.name.location());
203
204    // A directive definition must not contain the use of a directive which
205    // references itself directly.
206    //
207    // Returns Recursive Definition error.
208    match FindRecursiveDirective::check(schema, def) {
209        Ok(_) => {}
210        Err(CycleError::Recursed(trace)) => {
211            diagnostics.push(
212                head_location,
213                DiagnosticData::RecursiveDirectiveDefinition {
214                    name: def.name.clone(),
215                    trace,
216                },
217            );
218        }
219        Err(CycleError::Limit(_)) => diagnostics.push(
220            head_location,
221            DiagnosticData::DeeplyNestedType {
222                name: def.name.clone(),
223                describe_type: "directive",
224            },
225        ),
226    }
227}
228
229pub(crate) fn validate_directive_definitions(
230    diagnostics: &mut DiagnosticList,
231    schema: &crate::Schema,
232    built_in_scalars: &mut BuiltInScalars,
233) {
234    for directive_definition in schema.directive_definitions.values() {
235        validate_directive_definition(diagnostics, schema, built_in_scalars, directive_definition);
236    }
237}
238
239// TODO(@goto-bus-stop) This is a big function: should probably not be generic over the iterator
240// type
241pub(crate) fn validate_directives<'dir>(
242    diagnostics: &mut DiagnosticList,
243    schema: Option<&crate::Schema>,
244    dirs: impl Iterator<Item = &'dir Node<ast::Directive>>,
245    dir_loc: ast::DirectiveLocation,
246    var_defs: &[Node<ast::VariableDefinition>],
247) {
248    let mut seen_directives = HashMap::<_, Option<SourceSpan>>::default();
249
250    for dir in dirs {
251        super::argument::validate_arguments(diagnostics, &dir.arguments);
252
253        let name = &dir.name;
254        let loc = dir.location();
255        let directive_definition =
256            schema.and_then(|schema| Some((schema, schema.directive_definitions.get(name)?)));
257
258        if let Some(&original_loc) = seen_directives.get(name) {
259            let is_repeatable = directive_definition
260                .map(|(_, def)| def.repeatable)
261                // Assume unknown directives are repeatable to avoid producing confusing diagnostics
262                .unwrap_or(true);
263
264            if !is_repeatable {
265                diagnostics.push(
266                    loc,
267                    DiagnosticData::UniqueDirective {
268                        name: name.clone(),
269                        original_application: original_loc,
270                    },
271                );
272            }
273        } else {
274            let loc = SourceSpan::recompose(dir.location(), dir.name.location());
275            seen_directives.insert(&dir.name, loc);
276        }
277
278        if let Some((schema, directive_definition)) = directive_definition {
279            let allowed_loc: HashSet<ast::DirectiveLocation> =
280                HashSet::from_iter(directive_definition.locations.iter().cloned());
281            if !allowed_loc.contains(&dir_loc) {
282                diagnostics.push(
283                    loc,
284                    DiagnosticData::UnsupportedLocation {
285                        name: name.clone(),
286                        location: dir_loc,
287                        valid_locations: directive_definition.locations.clone(),
288                        definition_location: directive_definition.location(),
289                    },
290                );
291            }
292
293            for argument in &dir.arguments {
294                let input_value = directive_definition
295                    .arguments
296                    .iter()
297                    .find(|val| val.name == argument.name);
298
299                // @b(a: true)
300                if let Some(input_value) = input_value {
301                    // TODO(@goto-bus-stop) do we really need value validation and variable
302                    // validation separately?
303                    if super::variable::validate_variable_usage(
304                        diagnostics,
305                        input_value,
306                        var_defs,
307                        argument,
308                    )
309                    .is_ok()
310                    {
311                        super::value::validate_values(
312                            diagnostics,
313                            schema,
314                            &input_value.ty,
315                            argument,
316                            var_defs,
317                        );
318                    }
319                } else {
320                    diagnostics.push(
321                        argument.location(),
322                        DiagnosticData::UndefinedArgument {
323                            name: argument.name.clone(),
324                            coordinate: DirectiveCoordinate {
325                                directive: dir.name.clone(),
326                            }
327                            .into(),
328                            definition_location: loc,
329                        },
330                    );
331                }
332            }
333            for arg_def in &directive_definition.arguments {
334                let arg_value = dir
335                    .arguments
336                    .iter()
337                    .find_map(|arg| (arg.name == arg_def.name).then_some(&arg.value));
338                let is_null = match arg_value {
339                    None => true,
340                    // Prevents explicitly providing `requiredArg: null`,
341                    // but you can still indirectly do the wrong thing by typing `requiredArg: $mayBeNull`
342                    // and it won't raise a validation error at this stage.
343                    Some(value) => value.is_null(),
344                };
345
346                if arg_def.is_required() && is_null {
347                    diagnostics.push(
348                        dir.location(),
349                        DiagnosticData::RequiredArgument {
350                            name: arg_def.name.clone(),
351                            expected_type: arg_def.ty.clone(),
352                            coordinate: DirectiveArgumentCoordinate {
353                                directive: directive_definition.name.clone(),
354                                argument: arg_def.name.clone(),
355                            }
356                            .into(),
357                            definition_location: arg_def.location(),
358                        },
359                    );
360                }
361            }
362        } else {
363            diagnostics.push(
364                loc,
365                DiagnosticData::UndefinedDirective { name: name.clone() },
366            )
367        }
368    }
369}