Skip to content

Commit ccf167c

Browse files
committed
[Validation] Memoize collecting variable usage.
Related GraphQL-js commit: graphql/graphql-js@2afbff7
1 parent 6651507 commit ccf167c

File tree

5 files changed

+131
-111
lines changed

5 files changed

+131
-111
lines changed

graphql/core/validation/context.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,37 @@
1-
from ..language.ast import FragmentDefinition, FragmentSpread
1+
from ..language.ast import FragmentDefinition, FragmentSpread, VariableDefinition, Variable, OperationDefinition
2+
from ..utils.type_info import TypeInfo
3+
from ..language.visitor import Visitor, visit
4+
5+
6+
class VariableUsage(object):
7+
__slots__ = 'node', 'type'
8+
9+
def __init__(self, node, type):
10+
self.node = node
11+
self.type = type
12+
13+
14+
class UsageVisitor(Visitor):
15+
__slots__ = 'context', 'usages', 'type_info'
16+
17+
def __init__(self, usages, type_info):
18+
self.usages = usages
19+
self.type_info = type_info
20+
21+
def enter(self, node, key, parent, path, ancestors):
22+
self.type_info.enter(node)
23+
if isinstance(node, VariableDefinition):
24+
return False
25+
elif isinstance(node, Variable):
26+
usage = VariableUsage(node, type=self.type_info.get_input_type())
27+
self.usages.append(usage)
28+
29+
def leave(self, node, key, parent, path, ancestors):
30+
self.type_info.leave(node)
231

332

433
class ValidationContext(object):
5-
__slots__ = '_schema', '_ast', '_type_info', '_fragments', '_fragment_spreads', '_recursively_referenced_fragments'
34+
__slots__ = '_schema', '_ast', '_type_info', '_fragments', '_fragment_spreads', '_recursively_referenced_fragments', '_variable_usages', '_recursive_variable_usages'
635

736
def __init__(self, schema, ast, type_info):
837
self._schema = schema
@@ -11,11 +40,36 @@ def __init__(self, schema, ast, type_info):
1140
self._fragments = None
1241
self._fragment_spreads = {}
1342
self._recursively_referenced_fragments = {}
43+
self._variable_usages = {}
44+
self._recursive_variable_usages = {}
1445

1546
def get_schema(self):
1647
return self._schema
1748

49+
def get_variable_usages(self, node):
50+
usages = self._variable_usages.get(node)
51+
if usages is None:
52+
usages = []
53+
sub_visitor = UsageVisitor(usages, self._type_info)
54+
visit(node, sub_visitor)
55+
self._variable_usages[node] = usages
56+
57+
return usages
58+
59+
def get_recursive_variable_usages(self, operation):
60+
assert isinstance(operation, OperationDefinition)
61+
usages = self._recursive_variable_usages.get(operation)
62+
if usages is None:
63+
usages = self.get_variable_usages(operation)
64+
fragments = self.get_recursively_referenced_fragments(operation)
65+
for fragment in fragments:
66+
usages.extend(self.get_variable_usages(fragment))
67+
self._recursive_variable_usages[operation] = usages
68+
69+
return usages
70+
1871
def get_recursively_referenced_fragments(self, operation):
72+
assert isinstance(operation, OperationDefinition)
1973
fragments = self._recursively_referenced_fragments.get(operation)
2074
if not fragments:
2175
fragments = []

graphql/core/validation/rules/no_undefined_variables.py

Lines changed: 22 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,51 +4,38 @@
44

55

66
class NoUndefinedVariables(ValidationRule):
7-
__slots__ = 'visited_fragment_names', 'defined_variable_names', 'operation',
8-
visit_spread_fragments = True
7+
__slots__ = 'defined_variable_names',
98

109
def __init__(self, context):
11-
self.visited_fragment_names = set()
1210
self.defined_variable_names = set()
13-
self.operation = None
14-
1511
super(NoUndefinedVariables, self).__init__(context)
1612

1713
@staticmethod
18-
def undefined_var_message(var_name):
14+
def undefined_var_message(var_name, op_name=None):
15+
if op_name:
16+
return 'Variable "${}" is not defined by operation "{}".'.format(
17+
var_name, op_name
18+
)
1919
return 'Variable "${}" is not defined.'.format(var_name)
2020

21-
@staticmethod
22-
def undefined_var_by_op_message(var_name, op_name):
23-
return 'Variable "${}" is not defined by operation "{}".'.format(
24-
var_name, op_name
25-
)
26-
27-
def enter_OperationDefinition(self, node, key, parent, path, ancestors):
28-
self.operation = node
29-
self.visited_fragment_names = set()
21+
def enter_OperationDefinition(self, operation, key, parent, path, ancestors):
3022
self.defined_variable_names = set()
3123

32-
def enter_VariableDefinition(self, node, key, parent, path, ancestors):
33-
self.defined_variable_names.add(node.variable.name.value)
24+
def leave_OperationDefinition(self, operation, key, parent, path, ancestors):
25+
usages = self.context.get_recursive_variable_usages(operation)
26+
errors = []
3427

35-
def enter_Variable(self, variable, key, parent, path, ancestors):
36-
var_name = variable.name.value
37-
if var_name not in self.defined_variable_names:
38-
within_fragment = any(isinstance(node, ast.FragmentDefinition) for node in ancestors)
39-
if within_fragment and self.operation and self.operation.name:
40-
return GraphQLError(
41-
self.undefined_var_by_op_message(var_name, self.operation.name.value),
42-
[variable, self.operation]
43-
)
44-
45-
return GraphQLError(
46-
self.undefined_var_message(var_name),
47-
[variable]
48-
)
28+
for variable_usage in usages:
29+
node = variable_usage.node
30+
var_name = node.name.value
31+
if var_name not in self.defined_variable_names:
32+
errors.append(GraphQLError(
33+
self.undefined_var_message(var_name, operation.name and operation.name.value),
34+
[node, operation]
35+
))
4936

50-
def enter_FragmentSpread(self, spread_ast, *args):
51-
if spread_ast.name.value in self.visited_fragment_names:
52-
return False
37+
if errors:
38+
return errors
5339

54-
self.visited_fragment_names.add(spread_ast.name.value)
40+
def enter_VariableDefinition(self, node, key, parent, path, ancestors):
41+
self.defined_variable_names.add(node.variable.name.value)

graphql/core/validation/rules/no_unused_variables.py

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,50 +3,36 @@
33

44

55
class NoUnusedVariables(ValidationRule):
6-
__slots__ = 'visited_fragment_names', 'variable_definitions', 'variable_name_used', 'visit_spread_fragments'
6+
__slots__ = 'variable_definitions'
77

88
def __init__(self, context):
9-
self.visited_fragment_names = None
10-
self.variable_definitions = None
11-
self.variable_name_used = None
12-
self.visit_spread_fragments = True
9+
self.variable_definitions = []
1310
super(NoUnusedVariables, self).__init__(context)
1411

1512
def enter_OperationDefinition(self, node, key, parent, path, ancestors):
16-
self.visited_fragment_names = set()
1713
self.variable_definitions = []
18-
self.variable_name_used = set()
1914

20-
def leave_OperationDefinition(self, node, key, parent, path, ancestors):
15+
def leave_OperationDefinition(self, operation, key, parent, path, ancestors):
16+
variable_name_used = set()
17+
usages = self.context.get_recursive_variable_usages(operation)
18+
19+
for variable_usage in usages:
20+
variable_name_used.add(variable_usage.node.name.value)
21+
2122
errors = [
2223
GraphQLError(
2324
self.unused_variable_message(variable_definition.variable.name.value),
2425
[variable_definition]
2526
)
2627
for variable_definition in self.variable_definitions
27-
if variable_definition.variable.name.value not in self.variable_name_used
28+
if variable_definition.variable.name.value not in variable_name_used
2829
]
2930

3031
if errors:
3132
return errors
3233

3334
def enter_VariableDefinition(self, node, key, parent, path, ancestors):
34-
if self.variable_definitions is not None:
35-
self.variable_definitions.append(node)
36-
37-
return False
38-
39-
def enter_Variable(self, node, key, parent, path, ancestors):
40-
if self.variable_name_used is not None:
41-
self.variable_name_used.add(node.name.value)
42-
43-
def enter_FragmentSpread(self, node, key, parent, path, ancestors):
44-
if self.visited_fragment_names is not None:
45-
spread_name = node.name.value
46-
if spread_name in self.visited_fragment_names:
47-
return False
48-
49-
self.visited_fragment_names.add(spread_name)
35+
self.variable_definitions.append(node)
5036

5137
@staticmethod
5238
def unused_variable_message(variable_name):

graphql/core/validation/rules/variables_in_allowed_position.py

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,37 @@
88

99

1010
class VariablesInAllowedPosition(ValidationRule):
11-
visit_spread_fragments = True
12-
__slots__ = 'var_def_map', 'visited_fragment_names'
11+
__slots__ = 'var_def_map'
1312

1413
def __init__(self, context):
1514
super(VariablesInAllowedPosition, self).__init__(context)
1615
self.var_def_map = {}
17-
self.visited_fragment_names = set()
1816

1917
def enter_OperationDefinition(self, node, key, parent, path, ancestors):
2018
self.var_def_map = {}
21-
self.visited_fragment_names = set()
19+
20+
def leave_OperationDefinition(self, operation, key, parent, path, ancestors):
21+
usages = self.context.get_recursive_variable_usages(operation)
22+
errors = []
23+
24+
for usage in usages:
25+
node = usage.node
26+
type = usage.type
27+
var_name = node.name.value
28+
var_def = self.var_def_map.get(var_name)
29+
var_type = var_def and type_from_ast(self.context.get_schema(), var_def.type)
30+
if var_type and type and not self.var_type_allowed_for_type(self.effective_type(var_type, var_def), type):
31+
errors.append(GraphQLError(
32+
self.bad_var_pos_message(var_name, var_type, type),
33+
[node]
34+
))
35+
36+
if errors:
37+
return errors
2238

2339
def enter_VariableDefinition(self, node, key, parent, path, ancestors):
2440
self.var_def_map[node.variable.name.value] = node
2541

26-
def enter_Variable(self, node, key, parent, path, ancestors):
27-
var_name = node.name.value
28-
var_def = self.var_def_map.get(var_name)
29-
var_type = var_def and type_from_ast(self.context.get_schema(), var_def.type)
30-
input_type = self.context.get_input_type()
31-
if var_type and input_type and not self.var_type_allowed_for_type(self.effective_type(var_type, var_def),
32-
input_type):
33-
return GraphQLError(self.bad_var_pos_message(var_name, var_type, input_type),
34-
[node])
35-
36-
def enter_FragmentSpread(self, node, key, parent, path, ancestors):
37-
if node.name.value in self.visited_fragment_names:
38-
return False
39-
self.visited_fragment_names.add(node.name.value)
40-
4142
@staticmethod
4243
def effective_type(var_type, var_def):
4344
if not var_def.default_value or isinstance(var_type, GraphQLNonNull):

tests/core_validation/test_no_undefined_variables.py

Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,9 @@
33
from utils import expect_passes_rule, expect_fails_rule
44

55

6-
def undefined_var(var_name, line, column):
6+
def undefined_var(var_name, l1, c1, op_name, l2, c2):
77
return {
8-
'message': NoUndefinedVariables.undefined_var_message(var_name),
9-
'locations': [SourceLocation(line, column)]
10-
}
11-
12-
13-
def undefined_var_by_op(var_name, l1, c1, op_name, l2, c2):
14-
return {
15-
'message': NoUndefinedVariables.undefined_var_by_op_message(
16-
var_name, op_name),
8+
'message': NoUndefinedVariables.undefined_var_message(var_name, op_name),
179
'locations': [
1810
SourceLocation(l1, c1),
1911
SourceLocation(l2, c2),
@@ -128,7 +120,7 @@ def test_variable_not_defined():
128120
field(a: $a, b: $b, c: $c, d: $d)
129121
}
130122
''', [
131-
undefined_var('d', 3, 39)
123+
undefined_var('d', 3, 39, 'Foo', 2, 7)
132124
])
133125

134126

@@ -138,7 +130,7 @@ def variable_not_defined_by_unnamed_query():
138130
field(a: $a)
139131
}
140132
''', [
141-
undefined_var('a', 3, 18)
133+
undefined_var('a', 3, 18, '', 2, 7)
142134
])
143135

144136

@@ -148,8 +140,8 @@ def test_multiple_variables_not_defined():
148140
field(a: $a, b: $b, c: $c)
149141
}
150142
''', [
151-
undefined_var('a', 3, 18),
152-
undefined_var('c', 3, 32)
143+
undefined_var('a', 3, 18, 'Foo', 2, 7),
144+
undefined_var('c', 3, 32, 'Foo', 2, 7)
153145
])
154146

155147

@@ -162,7 +154,7 @@ def test_variable_in_fragment_not_defined_by_unnamed_query():
162154
field(a: $a)
163155
}
164156
''', [
165-
undefined_var('a', 6, 18)
157+
undefined_var('a', 6, 18, '', 2, 7)
166158
])
167159

168160

@@ -185,7 +177,7 @@ def test_variable_in_fragment_not_defined_by_operation():
185177
field(c: $c)
186178
}
187179
''', [
188-
undefined_var_by_op('c', 16, 18, 'Foo', 2, 7)
180+
undefined_var('c', 16, 18, 'Foo', 2, 7)
189181
])
190182

191183

@@ -208,8 +200,8 @@ def test_multiple_variables_in_fragments_not_defined():
208200
field(c: $c)
209201
}
210202
''', [
211-
undefined_var_by_op('a', 6, 18, 'Foo', 2, 7),
212-
undefined_var_by_op('c', 16, 18, 'Foo', 2, 7)
203+
undefined_var('a', 6, 18, 'Foo', 2, 7),
204+
undefined_var('c', 16, 18, 'Foo', 2, 7)
213205
])
214206

215207

@@ -225,8 +217,8 @@ def test_single_variable_in_fragment_not_defined_by_multiple_operations():
225217
field(a: $a, b: $b)
226218
}
227219
''', [
228-
undefined_var_by_op('b', 9, 25, 'Foo', 2, 7),
229-
undefined_var_by_op('b', 9, 25, 'Bar', 5, 7)
220+
undefined_var('b', 9, 25, 'Foo', 2, 7),
221+
undefined_var('b', 9, 25, 'Bar', 5, 7)
230222
])
231223

232224

@@ -242,8 +234,8 @@ def test_variables_in_fragment_not_defined_by_multiple_operations():
242234
field(a: $a, b: $b)
243235
}
244236
''', [
245-
undefined_var_by_op('a', 9, 18, 'Foo', 2, 7),
246-
undefined_var_by_op('b', 9, 25, 'Bar', 5, 7)
237+
undefined_var('a', 9, 18, 'Foo', 2, 7),
238+
undefined_var('b', 9, 25, 'Bar', 5, 7)
247239
])
248240

249241

@@ -262,8 +254,8 @@ def test_variable_in_fragment_used_by_other_operation():
262254
field(b: $b)
263255
}
264256
''', [
265-
undefined_var_by_op('a', 9, 18, 'Foo', 2, 7),
266-
undefined_var_by_op('b', 12, 18, 'Bar', 5, 7)
257+
undefined_var('a', 9, 18, 'Foo', 2, 7),
258+
undefined_var('b', 12, 18, 'Bar', 5, 7)
267259
])
268260

269261

@@ -284,10 +276,10 @@ def test_multiple_undefined_variables_produce_multiple_errors():
284276
field2(c: $c)
285277
}
286278
''', [
287-
undefined_var_by_op('a', 9, 19, 'Foo', 2, 7),
288-
undefined_var_by_op('c', 14, 19, 'Foo', 2, 7),
289-
undefined_var_by_op('a', 11, 19, 'Foo', 2, 7),
290-
undefined_var_by_op('b', 9, 26, 'Bar', 5, 7),
291-
undefined_var_by_op('c', 14, 19, 'Bar', 5, 7),
292-
undefined_var_by_op('b', 11, 26, 'Bar', 5, 7),
279+
undefined_var('a', 9, 19, 'Foo', 2, 7),
280+
undefined_var('a', 11, 19, 'Foo', 2, 7),
281+
undefined_var('c', 14, 19, 'Foo', 2, 7),
282+
undefined_var('b', 9, 26, 'Bar', 5, 7),
283+
undefined_var('b', 11, 26, 'Bar', 5, 7),
284+
undefined_var('c', 14, 19, 'Bar', 5, 7),
293285
])

0 commit comments

Comments
 (0)