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

fix(compiler): apply limits to recursive functions in validation #748

Merged
merged 16 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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
19 changes: 12 additions & 7 deletions crates/apollo-compiler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Older version may or may not be compatible.
You can get started with `apollo-compiler`:
```rust
use apollo_compiler::Schema;
use apollo_compiler::validation::ValidationOptions;

let input = r#"
interface Pet {
Expand Down Expand Up @@ -87,14 +88,15 @@ let schema = Schema::parse(input, "document.graphql");

/// In case of validation errors, the panic message will be nicely formatted
/// to point at relevant parts of the source file(s)
schema.validate().unwrap();
schema.validate(ValidationOptions::default()).unwrap();
```

### Examples
#### Accessing fragment definition field types

```rust
use apollo_compiler::{Schema, ExecutableDocument, Node, executable};
use apollo_compiler::validation::ValidationOptions;

fn main() {
let schema_input = r#"
Expand Down Expand Up @@ -124,8 +126,8 @@ fn main() {
let schema = Schema::parse(schema_input, "schema.graphql");
let document = ExecutableDocument::parse(&schema, query_input, "query.graphql");

schema.validate().unwrap();
document.validate(&schema).unwrap();
schema.validate(ValidationOptions::default()).unwrap();
document.validate(&schema, ValidationOptions::default()).unwrap();

let op = document.get_operation(Some("getUser")).expect("getUser query does not exist");
let fragment_in_op = op.selection_set.selections.iter().filter_map(|sel| match sel {
Expand All @@ -149,6 +151,7 @@ fn main() {
#### Get a directive defined on a field used in a query operation definition.
```rust
use apollo_compiler::{Schema, ExecutableDocument, Node, executable};
use apollo_compiler::validation::ValidationOptions;

fn main() {
let schema_input = r#"
Expand Down Expand Up @@ -187,8 +190,8 @@ fn main() {
let schema = Schema::parse(schema_input, "schema.graphql");
let document = ExecutableDocument::parse(&schema, query_input, "query.graphql");

schema.validate().unwrap();
document.validate(&schema).unwrap();
schema.validate(ValidationOptions::default()).unwrap();
document.validate(&schema, ValidationOptions::default()).unwrap();

let get_product_op = document
.get_operation(Some("getProduct"))
Expand All @@ -215,6 +218,8 @@ fn main() {

#### Printing diagnostics for a faulty GraphQL document
```rust
use apollo_compiler::validation::ValidationOptions;

let input = r#"
query {
cat {
Expand Down Expand Up @@ -273,10 +278,10 @@ union CatOrDog = Cat | Dog

let (schema, executable) = apollo_compiler::parse_mixed(input, "document.graphql");

if let Err(diagnostics) = schema.validate() {
if let Err(diagnostics) = schema.validate(ValidationOptions::default()) {
println!("{diagnostics}")
}
if let Err(diagnostics) = executable.validate(&schema) {
if let Err(diagnostics) = executable.validate(&schema, ValidationOptions::default()) {
println!("{diagnostics}")
}
```
Expand Down
4 changes: 2 additions & 2 deletions crates/apollo-compiler/benches/multi_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ fn compile(schema: &str, query: &str) -> (Schema, ExecutableDocument) {
fn compile_and_validate(schema: &str, query: &str) {
let schema = Schema::parse(schema, "schema.graphql");
let doc = ExecutableDocument::parse(&schema, query, "query.graphql");
let _ = black_box(schema.validate());
let _ = black_box(doc.validate(&schema));
let _ = black_box(schema.validate(Default::default()));
let _ = black_box(doc.validate(&schema, Default::default()));
}

fn bench_simple_query_compiler(c: &mut Criterion) {
Expand Down
4 changes: 2 additions & 2 deletions crates/apollo-compiler/examples/file_watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,10 @@ impl FileWatcher {
}

fn validate(&self, (schema, executable): &(Schema, ExecutableDocument)) {
if let Err(err) = schema.validate() {
if let Err(err) = schema.validate(Default::default()) {
println!("{err}")
}
if let Err(err) = executable.validate(schema) {
if let Err(err) = executable.validate(schema, Default::default()) {
println!("{err}")
}
}
Expand Down
12 changes: 6 additions & 6 deletions crates/apollo-compiler/examples/multi_source_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ fn compile_from_dir() -> io::Result<()> {
let entry = entry?;
let src = fs::read_to_string(entry.path()).expect("Could not read document file.");
let (schema, executable) = parse_mixed(&src, entry.path());
schema.validate().unwrap();
executable.validate(&schema).unwrap();
schema.validate(Default::default()).unwrap();
executable.validate(&schema, Default::default()).unwrap();
}
}
Ok(())
Expand All @@ -42,14 +42,14 @@ fn compile_schema_and_query_files() -> io::Result<()> {
.parse(src, schema)
.parse(src_ext, schema_ext)
.build();
schema.validate().unwrap();
schema.validate(Default::default()).unwrap();

// get_dog_name is a query-only file
let query = Path::new("crates/apollo-compiler/examples/documents/get_dog_name.graphql");
let src = fs::read_to_string(query).expect("Could not read query file.");
let doc = ExecutableDocument::parse(&schema, src, query);

doc.validate(&schema).unwrap();
doc.validate(&schema, Default::default()).unwrap();

Ok(())
}
Expand Down Expand Up @@ -127,9 +127,9 @@ query getDogName {
}
"#;
let schema = Schema::parse(schema, "schema.graphl");
schema.validate().unwrap();
schema.validate(Default::default()).unwrap();
let doc = ExecutableDocument::parse(&schema, query, "query.graphql");
doc.validate(&schema).unwrap();
doc.validate(&schema, Default::default()).unwrap();

Ok(())
}
4 changes: 2 additions & 2 deletions crates/apollo-compiler/examples/rename.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ fn main() {
fn renamed() -> Schema {
let input = "type Query { field: Int }";
let mut schema = Schema::parse(input, "schema.graphql");
schema.validate().unwrap();
schema.validate(Default::default()).unwrap();

// 1. Remove the definition from the `types` map, using its old name as a key
let mut type_def = schema.types.remove("Query").unwrap();
Expand All @@ -38,7 +38,7 @@ fn renamed() -> Schema {
.unwrap()
.name = new_name;

schema.validate().unwrap();
schema.validate(Default::default()).unwrap();
schema
}

Expand Down
6 changes: 4 additions & 2 deletions crates/apollo-compiler/examples/timed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ fn main() -> ExitCode {
let step = format!("Schema parse ({} bytes)", schema_source.len());
let schema = timed(&step, || Schema::parse(schema_source, schema_filename));

if let Err(errors) = timed("Schema validation", || schema.validate()) {
if let Err(errors) = timed("Schema validation", || schema.validate(Default::default())) {
println!("Schema is invalid:\n{errors}")
}

Expand All @@ -36,7 +36,9 @@ fn main() -> ExitCode {
ExecutableDocument::parse(&schema, executable_source, executable_filename)
});

if let Err(errors) = timed("Executable document validation", || doc.validate(&schema)) {
if let Err(errors) = timed("Executable document validation", || {
doc.validate(&schema, Default::default())
}) {
println!("Executable document is invalid:\n{errors}")
}

Expand Down
4 changes: 2 additions & 2 deletions crates/apollo-compiler/examples/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ fn main() {
};

let (schema, executable) = apollo_compiler::parse_mixed(source, filename);
let schema_result = schema.validate();
let executable_result = executable.validate(&schema);
let schema_result = schema.validate(Default::default());
let executable_result = executable.validate(&schema, Default::default());
let has_errors = schema_result.is_err() || executable_result.is_err();
match schema_result {
Ok(warnings) => println!("{warnings}"),
Expand Down
12 changes: 10 additions & 2 deletions crates/apollo-compiler/src/ast/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::schema::ComponentName;
use crate::schema::ComponentOrigin;
use crate::schema::SchemaBuilder;
use crate::validation::DiagnosticList;
use crate::validation::ValidationOptions;
use crate::ExecutableDocument;
use crate::Parser;
use crate::Schema;
Expand Down Expand Up @@ -47,15 +48,22 @@ impl Document {
}

/// Validate as an executable document, as much as possible without a schema
pub fn validate_standalone_executable(&self) -> Result<(), DiagnosticList> {
pub fn validate_standalone_executable(
&self,
options: ValidationOptions,
) -> Result<(), DiagnosticList> {
let type_system_definitions_are_errors = true;
let executable = crate::executable::from_ast::document_from_ast(
None,
self,
type_system_definitions_are_errors,
);
let mut errors = DiagnosticList::new(None, self.sources.clone());
crate::executable::validation::validate_standalone_executable(&mut errors, &executable);
crate::executable::validation::validate_standalone_executable(
&mut errors,
&executable,
options,
);
errors.into_result()
}

Expand Down
3 changes: 3 additions & 0 deletions crates/apollo-compiler/src/database/inputs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ pub(crate) trait InputDatabase {
#[salsa::input]
fn input(&self, file_id: FileId) -> Source;

#[salsa::input]
fn recursion_limit(&self) -> usize;

#[salsa::input]
fn schema_input(&self) -> Option<Arc<crate::Schema>>;

Expand Down
2 changes: 2 additions & 0 deletions crates/apollo-compiler/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ pub(crate) enum DiagnosticData {
/// Name of the argument where variable is used
arg_name: String,
},
#[error("too much recursion")]
RecursionError {},
}

impl DiagnosticData {
Expand Down
9 changes: 7 additions & 2 deletions crates/apollo-compiler/src/executable/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub use crate::ast::{
VariableDefinition,
};
use crate::validation::DiagnosticList;
use crate::validation::ValidationOptions;
use crate::NodeLocation;
use std::fmt;

Expand Down Expand Up @@ -227,9 +228,13 @@ impl ExecutableDocument {
Parser::new().parse_executable(schema, source_text, path)
}

pub fn validate(&self, schema: &Schema) -> Result<(), DiagnosticList> {
pub fn validate(
&self,
schema: &Schema,
options: ValidationOptions,
) -> Result<(), DiagnosticList> {
let mut errors = DiagnosticList::new(Some(schema.sources.clone()), self.sources.clone());
validation::validate_executable_document(&mut errors, schema, self);
validation::validate_executable_document(&mut errors, schema, self, options);
errors.into_result()
}

Expand Down
10 changes: 8 additions & 2 deletions crates/apollo-compiler/src/executable/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use super::FieldSet;
use crate::ast;
use crate::validation::Details;
use crate::validation::DiagnosticList;
use crate::validation::ValidationOptions;
use crate::ExecutableDocument;
use crate::FileId;
use crate::InputDatabase;
Expand All @@ -14,18 +15,20 @@ pub(crate) fn validate_executable_document(
errors: &mut DiagnosticList,
schema: &Schema,
document: &ExecutableDocument,
options: ValidationOptions,
) {
validate_common(errors, document);
compiler_validation(errors, Some(schema), document);
compiler_validation(errors, Some(schema), document, options);
// TODO
}

pub(crate) fn validate_standalone_executable(
errors: &mut DiagnosticList,
document: &ExecutableDocument,
options: ValidationOptions,
) {
validate_common(errors, document);
compiler_validation(errors, None, document);
compiler_validation(errors, None, document, options);
}

pub(crate) fn validate_common(errors: &mut DiagnosticList, document: &ExecutableDocument) {
Expand Down Expand Up @@ -75,6 +78,7 @@ fn compiler_validation(
errors: &mut DiagnosticList,
schema: Option<&Schema>,
document: &ExecutableDocument,
options: ValidationOptions,
) {
let mut compiler = crate::ApolloCompiler::new();
let mut ids = Vec::new();
Expand All @@ -93,6 +97,8 @@ fn compiler_validation(
compiler.db.set_schema_input(Some(Arc::new(schema.clone())));
}

compiler.db.set_recursion_limit(options.recursion_limit);

let ast_id = FileId::HACK_TMP;
ids.push(ast_id);
let ast = document.to_ast();
Expand Down
2 changes: 1 addition & 1 deletion crates/apollo-compiler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ mod node;
mod node_str;
mod parser;
pub mod schema;
mod validation;
pub mod validation;

use crate::database::{InputDatabase, ReprDatabase, RootDatabase, Source};
use crate::diagnostics::ApolloDiagnostic;
Expand Down
5 changes: 3 additions & 2 deletions crates/apollo-compiler/src/schema/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! High-level representation of a GraphQL schema

use crate::ast;
use crate::validation::ValidationOptions;
use crate::FileId;
use crate::Node;
use crate::NodeLocation;
Expand Down Expand Up @@ -313,9 +314,9 @@ impl Schema {
}

/// Returns `Err` if invalid, or `Ok` for potential warnings or advice
pub fn validate(&self) -> Result<DiagnosticList, DiagnosticList> {
pub fn validate(&self, options: ValidationOptions) -> Result<DiagnosticList, DiagnosticList> {
let mut errors = DiagnosticList::new(None, self.sources.clone());
let warnings_and_advice = validation::validate_schema(&mut errors, self);
let warnings_and_advice = validation::validate_schema(&mut errors, self, options);
let valid = errors.is_empty();
for diagnostic in warnings_and_advice {
errors.push(
Expand Down
6 changes: 5 additions & 1 deletion crates/apollo-compiler/src/schema/validation.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::BuildError;
use crate::validation::Details;
use crate::validation::DiagnosticList;
use crate::validation::ValidationOptions;
use crate::FileId;
use crate::InputDatabase;
use crate::Schema;
Expand All @@ -10,14 +11,15 @@ use std::sync::Arc;
pub(crate) fn validate_schema(
errors: &mut DiagnosticList,
schema: &Schema,
options: ValidationOptions,
) -> Vec<crate::ApolloDiagnostic> {
for (&file_id, source) in schema.sources.iter() {
source.validate_parse_errors(errors, file_id)
}
for build_error in &schema.build_errors {
validate_build_error(errors, build_error)
}
compiler_validation(errors, schema)
compiler_validation(errors, schema, options)
}

fn validate_build_error(errors: &mut DiagnosticList, build_error: &BuildError) {
Expand Down Expand Up @@ -47,6 +49,7 @@ fn validate_build_error(errors: &mut DiagnosticList, build_error: &BuildError) {
fn compiler_validation(
errors: &mut DiagnosticList,
schema: &Schema,
options: ValidationOptions,
) -> Vec<crate::ApolloDiagnostic> {
let mut compiler = crate::ApolloCompiler::new();
let mut ids = Vec::new();
Expand All @@ -68,6 +71,7 @@ fn compiler_validation(
},
);
compiler.db.set_source_files(ids);
compiler.db.set_recursion_limit(options.recursion_limit);
let mut warnings_and_advice = Vec::new();
for diagnostic in compiler.db.validate_type_system() {
if diagnostic.data.is_error() {
Expand Down
Loading