1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
|
---
layout: guide
doc_stub: false
search: true
section: Language Tools
title: AST Visitor
desc: Analyze and modify parsed GraphQL code
index: 0
---
GraphQL code is usually contained in a string, for example:
```ruby
query_string = "query { user(id: \"1\") { userName } }"
```
You can perform programmatic analysis and modifications to GraphQL code using a three-step process:
- __Parse__ the code into an abstract syntax tree
- __Analyze/Modify__ the code with a visitor
- __Print__ the code back to a string
## Parse
{{ "GraphQL.parse" | api_doc }} turns a string into a GraphQL document:
```ruby
parsed_doc = GraphQL.parse("{ user(id: \"1\") { userName } }")
# => #<GraphQL::Language::Nodes::Document ...>
```
Also, {{ "GraphQL.parse_file" | api_doc }} parses the contents of the named file and includes a `filename` in the parsed document.
#### AST Nodes
The parsed document is a tree of nodes, called an _abstract syntax tree_ (AST). This tree is _immutable_: once a document has been parsed, those Ruby objects can't be changed. Modifications are performed by _copying_ existing nodes, applying changes to the copy, then making a new tree to hold the copied node. Where possible, unmodified nodes are retained in the new tree (it's _persistent_).
The copy-and-modify workflow is supported by a few methods on the AST nodes:
- `.merge(new_attrs)` returns a copy of the node with `new_attrs` applied. This new copy can replace the original node.
- `.add_{child}(new_child_attrs)` makes a new node with `new_child_attrs`, adds it to the array specified by `{child}`, and returns a copy whose `{children}` array contains the newly created node.
For example, to rename a field and add an argument to it, you could:
```ruby
modified_node = field_node
# Apply a new name
.merge(name: "newName")
# Add an argument to this field's arguments
.add_argument(name: "newArgument", value: "newValue")
```
Above, `field_node` is unmodified, but `modified_node` reflects the new name and new argument.
## Analyze/Modify
To inspect or modify a parsed document, extend {{ "GraphQL::Language::Visitor" | api_doc }} and implement its various hooks. It's an implementation of the [visitor pattern](https://en.wikipedia.org/wiki/Visitor_pattern). In short, each node of the tree will be "visited" by calling a method, and those methods can gather information and perform modifications.
In the visitor, each node class has a hook, for example:
- {{ "GraphQL::Language::Nodes::Field" | api_doc }}s are routed to `#on_field`
- {{ "GraphQL::Language::Nodes::Argument" | api_doc }}s are routed to `#on_argument`
See the {{ "GraphQL::Language::Visitor" | api_doc }} API docs for a full list of methods.
Each method is called with `(node, parent)`, where:
- `node` is the AST node currently visited
- `parent` is the AST node above this node in the tree
The method has a few options for analyzing or modifying the AST:
#### Continue/Halt
To continue visiting, the hook should call `super`. This allows the visit to continue to `node`'s children in the tree, for example:
```ruby
def on_field(_node, _parent)
# Do nothing, this is the default behavior:
super
end
```
To _halt_ the visit, a method may skip the call to `super`. For example, if the visitor encountered an error, it might want to return early instead of continuing to visit.
#### Modify a Node
Visitor hooks are expected to return the `(node, parent)` they are called with. If they return a different node, then that node will replace the original `node`. When you call `super(node, parent)`, the `node` is returned. So, to modify a node and continue visiting:
- Make a modified copy of `node`
- Pass the modified copy to `super(new_node, parent)`
For example, to rename an argument:
```ruby
def on_argument(node, parent)
# make a copy of `node` with a new name
modified_node = node.merge(name: "renamed")
# continue visiting with the modified node and parent
super(modified_node, parent)
end
```
#### Delete a Node
To delete the currently-visited `node`, don't pass `node` to `super(...)`. Instead, pass a magic constant, `DELETE_NODE`, in place of `node`.
For example, to delete a directive:
```ruby
def on_directive(node, parent)
# Don't pass `node` to `super`,
# instead, pass `DELETE_NODE`
super(DELETE_NODE, parent)
end
```
#### Insert a Node
Inserting nodes is similar to modifying nodes. To insert a new child into `node`, call one of its `.add_` helpers. This returns a copied node with a new child added. For example, to add a selection to a field's selection set:
```ruby
def on_field(node, parent)
node_with_selection = node.add_selection(name: "emailAddress")
super(node_with_selection, parent)
end
```
This will add `emailAddress` the fields selection on `node`.
(These `.add_*` helpers are wrappers around {{ "GraphQL::Language::Nodes::AbstractNode#merge" | api_doc }}.)
## Print
The easiest way to turn an AST back into a string of GraphQL is {{ "GraphQL::Language::Nodes::AbstractNode#to_query_string" | api_doc }}, for example:
```ruby
parsed_doc.to_query_string
# => '{ user(id: "1") { userName } }'
```
You can also create a subclass of {{ "GraphQL::Language::Printer" | api_doc }} to customize how nodes are printed.
|