Skip to content

Commit e028188

Browse files
committed
Implement AST visitor
1 parent 1b4707b commit e028188

File tree

6 files changed

+614
-47
lines changed

6 files changed

+614
-47
lines changed

graphql/core/language/parser.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ def parse_fragment(parser):
299299
if parser.token.value == 'on':
300300
advance(parser)
301301
return ast.InlineFragment(
302-
type_condition=parse_name(parser),
302+
type_condition=parse_named_type(parser),
303303
directives=parse_directives(parser),
304304
selection_set=parse_selection_set(parser),
305305
loc=loc(parser, start)
@@ -318,7 +318,7 @@ def parse_fragment_definition(parser):
318318
name=parse_name(parser),
319319
type_condition=(
320320
expect_keyword(parser, 'on'),
321-
parse_name(parser))[1],
321+
parse_named_type(parser))[1],
322322
directives=parse_directives(parser),
323323
selection_set=parse_selection_set(parser),
324324
loc=loc(parser, start)
@@ -436,7 +436,15 @@ def parse_type(parser):
436436
expect(parser, TokenKind.BRACKET_R)
437437
type = ast.ListType(type=type, loc=loc(parser, start))
438438
else:
439-
type = parse_name(parser)
439+
type = parse_named_type(parser)
440440
if skip(parser, TokenKind.BANG):
441441
return ast.NonNullType(type=type, loc=loc(parser, start))
442442
return type
443+
444+
445+
def parse_named_type(parser):
446+
start = parser.token.start
447+
return ast.NamedType(
448+
name=parse_name(parser),
449+
loc=loc(parser, start),
450+
)

graphql/core/language/visitor.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
from copy import copy
2+
from collections import namedtuple
3+
from . import ast
4+
5+
QUERY_DOCUMENT_KEYS = {
6+
ast.Name: (),
7+
8+
ast.Document: ('definitions', ),
9+
ast.OperationDefinition: ('name', 'variable_definitions', 'directives', 'selection_set'),
10+
ast.VariableDefinition: ('variable', 'type', 'default_value'),
11+
ast.Variable: ('name', ),
12+
ast.SelectionSet: ('selections', ),
13+
ast.Field: ('alias', 'name', 'arguments', 'directives', 'selection_set'),
14+
ast.Argument: ('name', 'value'),
15+
16+
ast.FragmentSpread: ('name', 'directives'),
17+
ast.InlineFragment: ('type_condition', 'directives', 'selection_set'),
18+
ast.FragmentDefinition: ('name', 'type_condition', 'directives', 'selection_set'),
19+
20+
ast.IntValue: (),
21+
ast.FloatValue: (),
22+
ast.StringValue: (),
23+
ast.BooleanValue: (),
24+
ast.EnumValue: (),
25+
ast.ListValue: ('values', ),
26+
ast.ObjectValue: ('fields', ),
27+
ast.ObjectField: ('name', 'value'),
28+
29+
ast.Directive: ('name', 'arguments'),
30+
31+
ast.NamedType: ('name', ),
32+
ast.ListType: ('type', ),
33+
ast.NonNullType: ('type', ),
34+
}
35+
36+
BREAK = object()
37+
REMOVE = object()
38+
39+
Stack = namedtuple('Stack', 'in_array index keys edits prev')
40+
41+
42+
def visit(root, visitor, key_map=None):
43+
visitor_keys = key_map or QUERY_DOCUMENT_KEYS
44+
45+
stack = None
46+
in_array = isinstance(root, list)
47+
keys = [root]
48+
index = -1
49+
edits = []
50+
parent = None
51+
path = []
52+
ancestors = []
53+
new_root = root
54+
55+
while True:
56+
index += 1
57+
is_leaving = index == len(keys)
58+
key = None
59+
node = None
60+
is_edited = is_leaving and len(edits) != 0
61+
if is_leaving:
62+
key = path.pop() if len(ancestors) != 0 else None
63+
node = parent
64+
parent = ancestors.pop() if len(ancestors) != 0 else None
65+
if is_edited:
66+
if in_array:
67+
node = list(node)
68+
else:
69+
node = copy(node)
70+
edit_offset = 0
71+
for edit_key, edit_value in edits:
72+
if in_array:
73+
edit_key -= edit_offset
74+
if in_array and edit_value is REMOVE:
75+
node.pop(edit_key)
76+
edit_offset += 1
77+
else:
78+
if isinstance(node, list):
79+
node[edit_key] = edit_value
80+
else:
81+
node = node._replace(**{edit_key: edit_value})
82+
index = stack.index
83+
keys = stack.keys
84+
edits = stack.edits
85+
in_array = stack.in_array
86+
stack = stack.prev
87+
else:
88+
if parent:
89+
key = index if in_array else keys[index]
90+
if isinstance(parent, list):
91+
node = parent[key]
92+
else:
93+
node = getattr(parent, key, None)
94+
else:
95+
key = None
96+
node = new_root
97+
if node is None:
98+
continue
99+
if parent:
100+
path.append(key)
101+
102+
result = None
103+
if not isinstance(node, list):
104+
assert is_node(node), 'Invalid AST Node: ' + node
105+
if is_leaving:
106+
result = visitor.leave(node, key, parent, path, ancestors)
107+
else:
108+
result = visitor.enter(node, key, parent, path, ancestors)
109+
if result is BREAK:
110+
break
111+
112+
if result is False:
113+
if not is_leaving:
114+
path.pop()
115+
continue
116+
elif result is not None:
117+
edits.append((key, result))
118+
if not is_leaving:
119+
if result is not REMOVE:
120+
# TODO: check result is valid node
121+
node = result
122+
else:
123+
path.pop()
124+
continue
125+
126+
if result is None and is_edited:
127+
edits.append((key, node))
128+
129+
if not is_leaving:
130+
stack = Stack(in_array, index, keys, edits, prev=stack)
131+
in_array = isinstance(node, list)
132+
keys = node if in_array else visitor_keys.get(type(node), [])
133+
index = -1
134+
edits = []
135+
if parent:
136+
ancestors.append(parent)
137+
parent = node
138+
139+
if not stack:
140+
break
141+
142+
if edits:
143+
new_root = edits[0][1]
144+
145+
return new_root
146+
147+
148+
def is_node(maybe_node):
149+
return isinstance(maybe_node, object) # FIXME
150+
151+
152+
class Visitor(object):
153+
def enter(self, node, key, parent, path, ancestors):
154+
self._call_kind_specific_visitor('enter_', node, key, parent, path, ancestors)
155+
156+
def leave(self, node, key, parent, path, ancestors):
157+
self._call_kind_specific_visitor('leave_', node, key, parent, path, ancestors)
158+
159+
def _call_kind_specific_visitor(self, prefix, node, key, parent, path, ancestors):
160+
node_kind = type(node).__name__
161+
method_name = prefix + node_kind
162+
method = getattr(self, method_name, None)
163+
if method:
164+
method(node, key, parent, path, ancestors)

graphql/core/utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .language.ast import ListType, NonNullType, Name
1+
from .language.ast import ListType, NonNullType, NamedType
22
from .type.definition import GraphQLList, GraphQLNonNull
33

44

@@ -15,8 +15,8 @@ def type_from_ast(schema, input_type_ast):
1515
return GraphQLNonNull(inner_type)
1616
else:
1717
return None
18-
assert isinstance(input_type_ast, Name), 'Must be a type name.'
19-
return schema.get_type(input_type_ast.value)
18+
assert isinstance(input_type_ast, NamedType), 'Must be a type name.'
19+
return schema.get_type(input_type_ast.name.value)
2020

2121

2222
def is_nullish(value):

tests/core_language/fixtures.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
KITCHEN_SINK = """
2+
# Copyright (c) 2015, Facebook, Inc.
3+
# All rights reserved.
4+
#
5+
# This source code is licensed under the BSD-style license found in the
6+
# LICENSE file in the root directory of this source tree. An additional grant
7+
# of patent rights can be found in the PATENTS file in the same directory.
8+
9+
query queryName($foo: ComplexType, $site: Site = MOBILE) {
10+
whoever123is: node(id: [123, 456]) {
11+
id ,
12+
... on User @defer {
13+
field2 {
14+
id ,
15+
alias: field1(first:10, after:$foo,) @include(if: $foo) {
16+
id,
17+
...frag
18+
}
19+
}
20+
}
21+
}
22+
}
23+
24+
mutation likeStory {
25+
like(story: 123) @defer {
26+
story {
27+
id
28+
}
29+
}
30+
}
31+
32+
fragment frag on Friend {
33+
foo(size: $size, bar: $b, obj: {key: "value"})
34+
}
35+
36+
{
37+
unnamed(truthy: true, falsey: false),
38+
query
39+
}
40+
"""

tests/core_language/test_parser.py

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,47 +3,7 @@
33
from graphql.core.language.source import Source
44
from graphql.core.language.parser import parse
55
from graphql.core.language import ast
6-
7-
KITCHEN_SINK = """
8-
# Copyright (c) 2015, Facebook, Inc.
9-
# All rights reserved.
10-
#
11-
# This source code is licensed under the BSD-style license found in the
12-
# LICENSE file in the root directory of this source tree. An additional grant
13-
# of patent rights can be found in the PATENTS file in the same directory.
14-
15-
query queryName($foo: ComplexType, $site: Site = MOBILE) {
16-
whoever123is: node(id: [123, 456]) {
17-
id ,
18-
... on User @defer {
19-
field2 {
20-
id ,
21-
alias: field1(first:10, after:$foo,) @include(if: $foo) {
22-
id,
23-
...frag
24-
}
25-
}
26-
}
27-
}
28-
}
29-
30-
mutation likeStory {
31-
like(story: 123) @defer {
32-
story {
33-
id
34-
}
35-
}
36-
}
37-
38-
fragment frag on Friend {
39-
foo(size: $size, bar: $b, obj: {key: "value"})
40-
}
41-
42-
{
43-
unnamed(truthy: true, falsey: false),
44-
query
45-
}
46-
"""
6+
from fixtures import KITCHEN_SINK
477

488

499
def test_parse_provides_useful_errors():

0 commit comments

Comments
 (0)