Skip to main content

apollo_compiler/validation/
operation.rs

1use crate::collections::HashSet;
2use crate::executable;
3use crate::validation::diagnostics::DiagnosticData;
4use crate::validation::DepthCounter;
5use crate::validation::DepthGuard;
6use crate::validation::DiagnosticList;
7use crate::validation::ExecutableValidationContext;
8use crate::validation::RecursionLimitError;
9use crate::ExecutableDocument;
10use crate::Name;
11use crate::Node;
12
13/// Iterate all selections in the selection set.
14///
15/// This includes fields, fragment spreads, and inline fragments. For fragments, both the spread
16/// and the fragment's nested selections are reported.
17///
18/// Does not recurse into nested fields.
19fn walk_selections<'doc>(
20    document: &'doc ExecutableDocument,
21    selections: &'doc executable::SelectionSet,
22    mut f: impl FnMut(&'doc executable::Selection),
23) -> Result<(), RecursionLimitError> {
24    fn walk_selections_inner<'doc>(
25        document: &'doc ExecutableDocument,
26        selection_set: &'doc executable::SelectionSet,
27        seen: &mut HashSet<&'doc Name>,
28        mut guard: DepthGuard<'_>,
29        f: &mut dyn FnMut(&'doc executable::Selection),
30    ) -> Result<(), RecursionLimitError> {
31        for selection in &selection_set.selections {
32            f(selection);
33            match selection {
34                executable::Selection::Field(_) => {
35                    // Nothing to do
36                }
37                executable::Selection::FragmentSpread(fragment) => {
38                    let new = seen.insert(&fragment.fragment_name);
39                    if !new {
40                        continue;
41                    }
42
43                    // If the fragment doesn't exist, that error is reported elsewhere.
44                    if let Some(fragment_definition) =
45                        document.fragments.get(&fragment.fragment_name)
46                    {
47                        walk_selections_inner(
48                            document,
49                            &fragment_definition.selection_set,
50                            seen,
51                            guard.increment()?,
52                            f,
53                        )?;
54                    }
55                }
56                executable::Selection::InlineFragment(fragment) => {
57                    walk_selections_inner(
58                        document,
59                        &fragment.selection_set,
60                        seen,
61                        guard.increment()?,
62                        f,
63                    )?;
64                }
65            }
66        }
67        Ok(())
68    }
69
70    // This has a much higher limit than comparable recursive walks, like the one in
71    // `validate_fragment_cycles`, despite doing similar work. This is because this limit
72    // was introduced later and should not break (reasonable) existing queries that are
73    // under that pre-existing limit. Luckily the existing limit was very conservative.
74    let mut depth = DepthCounter::new().with_limit(500);
75
76    walk_selections_inner(
77        document,
78        selections,
79        &mut HashSet::default(),
80        depth.guard(),
81        &mut f,
82    )
83}
84
85pub(crate) fn validate_subscription(
86    document: &executable::ExecutableDocument,
87    operation: &Node<executable::Operation>,
88    diagnostics: &mut DiagnosticList,
89) {
90    if !operation.is_subscription() {
91        return;
92    }
93
94    let mut field_names = vec![];
95
96    let walked = walk_selections(document, &operation.selection_set, |selection| {
97        if let executable::Selection::Field(field) = selection {
98            field_names.push(field.name.clone());
99            if matches!(field.name.as_str(), "__type" | "__schema" | "__typename") {
100                diagnostics.push(
101                    field.location(),
102                    executable::BuildError::SubscriptionUsesIntrospection {
103                        name: operation.name.clone(),
104                        field: field.name.clone(),
105                    },
106                );
107            }
108        }
109
110        if let Some(conditional_directive) = selection
111            .directives()
112            .iter()
113            .find(|d| matches!(d.name.as_str(), "skip" | "include"))
114        {
115            diagnostics.push(
116                conditional_directive.location(),
117                executable::BuildError::SubscriptionUsesConditionalSelection {
118                    name: operation.name.clone(),
119                },
120            );
121        }
122    });
123
124    if walked.is_err() {
125        diagnostics.push(None, DiagnosticData::RecursionError {});
126        return;
127    }
128
129    if field_names.len() > 1 {
130        diagnostics.push(
131            operation.location(),
132            executable::BuildError::SubscriptionUsesMultipleFields {
133                name: operation.name.clone(),
134                fields: field_names,
135            },
136        );
137    }
138}
139
140pub(crate) fn validate_operation(
141    diagnostics: &mut DiagnosticList,
142    document: &ExecutableDocument,
143    operation: &executable::Operation,
144    context: &ExecutableValidationContext<'_>,
145) {
146    let against_type = if let Some(schema) = context.schema() {
147        schema
148            .root_operation(operation.operation_type)
149            .map(|ty| (schema, ty))
150    } else {
151        None
152    };
153
154    super::directive::validate_directives(
155        diagnostics,
156        context.schema(),
157        operation.directives.iter(),
158        operation.operation_type.into(),
159        &operation.variables,
160    );
161    super::variable::validate_variable_definitions(
162        diagnostics,
163        context.schema(),
164        &operation.variables,
165    );
166
167    super::variable::validate_unused_variables(diagnostics, document, operation);
168    super::selection::validate_selection_set(
169        diagnostics,
170        document,
171        against_type,
172        &operation.selection_set,
173        &mut context.operation_context(&operation.variables),
174    );
175}
176
177pub(crate) fn validate_operation_definitions(
178    diagnostics: &mut DiagnosticList,
179    document: &ExecutableDocument,
180    context: &ExecutableValidationContext<'_>,
181) {
182    for operation in document.operations.iter() {
183        validate_operation(diagnostics, document, operation, context);
184    }
185}