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 7 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
31 changes: 31 additions & 0 deletions crates/apollo-compiler/src/parser.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::ast;
use crate::ast::from_cst::Convert;
use crate::ast::Document;
use crate::executable;
use crate::schema;
use crate::schema::SchemaBuilder;
use crate::validation::Details;
use crate::validation::Diagnostics;
Expand Down Expand Up @@ -234,6 +236,35 @@ impl Parser {
}
}

/// Parse the given source of a field type.
///
/// `path` is the filesystem path (or arbitrary string) used in diagnostics
/// to identify this source file to users.
pub fn parse_field_type(
&mut self,
source_text: impl Into<String>,
path: impl AsRef<Path>,
) -> Result<(schema::FieldType, Diagnostics), Diagnostics> {
goto-bus-stop marked this conversation as resolved.
Show resolved Hide resolved
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 = Diagnostics::new(Some(sources.clone()), sources.clone());
for (file_id, source) in sources.iter() {
source.validate_parse_errors(&mut errors, *file_id)
}

if let Some(ty) = tree.ty().convert(file_id) {
let field_type = schema::FieldType { sources, ty };
Ok((field_type, 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
26 changes: 26 additions & 0 deletions crates/apollo-compiler/src/schema/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,17 @@ pub struct InputObjectType {
pub fields: IndexMap<Name, Component<InputValueDefinition>>,
}

#[derive(Debug, Clone)]
pub struct FieldType {
/// If this field type was originally parsed from a source file,
/// this map contains one entry for that file and its ID.
///
/// The document may have been modified since.
pub sources: crate::SourceMap,

pub ty: Type,
}

/// AST node that has been skipped during conversion to `Schema`
#[derive(thiserror::Error, Debug, Clone)]
pub(crate) enum BuildError {
Expand Down Expand Up @@ -851,6 +862,21 @@ impl Directives {
serialize_method!();
}

impl FieldType {
/// 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, Diagnostics), Diagnostics> {
Parser::new().parse_field_type(source_text, path)
}
}

impl std::fmt::Debug for Directives {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
Expand Down
34 changes: 34 additions & 0 deletions crates/apollo-compiler/tests/field_type.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use apollo_compiler::schema::FieldType;

#[test]
fn test_valid_field_type() {
let input = "String!";
let field_type = FieldType::parse(input, "field_type.graphql").expect("expected a field type");
assert!(field_type.1.is_empty());

let input = "[[[[[Int!]!]!]!]!]!";
let field_type = FieldType::parse(input, "field_type.graphql").expect("expected a field type");
assert!(field_type.1.is_empty());
}

#[test]
fn test_invalid_field_type() {
let input = "[[String]";
let field_type = FieldType::parse(input, "field_type.graphql").expect("expected a field type");
let errors = field_type.1.to_string_no_color();
assert!(
errors.contains("Error: syntax error: expected R_BRACK, got EOF"),
"{errors}"
);

let input = "[]";
let field_type = FieldType::parse(input, "field_type.graphql");
match field_type {
Ok(_) => panic!("this input should have no type"),
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;
30 changes: 27 additions & 3 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,15 @@ 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 selection set tokens. Useful for specifically parsing selections
/// sets which are part of specific directives, like `@requires`.
goto-bus-stop marked this conversation as resolved.
Show resolved Hide resolved
pub fn parse_selection_set(mut self) -> SyntaxTree<SelectionSet> {
grammar::selection::field_set(&mut self);

Expand All @@ -166,7 +169,28 @@ impl<'a> Parser<'a> {

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

/// Parse type tokens. Useful for specifically parsing field types which are
/// part of specific directives, like `@field`.
goto-bus-stop marked this conversation as resolved.
Show resolved Hide resolved
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 constructor can only construct a selection set")
}
}
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