diff --git a/crates/apollo-compiler/CHANGELOG.md b/crates/apollo-compiler/CHANGELOG.md index d4eceea3c..7324fe8f8 100644 --- a/crates/apollo-compiler/CHANGELOG.md +++ b/crates/apollo-compiler/CHANGELOG.md @@ -34,7 +34,13 @@ Assorted `Schema` API changes by [SimonSapin] in [pull/678]: ## Features -- Add opt-in configuration for “orphan” extensions to be “adopted”, by [SimonSapin] in [pull/678] +- **Add `executable::FieldSet` for a selection set with optional outer brackets - [lrlna], [pull/685] fixing [issue/681]** + This is intended to parse string value of a [`FieldSet` custom scalar][fieldset] + used in some Apollo Federation directives in the context of a specific schema and type. + Its `validate` method calls a subset of validation rules relevant to selection sets. + which is not part of a document. + +- **Add opt-in configuration for “orphan” extensions to be “adopted” - [SimonSapin], [pull/678]** Type extensions and schema extensions without a corresponding definition are normally ignored except for recording a validation error. @@ -50,17 +56,20 @@ Assorted `Schema` API changes by [SimonSapin] in [pull/678]: schema.validate()?; ``` - ## Fixes -- Allow built-in directives to be redefined, by [SimonSapin] in [pull/684], [issue/656] -- Allow schema extensions to extend a schema definition implied by object types named after default root operations, by [SimonSapin] in [pull/678], [issues/682] +- **Allow built-in directives to be redefined - [SimonSapin], [pull/684] fixing [issue/656]** +- **Allow schema extensions to extend a schema definition implied by object types named after default root operations - [SimonSapin], [pull/678] fixing [issues/682]** +[lrlna]: https://github.com/lrlna [SimonSapin]: https://github.com/SimonSapin [issue/656]: https://github.com/apollographql/apollo-rs/issues/656 [issue/682]: https://github.com/apollographql/apollo-rs/issues/682 +[issue/681]: https://github.com/apollographql/apollo-rs/issues/681 [pull/678]: https://github.com/apollographql/apollo-rs/pull/678 [pull/684]: https://github.com/apollographql/apollo-rs/pull/684 +[pull/685]: https://github.com/apollographql/apollo-rs/pull/685 +[fieldset]: https://www.apollographql.com/docs/federation/subgraph-spec/#scalar-fieldset # [1.0.0-beta.1](https://crates.io/crates/apollo-compiler/1.0.0-beta.1) - 2023-10-05 diff --git a/crates/apollo-compiler/src/ast/from_cst.rs b/crates/apollo-compiler/src/ast/from_cst.rs index ec2a2dc8d..4e01a6131 100644 --- a/crates/apollo-compiler/src/ast/from_cst.rs +++ b/crates/apollo-compiler/src/ast/from_cst.rs @@ -641,14 +641,20 @@ impl Convert for cst::SelectionSet { type Target = Vec; fn convert(&self, file_id: FileId) -> Option { - Some( - self.selections() - .filter_map(|selection| selection.convert(file_id)) - .collect(), - ) + Some(convert_selection_set(self, file_id)) } } +pub(crate) fn convert_selection_set( + selection_set: &cst::SelectionSet, + file_id: FileId, +) -> Vec { + selection_set + .selections() + .filter_map(|selection| selection.convert(file_id)) + .collect() +} + impl Convert for cst::Selection { type Target = ast::Selection; diff --git a/crates/apollo-compiler/src/ast/mod.rs b/crates/apollo-compiler/src/ast/mod.rs index e455621f3..c1e6d86ba 100644 --- a/crates/apollo-compiler/src/ast/mod.rs +++ b/crates/apollo-compiler/src/ast/mod.rs @@ -38,7 +38,7 @@ use crate::SourceFile; use std::collections::HashMap; use std::sync::Arc; -mod from_cst; +pub(crate) mod from_cst; pub(crate) mod impls; pub(crate) mod serialize; diff --git a/crates/apollo-compiler/src/executable/from_ast.rs b/crates/apollo-compiler/src/executable/from_ast.rs index 8b41076d4..fd0234bdd 100644 --- a/crates/apollo-compiler/src/executable/from_ast.rs +++ b/crates/apollo-compiler/src/executable/from_ast.rs @@ -1,8 +1,8 @@ use super::*; -struct BuildErrors { - errors: Vec, - path: SelectionPath, +pub(crate) struct BuildErrors { + pub(crate) errors: Vec, + pub(crate) path: SelectionPath, } pub(crate) fn document_from_ast( @@ -147,7 +147,7 @@ impl Fragment { } impl SelectionSet { - fn extend_from_ast( + pub(crate) fn extend_from_ast( &mut self, schema: Option<&Schema>, errors: &mut BuildErrors, diff --git a/crates/apollo-compiler/src/executable/mod.rs b/crates/apollo-compiler/src/executable/mod.rs index e733dba2f..d6fc9683c 100644 --- a/crates/apollo-compiler/src/executable/mod.rs +++ b/crates/apollo-compiler/src/executable/mod.rs @@ -44,6 +44,23 @@ pub struct ExecutableDocument { pub fragments: IndexMap>, } +/// FieldSet information created for FieldSet parsing in `@requires` directive. +/// Annotated with type information. +#[derive(Debug, Clone)] +pub struct FieldSet { + /// If this document was originally parsed from a source file, + /// that file and its ID. + /// + /// The document may have been modified since. + pub source: Option<(FileId, Arc)>, + + /// Errors that occurred when building this FieldSet, + /// either parsing a source file or converting from AST. + pub(crate) build_errors: Vec, + + pub selection_set: SelectionSet, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct Operation { pub operation_type: OperationType, @@ -176,8 +193,8 @@ pub(crate) enum BuildError { #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct SelectionPath { - root: ExecutableDefinitionName, - nested_fields: Vec, + pub(crate) root: ExecutableDefinitionName, + pub(crate) nested_fields: Vec, } /// Designates by name a top-level definition in an executable document @@ -673,6 +690,31 @@ impl FragmentSpread { } } +impl FieldSet { + /// Parse the given source a selection set with optional outer brackets. + /// + /// `path` is the filesystem path (or arbitrary string) used in diagnostics + /// to identify this source file to users. + /// + /// Create a [`Parser`] to use different parser configuration. + pub fn parse( + schema: &Schema, + type_name: impl Into, + source_text: impl Into, + path: impl AsRef, + ) -> Self { + Parser::new().parse_field_set(schema, type_name, source_text, path) + } + + pub fn validate(&self, schema: &Schema) -> Result<(), Diagnostics> { + let mut sources = schema.sources.clone(); + sources.extend(self.source.clone()); + let mut errors = Diagnostics::new(sources); + validation::validate_field_set(&mut errors, schema, self); + errors.into_result() + } +} + impl fmt::Display for SelectionPath { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.root { diff --git a/crates/apollo-compiler/src/executable/serialize.rs b/crates/apollo-compiler/src/executable/serialize.rs index 9dadb6360..c93373878 100644 --- a/crates/apollo-compiler/src/executable/serialize.rs +++ b/crates/apollo-compiler/src/executable/serialize.rs @@ -48,7 +48,7 @@ impl Node { } impl SelectionSet { - fn to_ast(&self) -> Vec { + pub(crate) fn to_ast(&self) -> Vec { self.selections .iter() .map(|selection| match selection { diff --git a/crates/apollo-compiler/src/executable/validation.rs b/crates/apollo-compiler/src/executable/validation.rs index 27facc70f..fe9a69515 100644 --- a/crates/apollo-compiler/src/executable/validation.rs +++ b/crates/apollo-compiler/src/executable/validation.rs @@ -1,4 +1,6 @@ use super::BuildError; +use super::FieldSet; +use crate::ast; use crate::validation::Details; use crate::validation::Diagnostics; use crate::ExecutableDocument; @@ -128,3 +130,71 @@ fn compiler_validation( ) } } + +pub(crate) fn validate_field_set(errors: &mut Diagnostics, schema: &Schema, field_set: &FieldSet) { + if let Some((file_id, source)) = &field_set.source { + source.validate_parse_errors(errors, *file_id) + } + for build_error in &field_set.build_errors { + validate_build_error(errors, build_error) + } + let mut compiler = crate::ApolloCompiler::new(); + let mut ids = Vec::new(); + for (id, source) in &schema.sources { + ids.push(*id); + compiler.db.set_input(*id, source.into()); + } + if let Some((id, source)) = &field_set.source { + ids.push(*id); + compiler.db.set_input(*id, source.into()); + } + + let schema_ast_id = FileId::HACK_TMP; + ids.push(schema_ast_id); + let mut ast = crate::ast::Document::new(); + ast.definitions.extend(schema.to_ast()); + compiler.db.set_input( + schema_ast_id, + crate::Source { + ty: crate::database::SourceType::Schema, + filename: Default::default(), + text: Default::default(), + ast: Some(Arc::new(ast)), + }, + ); + + let ast_id = FileId::HACK_TMP_2; + ids.push(ast_id); + let ast = ast::Document::new(); + compiler.db.set_input( + ast_id, + crate::Source { + ty: crate::database::SourceType::Executable, + filename: Default::default(), + text: Default::default(), + ast: Some(Arc::new(ast)), + }, + ); + compiler.db.set_source_files(ids); + let diagnostics = crate::validation::selection::validate_selection_set2( + &compiler.db, + ast_id, + Some(&field_set.selection_set.ty), + &field_set.selection_set.to_ast(), + crate::validation::operation::OperationValidationConfig { + has_schema: true, + variables: &[], + }, + ); + // if schema.is_some() { + // compiler.db.validate_executable(ast_id) + // } else { + // compiler.db.validate_standalone_executable(ast_id) + // }; + for diagnostic in diagnostics { + errors.push( + Some(diagnostic.location), + Details::CompilerDiagnostic(diagnostic), + ) + } +} diff --git a/crates/apollo-compiler/src/parser.rs b/crates/apollo-compiler/src/parser.rs index f1db79a6d..cf224d67f 100644 --- a/crates/apollo-compiler/src/parser.rs +++ b/crates/apollo-compiler/src/parser.rs @@ -1,4 +1,6 @@ +use crate::ast; use crate::ast::Document; +use crate::executable; use crate::schema::SchemaBuilder; use crate::validation::Details; use crate::validation::Diagnostics; @@ -84,6 +86,16 @@ impl Parser { path: PathBuf, file_id: FileId, ) -> Document { + let (tree, source_file) = self.parse_common(source_text, path, |parser| parser.parse()); + Document::from_cst(tree.document(), file_id, source_file) + } + + pub(crate) fn parse_common( + &mut self, + source_text: String, + path: PathBuf, + parse: impl FnOnce(apollo_parser::Parser) -> apollo_parser::SyntaxTree, + ) -> (apollo_parser::SyntaxTree, Arc) { let mut parser = apollo_parser::Parser::new(&source_text); if let Some(value) = self.recursion_limit { parser = parser.recursion_limit(value) @@ -91,7 +103,7 @@ impl Parser { if let Some(value) = self.token_limit { parser = parser.token_limit(value) } - let tree = parser.parse(); + let tree = parse(parser); self.recursion_reached = tree.recursion_limit().high; self.tokens_reached = tree.token_limit().high; let source_file = Arc::new(SourceFile { @@ -99,7 +111,7 @@ impl Parser { source_text, parse_errors: tree.errors().cloned().collect(), }); - Document::from_cst(tree.document(), file_id, source_file) + (tree, source_file) } /// Parse the given source text as the sole input file of a schema. @@ -171,6 +183,45 @@ impl Parser { self.parse_ast(source_text, path).to_mixed() } + /// Parse the given source a selection set with optional outer brackets. + /// + /// `path` is the filesystem path (or arbitrary string) used in diagnostics + /// to identify this source file to users. + /// + /// Parsing is fault-tolerant, so a selection set node is always returned. + /// TODO: document how to validate + pub fn parse_field_set( + &mut self, + schema: &Schema, + type_name: impl Into, + source_text: impl Into, + path: impl AsRef, + ) -> executable::FieldSet { + let (tree, source_file) = + self.parse_common(source_text.into(), path.as_ref().to_owned(), |parser| { + parser.parse_selection_set() + }); + let file_id = FileId::new(); + let ast = ast::from_cst::convert_selection_set(&tree.field_set(), file_id); + let mut selection_set = executable::SelectionSet::new(type_name); + let mut build_errors = executable::from_ast::BuildErrors { + errors: Vec::new(), + path: executable::SelectionPath { + nested_fields: Vec::new(), + // 🤷 + root: executable::ExecutableDefinitionName::AnonymousOperation( + ast::OperationType::Query, + ), + }, + }; + selection_set.extend_from_ast(Some(schema), &mut build_errors, &ast); + executable::FieldSet { + source: Some((file_id, source_file)), + build_errors: build_errors.errors, + selection_set, + } + } + /// What level of recursion was reached during the last call to a `parse_*` method. /// /// Collecting this on a corpus of documents can help decide diff --git a/crates/apollo-compiler/src/validation/mod.rs b/crates/apollo-compiler/src/validation/mod.rs index 60e4c0800..e1c46b29b 100644 --- a/crates/apollo-compiler/src/validation/mod.rs +++ b/crates/apollo-compiler/src/validation/mod.rs @@ -8,10 +8,10 @@ mod fragment; mod input_object; mod interface; mod object; -mod operation; +pub(crate) mod operation; mod scalar; mod schema; -mod selection; +pub(crate) mod selection; mod union_; mod value; mod variable; diff --git a/crates/apollo-compiler/src/validation/operation.rs b/crates/apollo-compiler/src/validation/operation.rs index 262442b9a..60f316cef 100644 --- a/crates/apollo-compiler/src/validation/operation.rs +++ b/crates/apollo-compiler/src/validation/operation.rs @@ -5,7 +5,7 @@ use crate::{ }; #[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub struct OperationValidationConfig<'vars> { +pub(crate) struct OperationValidationConfig<'vars> { /// When false, rules that require a schema to validate are disabled. pub has_schema: bool, /// The variables defined for this operation. diff --git a/crates/apollo-compiler/tests/field_set.rs b/crates/apollo-compiler/tests/field_set.rs new file mode 100644 index 000000000..d354645ed --- /dev/null +++ b/crates/apollo-compiler/tests/field_set.rs @@ -0,0 +1,72 @@ +use apollo_compiler::executable::FieldSet; +use apollo_compiler::Schema; + +fn common_schema() -> Schema { + let input = r#" + type Query { + id: ID + organization: Org + } + type Org { + id: ID + } + "#; + let schema = Schema::parse(input, "schema.graphql"); + schema.validate().unwrap(); + schema +} + +#[test] +fn test_valid_field_sets() { + let schema = common_schema(); + + let input = "id"; + let field_set = FieldSet::parse(&schema, "Query", input, "field_set.graphql"); + field_set.validate(&schema).unwrap(); + + let input = "id organization { id }"; + let field_set = FieldSet::parse(&schema, "Query", input, "field_set.graphql"); + field_set.validate(&schema).unwrap(); +} + +#[test] +fn test_invalid_field_sets() { + let schema = common_schema(); + + let input = "name"; + let field_set = FieldSet::parse(&schema, "Query", input, "field_set.graphql"); + let errors = field_set + .validate(&schema) + .unwrap_err() + .to_string_no_color(); + assert!( + errors.contains("type `Query` does not have a field `name`"), + "{errors}" + ); + + let input = "id organization"; + let field_set = FieldSet::parse(&schema, "Query", input, "field_set.graphql"); + let errors = field_set + .validate(&schema) + .unwrap_err() + .to_string_no_color(); + assert!( + errors.contains("interface, union and object types must have a subselection set"), + "{errors}" + ); + assert!( + errors.contains("field `organization` type `Org` is an object and must select fields"), + "{errors}" + ); + + let input = "id(arg: true)"; + let field_set = FieldSet::parse(&schema, "Query", input, "field_set.graphql"); + let errors = field_set + .validate(&schema) + .unwrap_err() + .to_string_no_color(); + assert!( + errors.contains("the argument `arg` is not supported"), + "{errors}" + ); +} diff --git a/crates/apollo-compiler/tests/main.rs b/crates/apollo-compiler/tests/main.rs index b336d8e5c..d3a80a781 100644 --- a/crates/apollo-compiler/tests/main.rs +++ b/crates/apollo-compiler/tests/main.rs @@ -1,5 +1,6 @@ mod executable; mod extensions; +mod field_set; mod merge_schemas; /// Formerly in src/lib.rs mod misc; diff --git a/crates/apollo-parser/CHANGELOG.md b/crates/apollo-parser/CHANGELOG.md index 871676b7b..be3dde917 100644 --- a/crates/apollo-parser/CHANGELOG.md +++ b/crates/apollo-parser/CHANGELOG.md @@ -17,6 +17,21 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## Documentation --> +# [x.x.x] (unreleased) - 2022-mm-dd + +## Features +- **`parse_field_set` parses a selection set with optional outer brackets - [lrlna], [pull/685] fixing [issue/681]** + This returns a `SyntaxTree` which instead of `.document() -> cst::Document` + has `.field_set() -> cst::SelectionSet`. + This is intended to parse string value of a [`FieldSet` custom scalar][fieldset] + used in some Apollo Federation directives. + +[lrlna]: https://github.com/lrlna +[pull/685]: https://github.com/apollographql/apollo-rs/pull/685 +[issue/681]: https://github.com/apollographql/apollo-rs/issues/681 +[fieldset]: https://www.apollographql.com/docs/federation/subgraph-spec/#scalar-fieldset + + # [0.7.0](https://crates.io/crates/apollo-parser/0.7.0) - 2023-10-05 ## BREAKING diff --git a/crates/apollo-parser/src/parser/grammar/mod.rs b/crates/apollo-parser/src/parser/grammar/mod.rs index aa492ef55..115872437 100644 --- a/crates/apollo-parser/src/parser/grammar/mod.rs +++ b/crates/apollo-parser/src/parser/grammar/mod.rs @@ -1,4 +1,5 @@ pub(crate) mod document; +pub(crate) mod selection; mod argument; mod description; @@ -14,7 +15,6 @@ mod object; mod operation; mod scalar; mod schema; -mod selection; mod ty; mod union_; mod value; diff --git a/crates/apollo-parser/src/parser/grammar/selection.rs b/crates/apollo-parser/src/parser/grammar/selection.rs index 27fb38391..43cb78069 100644 --- a/crates/apollo-parser/src/parser/grammar/selection.rs +++ b/crates/apollo-parser/src/parser/grammar/selection.rs @@ -26,6 +26,23 @@ pub(crate) fn selection_set(p: &mut Parser) { } } +pub(crate) fn field_set(p: &mut Parser) { + if let Some(T!['{']) = p.peek() { + selection_set(p) + } else { + let _g = p.start_node(SyntaxKind::SELECTION_SET); + // We need to enforce recursion limits to prevent + // excessive resource consumption or (more seriously) + // stack overflows. + if p.recursion_limit.check_and_increment() { + p.limit_err("parser recursion limit reached"); + return; + } + selection(p); + p.recursion_limit.decrement(); + } +} + /// See: https://spec.graphql.org/October2021/#Selection /// /// *Selection*: diff --git a/crates/apollo-parser/src/parser/mod.rs b/crates/apollo-parser/src/parser/mod.rs index 3d8b35ba9..30e376c91 100644 --- a/crates/apollo-parser/src/parser/mod.rs +++ b/crates/apollo-parser/src/parser/mod.rs @@ -7,7 +7,11 @@ pub(crate) mod grammar; use std::{cell::RefCell, rc::Rc}; -use crate::{lexer::Lexer, Error, LimitTracker, Token, TokenKind}; +use crate::{ + cst::{Document, SelectionSet}, + lexer::Lexer, + Error, LimitTracker, Token, TokenKind, +}; pub use generated::syntax_kind::SyntaxKind; pub use language::{SyntaxElement, SyntaxNode, SyntaxNodeChildren, SyntaxNodePtr, SyntaxToken}; @@ -131,13 +135,41 @@ impl<'a> Parser<'a> { } /// Parse the current tokens. - pub fn parse(mut self) -> SyntaxTree { + pub fn parse(mut self) -> SyntaxTree { grammar::document::document(&mut self); let builder = Rc::try_unwrap(self.builder) .expect("More than one reference to builder left") .into_inner(); - builder.finish(self.errors, self.recursion_limit, self.lexer.limit_tracker) + let builder = + builder.finish_document(self.errors, self.recursion_limit, self.lexer.limit_tracker); + + match builder { + syntax_tree::SyntaxTreeWrapper::Document(tree) => tree, + syntax_tree::SyntaxTreeWrapper::FieldSet(_) => { + unreachable!("parse constructor can only construct a document") + } + } + } + + pub fn parse_selection_set(mut self) -> SyntaxTree { + grammar::selection::field_set(&mut self); + + let builder = Rc::try_unwrap(self.builder) + .expect("More than one reference to builder left") + .into_inner(); + let builder = builder.finish_selection_set( + self.errors, + self.recursion_limit, + self.lexer.limit_tracker, + ); + + match builder { + syntax_tree::SyntaxTreeWrapper::FieldSet(tree) => tree, + syntax_tree::SyntaxTreeWrapper::Document(_) => { + unreachable!("parse constructor can only construct a selection set") + } + } } /// Check if the current token is `kind`. @@ -467,7 +499,7 @@ impl Checkpoint { #[cfg(test)] mod tests { use super::DEFAULT_RECURSION_LIMIT; - use crate::{Error, Parser, SyntaxTree}; + use crate::{cst, Error, Parser, SyntaxTree}; use expect_test::expect; #[test] @@ -786,4 +818,39 @@ mod tests { check(doc); } } + + #[test] + fn parse_field_set() { + let source = r#"{ a }"#; + + let parser = Parser::new(source); + let cst: SyntaxTree = parser.parse_selection_set(); + let errors = cst.errors().collect::>(); + assert_eq!(errors.len(), 0); + + let sel_set: cst::SelectionSet = cst.field_set(); + let _ = sel_set.selections().map(|sel| { + if let cst::Selection::Field(f) = sel { + assert_eq!(f.name().unwrap().text().as_ref(), "a") + } else { + panic!("no field a in field set selection") + } + }); + + let source = r#"a { a }"#; + + let parser = Parser::new(source); + let cst: SyntaxTree = parser.parse_selection_set(); + let errors = cst.errors().collect::>(); + assert_eq!(errors.len(), 0); + + let sel_set: cst::SelectionSet = cst.field_set(); + let _ = sel_set.selections().map(|sel| { + if let cst::Selection::Field(f) = sel { + assert_eq!(f.name().unwrap().text().as_ref(), "a") + } else { + panic!("no field a in field set selection") + } + }); + } } diff --git a/crates/apollo-parser/src/parser/syntax_tree.rs b/crates/apollo-parser/src/parser/syntax_tree.rs index 7aade7cc6..db8ee04f4 100644 --- a/crates/apollo-parser/src/parser/syntax_tree.rs +++ b/crates/apollo-parser/src/parser/syntax_tree.rs @@ -1,8 +1,11 @@ -use std::{fmt, slice::Iter}; +use std::{fmt, marker::PhantomData, slice::Iter}; use rowan::{GreenNode, GreenNodeBuilder}; -use crate::{cst::Document, Error, SyntaxElement, SyntaxKind, SyntaxNode}; +use crate::{ + cst::{self, CstNode}, + Error, SyntaxElement, SyntaxKind, SyntaxNode, +}; use super::LimitTracker; @@ -38,15 +41,26 @@ use super::LimitTracker; /// assert_eq!(nodes.len(), 1); /// ``` +// NOTE(@lrlna): This enum helps us setup a type state for document and field +// set parsing. Without the wrapper we'd have to add type annotations to +// SyntaxTreeBuilder, which then annotates the vast majority of the parser's +// function with the same type parameter. This is tedious and makes for a more +// complex design than necessary. +pub(crate) enum SyntaxTreeWrapper { + Document(SyntaxTree), + FieldSet(SyntaxTree), +} + #[derive(PartialEq, Eq, Clone)] -pub struct SyntaxTree { +pub struct SyntaxTree { pub(crate) green: GreenNode, pub(crate) errors: Vec, pub(crate) recursion_limit: LimitTracker, pub(crate) token_limit: LimitTracker, + _phantom: PhantomData, } -impl SyntaxTree { +impl SyntaxTree { /// Get a reference to the syntax tree's errors. pub fn errors(&self) -> Iter<'_, crate::Error> { self.errors.iter() @@ -69,16 +83,28 @@ impl SyntaxTree { pub(crate) fn syntax_node(&self) -> SyntaxNode { rowan::SyntaxNode::new_root(self.green.clone()) } +} +impl SyntaxTree { /// Return the root typed `Document` node. - pub fn document(&self) -> Document { - Document { + pub fn document(&self) -> cst::Document { + cst::Document { syntax: self.syntax_node(), } } } -impl fmt::Debug for SyntaxTree { +impl SyntaxTree { + /// Return the root typed `SelectionSet` node. This is used for parsing + /// selection sets defined by @requires directive. + pub fn field_set(&self) -> cst::SelectionSet { + cst::SelectionSet { + syntax: self.syntax_node(), + } + } +} + +impl fmt::Debug for SyntaxTree { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn print(f: &mut fmt::Formatter<'_>, indent: usize, element: SyntaxElement) -> fmt::Result { let kind: SyntaxKind = element.kind(); @@ -162,20 +188,38 @@ impl SyntaxTreeBuilder { self.builder.token(rowan::SyntaxKind(kind as u16), text); } - pub(crate) fn finish( + pub(crate) fn finish_document( self, errors: Vec, recursion_limit: LimitTracker, token_limit: LimitTracker, - ) -> SyntaxTree { - SyntaxTree { + ) -> SyntaxTreeWrapper { + SyntaxTreeWrapper::Document(SyntaxTree { green: self.builder.finish(), // TODO: keep the errors in the builder rather than pass it in here? errors, // TODO: keep the recursion and token limits in the builder rather than pass it in here? recursion_limit, token_limit, - } + _phantom: PhantomData, + }) + } + + pub(crate) fn finish_selection_set( + self, + errors: Vec, + recursion_limit: LimitTracker, + token_limit: LimitTracker, + ) -> SyntaxTreeWrapper { + SyntaxTreeWrapper::FieldSet(SyntaxTree { + green: self.builder.finish(), + // TODO: keep the errors in the builder rather than pass it in here? + errors, + // TODO: keep the recursion and token limits in the builder rather than pass it in here? + recursion_limit, + token_limit, + _phantom: PhantomData, + }) } }