Skip to content

Commit fd0fc67

Browse files
authored
Introduce apollo-compiler crate: beginnings of a incremental, query-based compiler for graphql (#202)
Beginnings of apollo-compiler crate, an incremental, query-based compiler built on top of `salsa-rs`. This PR uses salsa's queries to create a memoised database of Executable Definitions, Fragments and Operations. Based on the context built while populating the database we will then be able to run any possible validations to produce diagnostics. This PR does variable validation in operation definitions, operation definition name validations as well as subscription field validations.
1 parent 08bb87e commit fd0fc67

File tree

42 files changed

+1995
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1995
-1
lines changed

Cargo.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,9 @@
11
[workspace]
2-
members = ["xtask/", "crates/apollo-encoder", "crates/apollo-parser", "crates/apollo-smith", "fuzz"]
2+
members = [
3+
"xtask/",
4+
"crates/apollo-encoder",
5+
"crates/apollo-parser",
6+
"crates/apollo-compiler",
7+
"crates/apollo-smith",
8+
"fuzz",
9+
]

crates/apollo-compiler/Cargo.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
name = "apollo-compiler"
3+
version = "0.1.0"
4+
authors = ["Irina Shestak <[email protected]>"]
5+
license = "MIT OR Apache-2.0"
6+
repository = "https://github.com/apollographql/apollo-rs"
7+
documentation = "https://docs.rs/apollo-conmpiler"
8+
description = "Semantic analyzer for apollo-parser's AST."
9+
keywords = ["graphql", "parser", "graphql-tooling", "apollographql"]
10+
categories = [
11+
"compilers",
12+
"development-tools",
13+
"parser-implementations",
14+
"parsing",
15+
"web-programming",
16+
]
17+
edition = "2021"
18+
19+
[dependencies]
20+
apollo-parser = { path = "../apollo-parser" }
21+
salsa = "0.16.1"
22+
uuid = { version = "0.8.2", features = ["serde", "v4"] }
23+
ordered-float = { version = "2.10.0", features = ["std"] }
24+
25+
[dev-dependencies]
26+
expect-test = "1.1"

crates/apollo-compiler/LICENSE-APACHE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../LICENSE-APACHE

crates/apollo-compiler/LICENSE-MIT

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../LICENSE-MIT
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use std::{fs, path::Path};
2+
3+
use apollo_compiler::{values, ApolloCompiler};
4+
5+
fn compile_query() -> Option<values::FragmentDefinition> {
6+
let file = Path::new("crates/apollo-compiler/examples/query_with_errors.graphql");
7+
let src = fs::read_to_string(file).expect("Could not read schema file.");
8+
9+
let ctx = ApolloCompiler::new(&src);
10+
// let errors = ctx.validate();
11+
12+
let operations = ctx.operations();
13+
let operation_names: Vec<_> = operations.iter().filter_map(|op| op.name()).collect();
14+
assert_eq!(["ExampleQuery"], operation_names.as_slice());
15+
let frags = ctx.fragments();
16+
let fragments: Vec<_> = frags.iter().map(|frag| frag.name()).collect();
17+
assert_eq!(["vipCustomer"], fragments.as_slice());
18+
19+
let operation_variables: Vec<String> = ctx
20+
.operations()
21+
.find("ExampleQuery")?
22+
.variables()
23+
.iter()
24+
.map(|var| var.name())
25+
.collect();
26+
27+
assert_eq!(["definedVariable"], operation_variables.as_slice());
28+
ctx.fragments().find("vipCustomer")
29+
}
30+
31+
fn main() -> Result<(), ()> {
32+
match compile_query() {
33+
Some(_fragment) => Ok(()),
34+
None => Err(()),
35+
}
36+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
query ExampleQuery($definedVariable: String) {
2+
topProducts(first: $undefinedVariable) {
3+
name
4+
}
5+
}
6+
7+
fragment vipCustomer on User {
8+
id
9+
name
10+
profilePic(size: 50)
11+
status
12+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// NOTE @lrlna: only syntax errors currently have the source data.
2+
//
3+
// TODO: figure out a nice way of going back to the AST and get its source data
4+
// given a current Value, which will make sure the rest of the diagnostics have
5+
// source data.
6+
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
7+
pub enum ApolloDiagnostic {
8+
Error(ErrorDiagnostic),
9+
Warning(WarningDiagnostic),
10+
Hint(HintDiagnostic),
11+
Suggestion(SuggestionDiagnostic),
12+
}
13+
14+
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
15+
pub enum ErrorDiagnostic {
16+
MissingIdent(String),
17+
SingleRootField(String),
18+
UniqueOperationDefinition {
19+
message: String,
20+
operation: String,
21+
},
22+
UndefinedVariable {
23+
message: String,
24+
variable: String,
25+
},
26+
SyntaxError {
27+
message: String,
28+
data: String,
29+
index: usize,
30+
},
31+
}
32+
33+
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
34+
pub enum WarningDiagnostic {
35+
UnusedVariable { message: String, variable: String },
36+
}
37+
38+
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
39+
pub enum HintDiagnostic {}
40+
41+
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
42+
pub enum SuggestionDiagnostic {}

crates/apollo-compiler/src/lib.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
mod diagnostics;
2+
mod queries;
3+
#[cfg(test)]
4+
mod tests;
5+
mod validation;
6+
7+
use std::sync::Arc;
8+
9+
use apollo_parser::{ast, SyntaxTree};
10+
pub use queries::{
11+
database::{Database, SourceDatabase},
12+
values,
13+
};
14+
15+
use diagnostics::ApolloDiagnostic;
16+
use validation::Validator;
17+
18+
pub struct ApolloCompiler {
19+
db: Database,
20+
}
21+
22+
impl ApolloCompiler {
23+
pub fn new(input: &str) -> Self {
24+
let mut db = Database::default();
25+
let input = input.to_string();
26+
db.set_input_string((), Arc::new(input));
27+
Self { db }
28+
}
29+
30+
pub fn parse(&self) -> Arc<SyntaxTree> {
31+
self.db.parse()
32+
}
33+
34+
// should probably return an iter here
35+
pub fn validate(&self) -> Vec<ApolloDiagnostic> {
36+
let mut validator = Validator::new(&self.db);
37+
validator.validate().into()
38+
}
39+
40+
pub fn syntax_errors(&self) -> Vec<ApolloDiagnostic> {
41+
self.db.syntax_errors()
42+
}
43+
44+
pub fn definitions(&self) -> Arc<Vec<ast::Definition>> {
45+
self.db.definitions()
46+
}
47+
48+
pub fn operations(&self) -> values::Operations {
49+
self.db.operations()
50+
}
51+
52+
pub fn fragments(&self) -> values::Fragments {
53+
self.db.fragments()
54+
}
55+
}
56+
57+
#[cfg(test)]
58+
mod test {
59+
use crate::ApolloCompiler;
60+
61+
#[test]
62+
fn it_accesses_operation_definition_parts() {
63+
let input = r#"
64+
query ExampleQuery($definedVariable: Int, $definedVariable2: Boolean) {
65+
66+
topProducts(first: $definedVariable) {
67+
name
68+
}
69+
... vipCustomer
70+
}
71+
72+
fragment vipCustomer on User {
73+
id
74+
name
75+
profilePic(size: 50)
76+
status(activity: $definedVariable2)
77+
}
78+
"#;
79+
80+
let ctx = ApolloCompiler::new(input);
81+
let errors = ctx.validate();
82+
83+
assert!(errors.is_empty());
84+
85+
let operations = ctx.operations();
86+
let operation_names: Vec<_> = operations.iter().filter_map(|op| op.name()).collect();
87+
assert_eq!(["ExampleQuery"], operation_names.as_slice());
88+
89+
let fragments = ctx.fragments();
90+
let fragment_names: Vec<_> = fragments.iter().map(|fragment| fragment.name()).collect();
91+
assert_eq!(["vipCustomer"], fragment_names.as_slice());
92+
93+
let operation_variables: Vec<String> = match operations.find("ExampleQuery") {
94+
Some(op) => op.variables().iter().map(|var| var.name.clone()).collect(),
95+
None => Vec::new(),
96+
};
97+
assert_eq!(
98+
["definedVariable", "definedVariable2"],
99+
operation_variables.as_slice()
100+
);
101+
}
102+
}

0 commit comments

Comments
 (0)