Skip to content

Commit 9c0d45d

Browse files
committed
Sketch of a visitor pattern
This visitor pattern is designed for doing normalizations on executable documents, so it assumes you will mutate the tree as it is walked. (Perhaps a better design would be to make each visitor return an object of the type it is visiting (which could be the same object it received), constructing a new tree (possibly sharing most of its data) immutably.) This highlights some places where it would be helpful if the AST contained more explicit types where it currently only has variants, type aliases, or vecs. By having more little structs in the type system, it's easier to write a visitor like this that can easily target (say) every Name node. This includes: - It would be helpful if all enums (which actually contain data, so not OperationType) had a specific type for each variant, so that visitors can choose to visit that variant specifically. Definition and Selection work this way already; Type and Value do not. This is especially helpful for Value, as it would be nice to be able to write visitors that visit just string values or int values or whatever without needing to match against the Value. - Most uses of NodeStr in the AST are names or descriptions. It would be helpful if there were (non-alias) Name and Description types that could implement Walkable like the other AST types, and then you could implement `visit_name`, etc. (Sure, we could just call `visit_name` directly from each Walkable implementation for a type containing a name, but keeping the strict pattern of "each `walk` calls `visit_*` exactly once and then calls `walk` on its children" seems nice.) - It would be nice if some more of the Vec types were a struct like Directives, though I suppose implementing Walkable on `Vec<Selection>` isn't that much more awkward than implementing it on `SelectionSet`. I'm thinking `SelectionSet` and `Arguments` specifically. It's possible I'm missing something and the status quo is pretty reasonable though. It would be nice if apollo-compiler had some sort of built-in visitor API; maybe this one, maybe a more immutable one, maybe something else. Whether or not apollo-compiler adds such an API, this PR hopefully at least points out some potential improvements in the AST data types.
1 parent afb756d commit 9c0d45d

File tree

2 files changed

+349
-0
lines changed

2 files changed

+349
-0
lines changed

crates/apollo-compiler/src/ast/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use std::sync::Arc;
3939
pub(crate) mod from_cst;
4040
pub(crate) mod impls;
4141
pub(crate) mod serialize;
42+
pub(crate) mod visitor;
4243

4344
pub use self::serialize::Serialize;
4445

Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
use super::*;
2+
3+
/// Visitor for an AST. Implement this trait and pass it to
4+
/// document.walk(visitor). This visitor is expected to mutate the AST, so each
5+
/// call gets `&mut` (and the AST gets fully copied-on-write as it walks it).
6+
/// Visitors cannot control the walk or return a value, but they can mutate
7+
/// their nodes. It is a preorder traversal; any changes made by a `visit_*`
8+
/// function affect which children are visited.
9+
trait Visitor {
10+
fn visit_document(&mut self, _document: &mut Document) {}
11+
fn visit_operation_definition(&mut self, _operation_definition: &mut OperationDefinition) {}
12+
fn visit_fragment_definition(&mut self, _fragment_definition: &mut FragmentDefinition) {}
13+
fn visit_directive_definition(&mut self, _directive_definition: &mut DirectiveDefinition) {}
14+
fn visit_arguments(&mut self, _argument: &mut Vec<Node<Argument>>) {}
15+
fn visit_argument(&mut self, _argument: &mut Argument) {}
16+
fn visit_directives(&mut self, _directives: &mut Directives) {}
17+
fn visit_directive(&mut self, _directive: &mut Directive) {}
18+
fn visit_operation_type(&mut self, _operation_type: &mut OperationType) {}
19+
fn visit_directive_location(&mut self, _directive_location: &mut DirectiveLocation) {}
20+
fn visit_variable_definition(&mut self, _variable_definition: &mut VariableDefinition) {}
21+
// XXX Maybe should visit the individual enum variants instead?
22+
// We aren't calling this recursively right now.
23+
fn visit_type(&mut self, _ty: &mut Type) {}
24+
fn visit_input_value_definition(&mut self, _input_value_definition: &mut InputValueDefinition) {
25+
}
26+
fn visit_selection_set(&mut self, _selection_set: &mut Vec<Selection>) {}
27+
fn visit_field(&mut self, _field: &mut Field) {}
28+
fn visit_fragment_spread(&mut self, _fragment_spread: &mut FragmentSpread) {}
29+
fn visit_inline_fragment(&mut self, _inline_fragment: &mut InlineFragment) {}
30+
fn visit_value(&mut self, _value: &mut Value) {}
31+
}
32+
33+
/// Trait encapsulating the traversing logic for the AST
34+
trait Walkable {
35+
/// Visit children of the current node
36+
fn walk<V: Visitor>(&mut self, visitor: &mut V);
37+
}
38+
39+
impl Walkable for Document {
40+
fn walk<V: Visitor>(&mut self, visitor: &mut V) {
41+
visitor.visit_document(self);
42+
for definition in &mut self.definitions {
43+
definition.walk(visitor);
44+
}
45+
}
46+
}
47+
48+
impl Walkable for Definition {
49+
fn walk<V: Visitor>(&mut self, visitor: &mut V) {
50+
match self {
51+
Definition::OperationDefinition(node) => node.make_mut().walk(visitor),
52+
Definition::FragmentDefinition(node) => node.make_mut().walk(visitor),
53+
Definition::DirectiveDefinition(node) => node.make_mut().walk(visitor),
54+
_ => panic!("SDL visitors not implemented"),
55+
}
56+
}
57+
}
58+
59+
impl Walkable for OperationDefinition {
60+
fn walk<V: Visitor>(&mut self, visitor: &mut V) {
61+
visitor.visit_operation_definition(self);
62+
self.operation_type.walk(visitor);
63+
// #MoreSpecificTypes self.name.walk(visitor);
64+
for variable in &mut self.variables {
65+
variable.make_mut().walk(visitor);
66+
}
67+
self.directives.walk(visitor);
68+
self.selection_set.walk(visitor);
69+
}
70+
}
71+
72+
impl Walkable for FragmentDefinition {
73+
fn walk<V: Visitor>(&mut self, visitor: &mut V) {
74+
visitor.visit_fragment_definition(self);
75+
// #MoreSpecificTypes self.name.walk(visitor);
76+
// #MoreSpecificTypes self.type_condition.walk(visitor);
77+
self.directives.walk(visitor);
78+
self.selection_set.walk(visitor);
79+
}
80+
}
81+
82+
impl Walkable for DirectiveDefinition {
83+
fn walk<V: Visitor>(&mut self, visitor: &mut V) {
84+
visitor.visit_directive_definition(self);
85+
// #MoreSpecificTypes self.description.walk(visitor);
86+
// #MoreSpecificTypes self.name.walk(visitor);
87+
for argument in &mut self.arguments {
88+
argument.make_mut().walk(visitor);
89+
}
90+
for location in &mut self.locations {
91+
location.walk(visitor);
92+
}
93+
}
94+
}
95+
96+
impl Walkable for Vec<Node<Argument>> {
97+
fn walk<V: Visitor>(&mut self, visitor: &mut V) {
98+
visitor.visit_arguments(self);
99+
for argument in self {
100+
argument.make_mut().walk(visitor);
101+
}
102+
}
103+
}
104+
105+
impl Walkable for Argument {
106+
fn walk<V: Visitor>(&mut self, visitor: &mut V) {
107+
visitor.visit_argument(self);
108+
// #MoreSpecificTypes self.name.walk(visitor);
109+
self.value.make_mut().walk(visitor);
110+
}
111+
}
112+
113+
impl Walkable for Directives {
114+
fn walk<V: Visitor>(&mut self, visitor: &mut V) {
115+
visitor.visit_directives(self);
116+
for directive in &mut self.0 {
117+
directive.make_mut().walk(visitor);
118+
}
119+
}
120+
}
121+
122+
impl Walkable for Directive {
123+
fn walk<V: Visitor>(&mut self, visitor: &mut V) {
124+
visitor.visit_directive(self);
125+
// #MoreSpecificTypes self.name.walk(visitor);
126+
self.arguments.walk(visitor);
127+
}
128+
}
129+
130+
impl Walkable for OperationType {
131+
fn walk<V: Visitor>(&mut self, visitor: &mut V) {
132+
visitor.visit_operation_type(self);
133+
}
134+
}
135+
136+
impl Walkable for DirectiveLocation {
137+
fn walk<V: Visitor>(&mut self, visitor: &mut V) {
138+
visitor.visit_directive_location(self);
139+
}
140+
}
141+
142+
impl Walkable for VariableDefinition {
143+
fn walk<V: Visitor>(&mut self, visitor: &mut V) {
144+
visitor.visit_variable_definition(self);
145+
// #MoreSpecificTypes self.name.walk(visitor);
146+
self.ty.make_mut().walk(visitor);
147+
self.default_value
148+
.as_mut()
149+
.map(|value| value.make_mut().walk(visitor));
150+
self.directives.walk(visitor);
151+
}
152+
}
153+
154+
impl Walkable for Type {
155+
fn walk<V: Visitor>(&mut self, visitor: &mut V) {
156+
visitor.visit_type(self);
157+
// No recursive walks for the moment (including to the name). #MoreSpecificTypes
158+
}
159+
}
160+
161+
impl Walkable for InputValueDefinition {
162+
fn walk<V: Visitor>(&mut self, visitor: &mut V) {
163+
visitor.visit_input_value_definition(self);
164+
// #MoreSpecificTypes self.name.walk(visitor);
165+
self.ty.make_mut().walk(visitor);
166+
self.default_value
167+
.as_mut()
168+
.map(|value| value.make_mut().walk(visitor));
169+
self.directives.walk(visitor);
170+
}
171+
}
172+
173+
impl Walkable for Vec<Selection> {
174+
fn walk<V: Visitor>(&mut self, visitor: &mut V) {
175+
visitor.visit_selection_set(self);
176+
for selection in self {
177+
selection.walk(visitor);
178+
}
179+
}
180+
}
181+
182+
impl Walkable for Selection {
183+
fn walk<V: Visitor>(&mut self, visitor: &mut V) {
184+
match self {
185+
Selection::Field(node) => node.make_mut().walk(visitor),
186+
Selection::FragmentSpread(node) => node.make_mut().walk(visitor),
187+
Selection::InlineFragment(node) => node.make_mut().walk(visitor),
188+
}
189+
}
190+
}
191+
192+
impl Walkable for Field {
193+
fn walk<V: Visitor>(&mut self, visitor: &mut V) {
194+
visitor.visit_field(self);
195+
// #MoreSpecificTypes self.alias.walk(visitor);
196+
// #MoreSpecificTypes self.name.walk(visitor);
197+
self.arguments.walk(visitor);
198+
self.directives.walk(visitor);
199+
self.selection_set.walk(visitor);
200+
}
201+
}
202+
203+
impl Walkable for FragmentSpread {
204+
fn walk<V: Visitor>(&mut self, visitor: &mut V) {
205+
visitor.visit_fragment_spread(self);
206+
// #MoreSpecificTypes self.fragment_name.walk(visitor);
207+
self.directives.walk(visitor);
208+
}
209+
}
210+
211+
impl Walkable for InlineFragment {
212+
fn walk<V: Visitor>(&mut self, visitor: &mut V) {
213+
visitor.visit_inline_fragment(self);
214+
// #MoreSpecificTypes self.type_condition.walk(visitor);
215+
self.directives.walk(visitor);
216+
self.selection_set.walk(visitor);
217+
}
218+
}
219+
220+
impl Walkable for Value {
221+
fn walk<V: Visitor>(&mut self, visitor: &mut V) {
222+
// #MoreSpecificTypes I would rather have visitors for StringValue,
223+
// IntValue, etc, instead of a Value visitor that has to match.
224+
visitor.visit_value(self);
225+
match self {
226+
Value::List(values) => {
227+
for value in values {
228+
value.make_mut().walk(visitor);
229+
}
230+
}
231+
Value::Object(values) => {
232+
for (_, value) in values {
233+
value.make_mut().walk(visitor);
234+
}
235+
}
236+
// Nothing else is needed for non-recursive cases.
237+
_ => {}
238+
}
239+
}
240+
}
241+
242+
#[cfg(test)]
243+
mod tests {
244+
use crate::Diagnostics;
245+
246+
use super::*;
247+
248+
struct HideStringAndNumericLiterals;
249+
250+
impl Visitor for HideStringAndNumericLiterals {
251+
fn visit_value(&mut self, value: &mut Value) {
252+
// This is awkward; I'd prefer to define visit_int_value, etc.
253+
match value {
254+
Value::Int(int_value) => int_value.0 = "0".to_string(),
255+
Value::Float(float_value) => float_value.0 = "0".to_string(),
256+
Value::String(node_str) => *node_str = NodeStr::new(""),
257+
_ => {}
258+
}
259+
}
260+
}
261+
262+
#[test]
263+
fn it_hides_string_and_numeric_literals() -> Result<(), Diagnostics> {
264+
let input = r#"
265+
query Foo($b: Int = 5, $a: Boolean, $c: String = "asdf", $d: Float = 0.5) {
266+
user(
267+
name: "hello"
268+
age: 5
269+
pct: 0.4
270+
lst: ["a", "b", "c"]
271+
obj: { a: "a", b: 1 }
272+
) {
273+
...Bar
274+
... on User {
275+
hello
276+
bee
277+
}
278+
tz @someDirective(a: [500])
279+
aliased: name
280+
withInputs(
281+
str: "hi"
282+
int: 2
283+
flt: 0.3
284+
lst: ["", "", ""]
285+
obj: { q: "", s: 0 }
286+
)
287+
}
288+
}
289+
290+
fragment Bar on User {
291+
age @skip(if: $a)
292+
...Nested
293+
}
294+
295+
fragment Nested on User {
296+
blah
297+
}
298+
"#;
299+
300+
let expected_output = r#"
301+
query Foo($b: Int = 0, $a: Boolean, $c: String = "", $d: Float = 0) {
302+
user(
303+
name: ""
304+
age: 0
305+
pct: 0
306+
lst: ["", "", ""]
307+
obj: { a: "", b: 0 }
308+
) {
309+
...Bar
310+
... on User {
311+
hello
312+
bee
313+
}
314+
tz @someDirective(a: [0])
315+
aliased: name
316+
withInputs(
317+
str: ""
318+
int: 0
319+
flt: 0
320+
lst: ["", "", ""]
321+
obj: { q: "", s: 0 }
322+
)
323+
}
324+
}
325+
326+
fragment Bar on User {
327+
age @skip(if: $a)
328+
...Nested
329+
}
330+
331+
fragment Nested on User {
332+
blah
333+
}
334+
"#;
335+
let mut document = Document::parse(input, "input.graphql");
336+
document.check_parse_errors()?;
337+
document.walk(&mut HideStringAndNumericLiterals);
338+
339+
// We don't care about formatting differences, so normalize the above
340+
// expected output.
341+
let normalized_expected_output =
342+
Document::parse(expected_output, "expected_output.graphql").to_string();
343+
344+
assert_eq!(document.to_string(), normalized_expected_output);
345+
346+
Ok(())
347+
}
348+
}

0 commit comments

Comments
 (0)