Skip to main content

apollo_compiler/executable/
from_ast.rs

1use super::*;
2use crate::ty;
3use std::sync::Arc;
4
5pub(crate) struct BuildErrors<'a> {
6    pub(crate) errors: &'a mut DiagnosticList,
7    pub(crate) path: SelectionPath,
8}
9
10/// A builder for constructing an [`ExecutableDocument`] from multiple AST documents.
11///
12/// This builder allows you to parse and combine executable definitions (operations and fragments)
13/// from multiple source files into a single [`ExecutableDocument`].
14///
15/// # Example
16///
17/// ```rust
18/// use apollo_compiler::{Schema, ExecutableDocument};
19/// use apollo_compiler::parser::Parser;
20/// use apollo_compiler::validation::DiagnosticList;
21/// # let schema_src = "type Query { user: User } type User { id: ID }";
22/// # let schema = Schema::parse_and_validate(schema_src, "schema.graphql").unwrap();
23///
24/// // Create a builder
25/// let mut errors = DiagnosticList::new(Default::default());
26/// let mut errors = DiagnosticList::new(Default::default());
27/// let doc = ExecutableDocument::builder(Some(&schema), &mut errors)
28///     .parse("query GetUser { user { id } }", "query1.graphql")
29///     .parse("query GetMore { user { id } }", "query2.graphql")
30///     .build();
31///
32/// assert!(errors.is_empty());
33/// assert_eq!(doc.operations.named.len(), 2);
34/// ```
35pub struct ExecutableDocumentBuilder<'schema, 'errors> {
36    /// The executable document being built
37    pub(crate) document: ExecutableDocument,
38    /// Optional schema for type checking during build
39    schema: Option<&'schema Schema>,
40    /// Accumulated diagnostics
41    pub(crate) errors: &'errors mut DiagnosticList,
42    /// Track if we've seen multiple anonymous operations
43    multiple_anonymous: bool,
44}
45
46impl<'schema, 'errors> ExecutableDocumentBuilder<'schema, 'errors> {
47    /// Creates a new [`ExecutableDocumentBuilder`].
48    pub fn new(schema: Option<&'schema Schema>, errors: &'errors mut DiagnosticList) -> Self {
49        Self {
50            document: ExecutableDocument::new(),
51            schema,
52            errors,
53            multiple_anonymous: false,
54        }
55    }
56
57    /// Parses a GraphQL executable document from source text and adds it to the builder.
58    ///
59    /// This is a convenience method that creates a parser and calls
60    /// [`Parser::parse_into_executable_builder`](crate::parser::Parser::parse_into_executable_builder).
61    ///
62    pub fn parse(
63        mut self,
64        source_text: impl Into<String>,
65        path: impl AsRef<std::path::Path>,
66    ) -> Self {
67        Parser::new().parse_into_executable_builder(source_text, path, &mut self);
68        self
69    }
70
71    /// Adds an AST document to the executable document being built.
72    pub fn add_ast_document(
73        &mut self,
74        document: &ast::Document,
75        type_system_definitions_are_errors: bool,
76    ) {
77        Arc::make_mut(&mut self.errors.sources)
78            .extend(document.sources.iter().map(|(k, v)| (*k, v.clone())));
79        self.add_ast_document_not_adding_sources(document, type_system_definitions_are_errors);
80    }
81
82    pub(crate) fn add_ast_document_not_adding_sources(
83        &mut self,
84        document: &ast::Document,
85        type_system_definitions_are_errors: bool,
86    ) {
87        let mut errors = BuildErrors {
88            errors: self.errors,
89            path: SelectionPath {
90                nested_fields: Vec::new(),
91                // overwritten:
92                root: ExecutableDefinitionName::AnonymousOperation(ast::OperationType::Query),
93            },
94        };
95
96        for definition in &document.definitions {
97            debug_assert!(errors.path.nested_fields.is_empty());
98            match definition {
99                ast::Definition::OperationDefinition(operation) => {
100                    if let Some(name) = &operation.name {
101                        if let Some(anonymous) = &self.document.operations.anonymous {
102                            errors.errors.push(
103                                anonymous.location(),
104                                BuildError::AmbiguousAnonymousOperation,
105                            )
106                        }
107                        if let Entry::Vacant(entry) =
108                            self.document.operations.named.entry(name.clone())
109                        {
110                            errors.path.root = ExecutableDefinitionName::NamedOperation(
111                                operation.operation_type,
112                                name.clone(),
113                            );
114                            if let Some(op) =
115                                Operation::from_ast(self.schema, &mut errors, operation)
116                            {
117                                entry.insert(operation.same_location(op));
118                            } else {
119                                errors.errors.push(
120                                    operation.location(),
121                                    BuildError::UndefinedRootOperation {
122                                        operation_type: operation.operation_type.name(),
123                                    },
124                                )
125                            }
126                        } else {
127                            let (key, _) =
128                                self.document.operations.named.get_key_value(name).unwrap();
129                            errors.errors.push(
130                                name.location(),
131                                BuildError::OperationNameCollision {
132                                    name_at_previous_location: key.clone(),
133                                },
134                            );
135                        }
136                    } else if let Some(previous) = &self.document.operations.anonymous {
137                        if !self.multiple_anonymous {
138                            self.multiple_anonymous = true;
139                            errors
140                                .errors
141                                .push(previous.location(), BuildError::AmbiguousAnonymousOperation)
142                        }
143                        errors.errors.push(
144                            operation.location(),
145                            BuildError::AmbiguousAnonymousOperation,
146                        )
147                    } else if !self.document.operations.named.is_empty() {
148                        errors.errors.push(
149                            operation.location(),
150                            BuildError::AmbiguousAnonymousOperation,
151                        )
152                    } else {
153                        errors.path.root =
154                            ExecutableDefinitionName::AnonymousOperation(operation.operation_type);
155                        if let Some(op) = Operation::from_ast(self.schema, &mut errors, operation) {
156                            self.document.operations.anonymous = Some(operation.same_location(op));
157                        } else {
158                            errors.errors.push(
159                                operation.location(),
160                                BuildError::UndefinedRootOperation {
161                                    operation_type: operation.operation_type.name(),
162                                },
163                            )
164                        }
165                    }
166                }
167                ast::Definition::FragmentDefinition(fragment) => {
168                    if let Entry::Vacant(entry) =
169                        self.document.fragments.entry(fragment.name.clone())
170                    {
171                        errors.path.root =
172                            ExecutableDefinitionName::Fragment(fragment.name.clone());
173                        if let Some(node) = Fragment::from_ast(self.schema, &mut errors, fragment) {
174                            entry.insert(fragment.same_location(node));
175                        }
176                    } else {
177                        let (key, _) = self
178                            .document
179                            .fragments
180                            .get_key_value(&fragment.name)
181                            .unwrap();
182                        errors.errors.push(
183                            fragment.name.location(),
184                            BuildError::FragmentNameCollision {
185                                name_at_previous_location: key.clone(),
186                            },
187                        )
188                    }
189                }
190                _ => {
191                    if type_system_definitions_are_errors {
192                        errors.errors.push(
193                            definition.location(),
194                            BuildError::TypeSystemDefinition {
195                                name: definition.name().cloned(),
196                                describe: definition.describe(),
197                            },
198                        )
199                    }
200                }
201            }
202        }
203
204        Arc::make_mut(&mut self.document.sources)
205            .extend(document.sources.iter().map(|(k, v)| (*k, v.clone())));
206    }
207
208    /// Returns the executable document built from all added AST documents.
209    pub fn build(self) -> ExecutableDocument {
210        self.build_inner()
211    }
212
213    pub(crate) fn build_inner(mut self) -> ExecutableDocument {
214        self.document.sources = self.errors.sources.clone();
215        self.document
216    }
217}
218
219pub(crate) fn document_from_ast(
220    schema: Option<&Schema>,
221    document: &ast::Document,
222    errors: &mut DiagnosticList,
223    type_system_definitions_are_errors: bool,
224) -> ExecutableDocument {
225    let mut builder = ExecutableDocumentBuilder::new(schema, errors);
226
227    builder.add_ast_document_not_adding_sources(document, type_system_definitions_are_errors);
228
229    let doc = builder.build_inner();
230
231    ExecutableDocument {
232        sources: document.sources.clone(),
233        operations: doc.operations,
234        fragments: doc.fragments,
235    }
236}
237
238impl Operation {
239    fn from_ast(
240        schema: Option<&Schema>,
241        errors: &mut BuildErrors,
242        ast: &ast::OperationDefinition,
243    ) -> Option<Self> {
244        let ty = if let Some(s) = schema {
245            s.root_operation(ast.operation_type)?.clone()
246        } else {
247            // Hack for validate_standalone_executable
248            ast.operation_type.default_type_name().clone()
249        };
250        let mut selection_set = SelectionSet::new(ty);
251        selection_set.extend_from_ast(schema, errors, &ast.selection_set);
252        Some(Self {
253            operation_type: ast.operation_type,
254            name: ast.name.clone(),
255            variables: ast.variables.clone(),
256            directives: ast.directives.clone(),
257            selection_set,
258        })
259    }
260}
261
262impl Fragment {
263    fn from_ast(
264        schema: Option<&Schema>,
265        errors: &mut BuildErrors,
266        ast: &ast::FragmentDefinition,
267    ) -> Option<Self> {
268        if let Some(schema) = schema {
269            if !schema.types.contains_key(&ast.type_condition) {
270                errors.errors.push(
271                    ast.type_condition.location(),
272                    BuildError::UndefinedTypeInNamedFragmentTypeCondition {
273                        type_name: ast.type_condition.clone(),
274                        fragment_name: ast.name.clone(),
275                    },
276                );
277                return None;
278            }
279        }
280        let mut selection_set = SelectionSet::new(ast.type_condition.clone());
281        selection_set.extend_from_ast(schema, errors, &ast.selection_set);
282        Some(Self {
283            name: ast.name.clone(),
284            directives: ast.directives.clone(),
285            selection_set,
286        })
287    }
288}
289
290impl SelectionSet {
291    pub(crate) fn extend_from_ast(
292        &mut self,
293        schema: Option<&Schema>,
294        errors: &mut BuildErrors,
295        ast_selections: &[ast::Selection],
296    ) {
297        for selection in ast_selections {
298            match selection {
299                ast::Selection::Field(ast) => {
300                    let field_def_result = if let Some(s) = schema {
301                        s.type_field(&self.ty, &ast.name).map(|c| c.node.clone())
302                    } else {
303                        Ok(Node::new(ast::FieldDefinition {
304                            description: None,
305                            name: ast.name.clone(),
306                            arguments: Vec::new(),
307                            ty: ty!(UNKNOWN),
308                            directives: Default::default(),
309                        }))
310                    };
311                    errors
312                        .path
313                        .nested_fields
314                        .push(ast.alias.clone().unwrap_or_else(|| ast.name.clone()));
315                    match field_def_result {
316                        Ok(field_def) => {
317                            let leaf = ast.selection_set.is_empty();
318                            let type_name = field_def.ty.inner_named_type();
319                            match schema
320                                .as_ref()
321                                .and_then(|schema| schema.types.get(type_name))
322                            {
323                                Some(schema::ExtendedType::Scalar(_)) if !leaf => {
324                                    errors.errors.push(
325                                        ast.location(),
326                                        BuildError::SubselectionOnScalarType {
327                                            type_name: type_name.clone(),
328                                            path: errors.path.clone(),
329                                        },
330                                    )
331                                }
332                                Some(schema::ExtendedType::Enum(_)) if !leaf => errors.errors.push(
333                                    ast.location(),
334                                    BuildError::SubselectionOnEnumType {
335                                        type_name: type_name.clone(),
336                                        path: errors.path.clone(),
337                                    },
338                                ),
339                                _ => self.push(
340                                    ast.same_location(
341                                        Field::new(ast.name.clone(), field_def)
342                                            .with_opt_alias(ast.alias.clone())
343                                            .with_arguments(ast.arguments.iter().cloned())
344                                            .with_directives(ast.directives.iter().cloned())
345                                            .with_ast_selections(
346                                                schema,
347                                                errors,
348                                                &ast.selection_set,
349                                            ),
350                                    ),
351                                ),
352                            }
353                        }
354                        Err(schema::FieldLookupError::NoSuchField(type_name, _)) => {
355                            errors.errors.push(
356                                ast.name.location(),
357                                BuildError::UndefinedField {
358                                    type_name: type_name.clone(),
359                                    field_name: ast.name.clone(),
360                                    path: errors.path.clone(),
361                                },
362                            )
363                        }
364                        Err(schema::FieldLookupError::NoSuchType) => {
365                            // `self.ty` is the name of a type not defined in the schema.
366                            // It can come from:
367                            // * A root operation type, or a field definition:
368                            //   the schema is invalid, no need to record another error here.
369                            // * An inline fragment with a type condition:
370                            //   we emitted `UndefinedTypeInInlineFragmentTypeCondition` already
371                            // * The type condition of a named fragment definition:
372                            //   we emitted `UndefinedTypeInNamedFragmentTypeCondition` already
373                        }
374                    }
375                    errors.path.nested_fields.pop();
376                }
377                ast::Selection::FragmentSpread(ast) => self.push(
378                    ast.same_location(
379                        self.new_fragment_spread(ast.fragment_name.clone())
380                            .with_directives(ast.directives.iter().cloned()),
381                    ),
382                ),
383                ast::Selection::InlineFragment(ast) => {
384                    let opt_type_condition = ast.type_condition.clone();
385                    match (&opt_type_condition, schema) {
386                        (Some(type_condition), Some(schema))
387                            if !schema.types.contains_key(type_condition) =>
388                        {
389                            errors.errors.push(
390                                type_condition.location(),
391                                BuildError::UndefinedTypeInInlineFragmentTypeCondition {
392                                    type_name: type_condition.clone(),
393                                    path: errors.path.clone(),
394                                },
395                            )
396                        }
397                        _ => self.push(
398                            ast.same_location(
399                                self.new_inline_fragment(opt_type_condition)
400                                    .with_directives(ast.directives.iter().cloned())
401                                    .with_ast_selections(schema, errors, &ast.selection_set),
402                            ),
403                        ),
404                    }
405                }
406            }
407        }
408    }
409}
410
411impl Field {
412    fn with_ast_selections(
413        mut self,
414        schema: Option<&Schema>,
415        errors: &mut BuildErrors,
416        ast_selections: &[ast::Selection],
417    ) -> Self {
418        self.selection_set
419            .extend_from_ast(schema, errors, ast_selections);
420        self
421    }
422}
423
424impl InlineFragment {
425    fn with_ast_selections(
426        mut self,
427        schema: Option<&Schema>,
428        errors: &mut BuildErrors,
429        ast_selections: &[ast::Selection],
430    ) -> Self {
431        self.selection_set
432            .extend_from_ast(schema, errors, ast_selections);
433        self
434    }
435}