Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(parser, compiler): add parsing a type reference from a string #718

Merged
merged 17 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/apollo-compiler/src/ast/from_cst.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ impl Document {
}

/// Similar to `TryFrom`, but with an `Option` return type because AST uses Option a lot.
trait Convert {
pub(crate) trait Convert {
type Target;
fn convert(&self, file_id: FileId) -> Option<Self::Target>;
}
Expand Down
13 changes: 13 additions & 0 deletions crates/apollo-compiler/src/ast/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,19 @@ impl Type {
}
}

/// Parse the given source with a field type.
///
/// `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(
source_text: impl Into<String>,
path: impl AsRef<Path>,
) -> Result<Self, DiagnosticList> {
Parser::new().parse_type(source_text, path)
}

serialize_method!();
}

Expand Down
34 changes: 33 additions & 1 deletion crates/apollo-compiler/src/parser.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::ast;
use crate::ast::from_cst::Convert;
use crate::ast::Document;
use crate::executable;
use crate::schema::SchemaBuilder;
Expand Down Expand Up @@ -195,7 +196,7 @@ impl Parser {
self.parse_ast(source_text, path).to_mixed()
}

/// Parse the given source a selection set with optional outer brackets.
/// Parse the given source text as 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.
Expand Down Expand Up @@ -234,6 +235,37 @@ impl Parser {
}
}

/// Parse the given source text as a reference to a type.
///
/// `path` is the filesystem path (or arbitrary string) used in diagnostics
/// to identify this source file to users.
pub fn parse_type(
&mut self,
source_text: impl Into<String>,
path: impl AsRef<Path>,
) -> Result<ast::Type, DiagnosticList> {
let (tree, source_file) =
self.parse_common(source_text.into(), path.as_ref().to_owned(), |parser| {
parser.parse_type()
});
let file_id = FileId::new();

let sources: crate::SourceMap = Arc::new([(file_id, source_file)].into());
let mut errors = DiagnosticList::new(None, sources.clone());
for (file_id, source) in sources.iter() {
source.validate_parse_errors(&mut errors, *file_id)
}

if errors.is_empty() {
if let Some(ty) = tree.ty().convert(file_id) {
return Ok(ty);
}
unreachable!("conversion is infallible if there were no syntax errors");
} else {
Err(errors)
}
}

/// What level of recursion was reached during the last call to a `parse_*` method.
///
/// Collecting this on a corpus of documents can help decide
Expand Down
37 changes: 37 additions & 0 deletions crates/apollo-compiler/tests/field_type.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use apollo_compiler::schema::Type;

#[test]
fn test_valid_field_type() {
let input = "String!";
let field_type = Type::parse(input, "field_type.graphql").expect("expected a field type");
assert_eq!(field_type.to_string(), input);

let input = "[[[[[Int!]!]!]!]!]!";
let field_type = Type::parse(input, "field_type.graphql").expect("expected a field type");
assert_eq!(field_type.to_string(), input);
}

#[test]
fn test_invalid_field_type() {
let input = "[[String]";
match Type::parse(input, "field_type.graphql") {
Ok(parsed) => panic!("Field type should fail to parse, instead got `{parsed}`"),
Err(errors) => {
let errors = errors.to_string_no_color();
assert!(
errors.contains("Error: syntax error: expected R_BRACK, got EOF"),
"{errors}"
);
}
}

let input = "[]";
match Type::parse(input, "field_type.graphql") {
Ok(parsed) => panic!("Field type should fail to parse, instead got `{parsed}`"),
Err(diag) => {
let errors = diag.to_string_no_color();
assert!(errors.contains("expected item type"), "{errors}");
assert!(errors.contains("expected R_BRACK, got EOF"), "{errors}");
}
}
}
1 change: 1 addition & 0 deletions crates/apollo-compiler/tests/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod executable;
mod extensions;
mod field_set;
mod field_type;
mod merge_schemas;
/// Formerly in src/lib.rs
mod misc;
Expand Down
2 changes: 1 addition & 1 deletion crates/apollo-parser/src/parser/grammar/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub(crate) mod document;
pub(crate) mod selection;
pub(crate) mod ty;

mod argument;
mod description;
Expand All @@ -15,7 +16,6 @@ mod object;
mod operation;
mod scalar;
mod schema;
mod ty;
mod union_;
mod value;
mod variable;
38 changes: 31 additions & 7 deletions crates/apollo-parser/src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ pub(crate) mod grammar;
use std::{cell::RefCell, rc::Rc};

use crate::{
cst::{Document, SelectionSet},
cst::{Document, SelectionSet, Type},
lexer::Lexer,
Error, LimitTracker, Token, TokenKind,
};
Expand Down Expand Up @@ -146,12 +146,16 @@ impl<'a> Parser<'a> {

match builder {
syntax_tree::SyntaxTreeWrapper::Document(tree) => tree,
syntax_tree::SyntaxTreeWrapper::FieldSet(_) => {
syntax_tree::SyntaxTreeWrapper::Type(_)
| syntax_tree::SyntaxTreeWrapper::FieldSet(_) => {
unreachable!("parse constructor can only construct a document")
}
}
}

/// Parse a selection set with optional outer braces.
/// This is the expected format of the string value of the `fields` argument of some directives
/// like [`@requires`](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#requires).
pub fn parse_selection_set(mut self) -> SyntaxTree<SelectionSet> {
grammar::selection::field_set(&mut self);

Expand All @@ -166,8 +170,30 @@ impl<'a> Parser<'a> {

match builder {
syntax_tree::SyntaxTreeWrapper::FieldSet(tree) => tree,
syntax_tree::SyntaxTreeWrapper::Document(_) => {
unreachable!("parse constructor can only construct a selection set")
syntax_tree::SyntaxTreeWrapper::Document(_)
| syntax_tree::SyntaxTreeWrapper::Type(_) => {
unreachable!("parse_selection_set constructor can only construct a selection set")
}
}
}

/// Parse a GraphQL type.
/// This is the expected format of the string value of the `type` argument
/// of some directives like [`@field`](https://specs.apollo.dev/join/v0.3/#@field).
pub fn parse_type(mut self) -> SyntaxTree<Type> {
grammar::ty::ty(&mut self);

let builder = Rc::try_unwrap(self.builder)
.expect("More than one reference to builder left")
.into_inner();
let builder =
builder.finish_type(self.errors, self.recursion_limit, self.lexer.limit_tracker);

match builder {
syntax_tree::SyntaxTreeWrapper::Type(tree) => tree,
syntax_tree::SyntaxTreeWrapper::FieldSet(_)
| syntax_tree::SyntaxTreeWrapper::Document(_) => {
unreachable!("parse_type constructor can only construct a type")
}
}
}
Expand Down Expand Up @@ -299,9 +325,7 @@ impl<'a> Parser<'a> {
/// Consume the next token if it is `kind` or emit an error
/// otherwise.
pub(crate) fn expect(&mut self, token: TokenKind, kind: SyntaxKind) {
let current = if let Some(current) = self.current() {
current
} else {
let Some(current) = self.current() else {
return;
};
let is_eof = current.kind == TokenKind::Eof;
Expand Down
37 changes: 37 additions & 0 deletions crates/apollo-parser/src/parser/syntax_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ use super::LimitTracker;
pub(crate) enum SyntaxTreeWrapper {
Document(SyntaxTree<cst::Document>),
FieldSet(SyntaxTree<cst::SelectionSet>),
Type(SyntaxTree<cst::Type>),
}

#[derive(PartialEq, Eq, Clone)]
Expand Down Expand Up @@ -111,6 +112,25 @@ impl SyntaxTree<cst::SelectionSet> {
}
}

impl SyntaxTree<cst::Type> {
/// Return the root typed `SelectionSet` node. This is used for parsing
/// selection sets defined by @requires directive.
pub fn ty(&self) -> cst::Type {
match self.syntax_node().kind() {
SyntaxKind::NAMED_TYPE => cst::Type::NamedType(cst::NamedType {
syntax: self.syntax_node(),
}),
SyntaxKind::LIST_TYPE => cst::Type::ListType(cst::ListType {
syntax: self.syntax_node(),
}),
SyntaxKind::NON_NULL_TYPE => cst::Type::NonNullType(cst::NonNullType {
syntax: self.syntax_node(),
}),
_ => unreachable!("this should only return Type node"),
}
}
}

impl<T: CstNode> fmt::Debug for SyntaxTree<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn print(f: &mut fmt::Formatter<'_>, indent: usize, element: SyntaxElement) -> fmt::Result {
Expand Down Expand Up @@ -228,6 +248,23 @@ impl SyntaxTreeBuilder {
_phantom: PhantomData,
})
}

pub(crate) fn finish_type(
self,
errors: Vec<Error>,
recursion_limit: LimitTracker,
token_limit: LimitTracker,
) -> SyntaxTreeWrapper {
SyntaxTreeWrapper::Type(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,
})
}
}

#[cfg(test)]
Expand Down