Skip to content

Commit b6db04c

Browse files
committed
[validator] Add suggested types to incorrect field message
Related GraphQL-js commit: graphql/graphql-js@7861b22
1 parent 4bc24e2 commit b6db04c

File tree

4 files changed

+101
-22
lines changed

4 files changed

+101
-22
lines changed

graphql/core/validation/rules/fields_on_correct_type.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
from collections import Counter
2+
13
from ...error import GraphQLError
4+
from ...type.definition import is_abstract_type, GraphQLObjectType
25
from .base import ValidationRule
36

47

@@ -11,11 +14,50 @@ def enter_Field(self, node, key, parent, path, ancestors):
1114

1215
field_def = self.context.get_field_def()
1316
if not field_def:
17+
suggested_types = []
18+
if is_abstract_type(type):
19+
suggested_types = get_sibling_interfaces_including_field(type, node.name.value)
20+
suggested_types += get_implementations_including_field(type, node.name.value)
1421
self.context.report_error(GraphQLError(
15-
self.undefined_field_message(node.name.value, type.name),
22+
self.undefined_field_message(node.name.value, type.name, suggested_types),
1623
[node]
1724
))
1825

1926
@staticmethod
20-
def undefined_field_message(field_name, type):
21-
return 'Cannot query field "{}" on "{}".'.format(field_name, type)
27+
def undefined_field_message(field_name, type, suggested_types):
28+
message = 'Cannot query field "{}" on type "{}".'.format(field_name, type)
29+
MAX_LENGTH = 5
30+
if suggested_types:
31+
suggestions = ', '.join(['"{}"'.format(t) for t in suggested_types[:MAX_LENGTH]])
32+
l_suggested_types = len(suggested_types)
33+
if l_suggested_types > MAX_LENGTH:
34+
suggestions += ", and {} other types".format(l_suggested_types-MAX_LENGTH)
35+
message += " However, this field exists on {}.".format(suggestions)
36+
message += " Perhaps you meant to use an inline fragment?"
37+
return message
38+
39+
40+
def get_implementations_including_field(type, field_name):
41+
'''Return implementations of `type` that include `fieldName` as a valid field.'''
42+
return sorted(map(lambda t: t.name, filter(lambda t: field_name in t.get_fields(), type.get_possible_types())))
43+
44+
45+
def get_sibling_interfaces_including_field(type, field_name):
46+
'''Go through all of the implementations of type, and find other interaces
47+
that they implement. If those interfaces include `field` as a valid field,
48+
return them, sorted by how often the implementations include the other
49+
interface.'''
50+
51+
implementing_objects = filter(lambda t: isinstance(t, GraphQLObjectType), type.get_possible_types())
52+
suggested_interfaces = Counter()
53+
for t in implementing_objects:
54+
print t.name
55+
for i in t.get_interfaces():
56+
print i.name
57+
if field_name not in i.get_fields():
58+
break
59+
suggested_interfaces[i.name] += 1
60+
most_common = suggested_interfaces.most_common()
61+
if not most_common:
62+
return []
63+
return list(zip(*most_common)[0])

tests/core_validation/test_fields_on_correct_type.py

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

55

6-
def undefined_field(field, type, line, column):
6+
def undefined_field(field, type, suggestions, line, column):
77
return {
8-
'message': FieldsOnCorrectType.undefined_field_message(field, type),
8+
'message': FieldsOnCorrectType.undefined_field_message(field, type, suggestions),
99
'locations': [SourceLocation(line, column)]
1010
}
1111

@@ -71,8 +71,8 @@ def test_reports_errors_when_type_is_known_again():
7171
}
7272
},
7373
''', [
74-
undefined_field('unknown_pet_field', 'Pet', 3, 9),
75-
undefined_field('unknown_cat_field', 'Cat', 5, 13)
74+
undefined_field('unknown_pet_field', 'Pet', [], 3, 9),
75+
undefined_field('unknown_cat_field', 'Cat', [], 5, 13)
7676
])
7777

7878

@@ -82,7 +82,7 @@ def test_field_not_defined_on_fragment():
8282
meowVolume
8383
}
8484
''', [
85-
undefined_field('meowVolume', 'Dog', 3, 9)
85+
undefined_field('meowVolume', 'Dog', [], 3, 9)
8686
])
8787

8888

@@ -94,7 +94,7 @@ def test_ignores_deeply_unknown_field():
9494
}
9595
}
9696
''', [
97-
undefined_field('unknown_field', 'Dog', 3, 9)
97+
undefined_field('unknown_field', 'Dog', [], 3, 9)
9898
])
9999

100100

@@ -106,7 +106,7 @@ def test_sub_field_not_defined():
106106
}
107107
}
108108
''', [
109-
undefined_field('unknown_field', 'Pet', 4, 11)
109+
undefined_field('unknown_field', 'Pet', [], 4, 11)
110110
])
111111

112112

@@ -118,7 +118,7 @@ def test_field_not_defined_on_inline_fragment():
118118
}
119119
}
120120
''', [
121-
undefined_field('meowVolume', 'Dog', 4, 11)
121+
undefined_field('meowVolume', 'Dog', [], 4, 11)
122122
])
123123

124124

@@ -128,7 +128,7 @@ def test_aliased_field_target_not_defined():
128128
volume : mooVolume
129129
}
130130
''', [
131-
undefined_field('mooVolume', 'Dog', 3, 9)
131+
undefined_field('mooVolume', 'Dog', [], 3, 9)
132132
])
133133

134134

@@ -138,7 +138,7 @@ def test_aliased_lying_field_target_not_defined():
138138
barkVolume : kawVolume
139139
}
140140
''', [
141-
undefined_field('kawVolume', 'Dog', 3, 9)
141+
undefined_field('kawVolume', 'Dog', [], 3, 9)
142142
])
143143

144144

@@ -148,7 +148,7 @@ def test_not_defined_on_interface():
148148
tailLength
149149
}
150150
''', [
151-
undefined_field('tailLength', 'Pet', 3, 9)
151+
undefined_field('tailLength', 'Pet', [], 3, 9)
152152
])
153153

154154

@@ -158,7 +158,7 @@ def test_defined_on_implementors_but_not_on_interface():
158158
nickname
159159
}
160160
''', [
161-
undefined_field('nickname', 'Pet', 3, 9)
161+
undefined_field('nickname', 'Pet', ['Cat', 'Dog'], 3, 9)
162162
])
163163

164164

@@ -176,7 +176,7 @@ def test_direct_field_selection_on_union():
176176
directField
177177
}
178178
''', [
179-
undefined_field('directField', 'CatOrDog', 3, 9)
179+
undefined_field('directField', 'CatOrDog', [], 3, 9)
180180
])
181181

182182

@@ -186,7 +186,13 @@ def test_defined_on_implementors_queried_on_union():
186186
name
187187
}
188188
''', [
189-
undefined_field('name', 'CatOrDog', 3, 9)
189+
undefined_field(
190+
'name',
191+
'CatOrDog',
192+
['Being', 'Pet', 'Canine', 'Cat', 'Dog'],
193+
3,
194+
9
195+
)
190196
])
191197

192198

@@ -201,3 +207,27 @@ def test_valid_field_in_inline_fragment():
201207
}
202208
}
203209
''')
210+
211+
212+
def test_fields_correct_type_no_suggestion():
213+
message = FieldsOnCorrectType.undefined_field_message('T', 'f', [])
214+
assert message == 'Cannot query field "T" on type "f".'
215+
216+
217+
def test_fields_correct_type_no_small_number_suggestions():
218+
message = FieldsOnCorrectType.undefined_field_message('T', 'f', ['A', 'B'])
219+
assert message == (
220+
'Cannot query field "T" on type "f". ' +
221+
'However, this field exists on "A", "B". ' +
222+
'Perhaps you meant to use an inline fragment?'
223+
)
224+
225+
226+
def test_fields_correct_type_lot_suggestions():
227+
message = FieldsOnCorrectType.undefined_field_message('T', 'f', ['A', 'B', 'C', 'D', 'E', 'F'])
228+
assert message == (
229+
'Cannot query field "T" on type "f". ' +
230+
'However, this field exists on "A", "B", "C", "D", "E", ' +
231+
'and 1 other types. '+
232+
'Perhaps you meant to use an inline fragment?'
233+
)

tests/core_validation/test_validation.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,6 @@ def test_validates_using_a_custom_type_info():
4949
)
5050

5151
assert len(errors) == 3
52-
assert errors[0].message == 'Cannot query field "catOrDog" on "QueryRoot".'
53-
assert errors[1].message == 'Cannot query field "furColor" on "Cat".'
54-
assert errors[2].message == 'Cannot query field "isHousetrained" on "Dog".'
52+
assert errors[0].message == 'Cannot query field "catOrDog" on type "QueryRoot".'
53+
assert errors[1].message == 'Cannot query field "furColor" on type "Cat".'
54+
assert errors[2].message == 'Cannot query field "isHousetrained" on type "Dog".'

tests/core_validation/utils.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@
2424
}),
2525
})
2626

27+
Canine = GraphQLInterfaceType('Canine', {
28+
'name': GraphQLField(GraphQLString, {
29+
'surname': GraphQLArgument(GraphQLBoolean),
30+
}),
31+
})
32+
2733
DogCommand = GraphQLEnumType('DogCommand', {
2834
'SIT': GraphQLEnumValue(0),
2935
'HEEL': GraphQLEnumValue(1),
@@ -53,13 +59,14 @@
5359
'y': GraphQLArgument(GraphQLInt)
5460
}
5561
)
56-
}, interfaces=[Being, Pet], is_type_of=lambda: None)
62+
}, interfaces=[Being, Pet, Canine], is_type_of=lambda: None)
5763

5864
Cat = GraphQLObjectType('Cat', lambda: {
5965
'furColor': GraphQLField(FurColor),
6066
'name': GraphQLField(GraphQLString, {
6167
'surname': GraphQLArgument(GraphQLBoolean),
62-
})
68+
}),
69+
'nickname': GraphQLField(GraphQLString),
6370
}, interfaces=[Being, Pet], is_type_of=lambda: None)
6471

6572
CatOrDog = GraphQLUnionType('CatOrDog', [Dog, Cat])

0 commit comments

Comments
 (0)