Skip to content

Commit be05925

Browse files
committed
Merge branch 'guy_possible_fragment_spreads'
Conflicts: .gitignore graphql/core/validation/rules.py
2 parents 526fb60 + 8628cba commit be05925

File tree

3 files changed

+246
-9
lines changed

3 files changed

+246
-9
lines changed

graphql/core/validation/rules.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
from ..utils import type_from_ast, is_valid_literal_value
22
from ..error import GraphQLError
3-
from ..type.definition import is_composite_type, is_input_type, is_leaf_type, GraphQLNonNull, GraphQLList
3+
from ..type.definition import (
4+
is_composite_type,
5+
is_input_type,
6+
is_leaf_type,
7+
GraphQLNonNull,
8+
GraphQLList,
9+
GraphQLObjectType,
10+
GraphQLInterfaceType,
11+
GraphQLUnionType,
12+
)
413
from ..language import ast
514
from ..language.visitor import Visitor, visit
615
from ..language.printer import print_ast
@@ -245,7 +254,52 @@ def unused_fragment_message(fragment_name):
245254

246255

247256
class PossibleFragmentSpreads(ValidationRule):
248-
pass
257+
def enter_InlineFragment(self, node, *args):
258+
frag_type = self.context.get_type()
259+
parent_type = self.context.get_parent_type()
260+
if frag_type and parent_type and not self.do_types_overlap(frag_type, parent_type):
261+
return GraphQLError(
262+
self.type_incompatible_anon_spread_message(parent_type, frag_type),
263+
[node]
264+
)
265+
266+
def enter_FragmentSpread(self, node, *args):
267+
frag_name = node.name.value
268+
frag_type = self.get_fragment_type(self.context, frag_name)
269+
parent_type = self.context.get_parent_type()
270+
if frag_type and parent_type and not self.do_types_overlap(frag_type, parent_type):
271+
return GraphQLError(
272+
self.type_incompatible_spread_message(frag_name, parent_type, frag_type),
273+
[node]
274+
)
275+
276+
@staticmethod
277+
def get_fragment_type(context, name):
278+
frag = context.get_fragment(name)
279+
return frag and type_from_ast(context.get_schema(), frag.type_condition)
280+
281+
@staticmethod
282+
def do_types_overlap(t1, t2):
283+
if t1 == t2:
284+
return True
285+
if isinstance(t1, GraphQLObjectType):
286+
if isinstance(t2, GraphQLObjectType):
287+
return False
288+
return t1 in t2.get_possible_types()
289+
if isinstance(t1, GraphQLInterfaceType) or isinstance(t1, GraphQLUnionType):
290+
if isinstance(t2, GraphQLObjectType):
291+
return t2 in t1.get_possible_types()
292+
293+
t1_type_names = {possible_type.name: possible_type for possible_type in t1.get_possible_types()}
294+
return any(t.name in t1_type_names for t in t2.get_possible_types())
295+
296+
@staticmethod
297+
def type_incompatible_spread_message(frag_name, parent_type, frag_type):
298+
return 'Fragment {} cannot be spread here as objects of type {} can never be of type {}'.format(frag_name, parent_type, frag_type)
299+
300+
@staticmethod
301+
def type_incompatible_anon_spread_message(parent_type, frag_type):
302+
return 'Fragment cannot be spread here as objects of type {} can never be of type {}'.format(parent_type, frag_type)
249303

250304

251305
class NoFragmentCycles(ValidationRule):
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
from graphql.core.language.location import SourceLocation
2+
from graphql.core.validation.rules import PossibleFragmentSpreads
3+
from utils import expect_passes_rule, expect_fails_rule
4+
5+
def error(frag_name, parent_type, frag_type, line, column):
6+
return {
7+
'message': PossibleFragmentSpreads.type_incompatible_spread_message(frag_name, parent_type, frag_type),
8+
'locations': [SourceLocation(line, column)]
9+
}
10+
11+
12+
def error_anon(parent_type, frag_type, line, column):
13+
return {
14+
'message': PossibleFragmentSpreads.type_incompatible_anon_spread_message(parent_type, frag_type),
15+
'locations': [SourceLocation(line, column)]
16+
}
17+
18+
def test_same_object():
19+
expect_passes_rule(PossibleFragmentSpreads, '''
20+
fragment objectWithinObject on Dog { ...dogFragment }
21+
fragment dogFragment on Dog { barkVolume }
22+
''')
23+
24+
def test_same_object_inline_frag():
25+
expect_passes_rule(PossibleFragmentSpreads, '''
26+
fragment objectWithinObjectAnon on Dog { ... on Dog { barkVolume } }
27+
''')
28+
29+
def test_object_into_implemented_interface():
30+
expect_passes_rule(PossibleFragmentSpreads, '''
31+
fragment objectWithinInterface on Pet { ...dogFragment }
32+
fragment dogFragment on Dog { barkVolume }
33+
''')
34+
35+
def test_object_into_containing_union():
36+
expect_passes_rule(PossibleFragmentSpreads, '''
37+
fragment objectWithinUnion on CatOrDog { ...dogFragment }
38+
fragment dogFragment on Dog { barkVolume }
39+
''')
40+
41+
def test_union_into_contained_object():
42+
expect_passes_rule(PossibleFragmentSpreads, '''
43+
fragment unionWithinObject on Dog { ...catOrDogFragment }
44+
fragment catOrDogFragment on CatOrDog { __typename }
45+
''')
46+
47+
def test_union_into_overlapping_interface():
48+
expect_passes_rule(PossibleFragmentSpreads, '''
49+
fragment unionWithinInterface on Pet { ...catOrDogFragment }
50+
fragment catOrDogFragment on CatOrDog { __typename }
51+
''')
52+
53+
def test_union_into_overlapping_union():
54+
expect_passes_rule(PossibleFragmentSpreads, '''
55+
fragment unionWithinUnion on DogOrHuman { ...catOrDogFragment }
56+
fragment catOrDogFragment on CatOrDog { __typename }
57+
''')
58+
59+
def test_interface_into_implemented_object():
60+
expect_passes_rule(PossibleFragmentSpreads, '''
61+
fragment interfaceWithinObject on Dog { ...petFragment }
62+
fragment petFragment on Pet { name }
63+
''')
64+
65+
def test_interface_into_overlapping_interface():
66+
expect_passes_rule(PossibleFragmentSpreads, '''
67+
fragment interfaceWithinInterface on Pet { ...beingFragment }
68+
fragment beingFragment on Being { name }
69+
''')
70+
71+
def test_interface_into_overlapping_interface_in_inline_fragment():
72+
expect_passes_rule(PossibleFragmentSpreads, '''
73+
fragment interfaceWithinInterface on Pet { ... on Being { name } }
74+
''')
75+
76+
def test_interface_into_overlapping_union():
77+
expect_passes_rule(PossibleFragmentSpreads, '''
78+
fragment interfaceWithinUnion on CatOrDog { ...petFragment }
79+
fragment petFragment on Pet { name }
80+
''')
81+
82+
def test_different_object_into_object():
83+
expect_fails_rule(PossibleFragmentSpreads, '''
84+
fragment invalidObjectWithinObject on Cat { ...dogFragment }
85+
fragment dogFragment on Dog { barkVolume }
86+
''', [error('dogFragment', 'Cat', 'Dog', 2, 51)])
87+
88+
def test_different_object_into_object_in_inline_fragment():
89+
expect_fails_rule(PossibleFragmentSpreads, '''
90+
fragment invalidObjectWithinObjectAnon on Cat {
91+
... on Dog { barkVolume }
92+
}
93+
''', [error_anon('Cat', 'Dog', 3, 9)])
94+
95+
def test_object_into_not_implementing_interface():
96+
expect_fails_rule(PossibleFragmentSpreads, '''
97+
fragment invalidObjectWithinInterface on Pet { ...humanFragment }
98+
fragment humanFragment on Human { pets { name } }
99+
''', [error('humanFragment', 'Pet', 'Human', 2, 54)])
100+
101+
def test_object_into_not_containing_union():
102+
expect_fails_rule(PossibleFragmentSpreads, '''
103+
fragment invalidObjectWithinUnion on CatOrDog { ...humanFragment }
104+
fragment humanFragment on Human { pets { name } }
105+
''', [error('humanFragment', 'CatOrDog', 'Human', 2, 55)])
106+
107+
def test_union_into_not_contained_object():
108+
expect_fails_rule(PossibleFragmentSpreads, '''
109+
fragment invalidUnionWithinObject on Human { ...catOrDogFragment }
110+
fragment catOrDogFragment on CatOrDog { __typename }
111+
''', [error('catOrDogFragment', 'Human', 'CatOrDog', 2, 52)])
112+
113+
def test_union_into_non_overlapping_interface():
114+
expect_fails_rule(PossibleFragmentSpreads, '''
115+
fragment invalidUnionWithinInterface on Pet { ...humanOrAlienFragment }
116+
fragment humanOrAlienFragment on HumanOrAlien { __typename }
117+
''', [error('humanOrAlienFragment', 'Pet', 'HumanOrAlien', 2, 53)])
118+
119+
def test_union_into_non_overlapping_union():
120+
expect_fails_rule(PossibleFragmentSpreads, '''
121+
fragment invalidUnionWithinUnion on CatOrDog { ...humanOrAlienFragment }
122+
fragment humanOrAlienFragment on HumanOrAlien { __typename }
123+
''', [error('humanOrAlienFragment', 'CatOrDog', 'HumanOrAlien', 2, 54)])
124+
125+
def test_interface_into_non_implementing_object():
126+
expect_fails_rule(PossibleFragmentSpreads, '''
127+
fragment invalidInterfaceWithinObject on Cat { ...intelligentFragment }
128+
fragment intelligentFragment on Intelligent { iq }
129+
''', [error('intelligentFragment', 'Cat', 'Intelligent', 2, 54)])
130+
131+
def test_interface_into_non_overlapping_interface():
132+
expect_fails_rule(PossibleFragmentSpreads, '''
133+
fragment invalidInterfaceWithinInterface on Pet {
134+
...intelligentFragment
135+
}
136+
fragment intelligentFragment on Intelligent { iq }
137+
''', [error('intelligentFragment', 'Pet', 'Intelligent', 3, 9)])
138+
139+
def test_interface_into_non_overlapping_interface_in_inline_fragment():
140+
expect_fails_rule(PossibleFragmentSpreads, '''
141+
fragment invalidInterfaceWithinInterfaceAnon on Pet {
142+
...on Intelligent { iq }
143+
}
144+
''', [error_anon('Pet', 'Intelligent', 3, 9)])
145+
146+
def test_interface_into_non_overlapping_union():
147+
expect_fails_rule(PossibleFragmentSpreads, '''
148+
fragment invalidInterfaceWithinUnion on HumanOrAlien { ...petFragment }
149+
fragment petFragment on Pet { name }
150+
''', [error('petFragment', 'HumanOrAlien', 'Pet', 2, 62)])

tests/core_validation/utils.py

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@
1919
GraphQLList)
2020
from graphql.core.error import format_error
2121

22+
Being = GraphQLInterfaceType('Being', {
23+
'name': GraphQLField(GraphQLString, {
24+
'surname': GraphQLArgument(GraphQLBoolean),
25+
})
26+
})
27+
2228
Pet = GraphQLInterfaceType('Pet', {
2329
'name': GraphQLField(GraphQLString, {
2430
'surname': GraphQLArgument(GraphQLBoolean),
@@ -40,21 +46,47 @@
4046
'doesKnowCommand': GraphQLField(GraphQLBoolean, {
4147
'dogCommand': GraphQLArgument(DogCommand)
4248
})
43-
}, interfaces=[Pet])
49+
}, interfaces=[Being, Pet])
4450

4551
Cat = GraphQLObjectType('Cat', lambda: {
4652
'furColor': GraphQLField(FurColor)
47-
}, interfaces=[Pet])
53+
}, interfaces=[Being, Pet])
4854

4955
CatOrDog = GraphQLUnionType('CatOrDog', [Dog, Cat])
5056

51-
Human = GraphQLObjectType('Human', {
52-
'name': GraphQLField(GraphQLString, {
53-
'surname': GraphQLArgument(GraphQLBoolean),
54-
}),
55-
'pets': GraphQLField(GraphQLList(Pet)),
57+
Intelligent = GraphQLInterfaceType('Intelligent', {
58+
'iq': GraphQLField(GraphQLInt),
5659
})
5760

61+
Human = GraphQLObjectType(
62+
name='Human',
63+
interfaces=[Being, Intelligent],
64+
fields={
65+
'name': GraphQLField(GraphQLString, {
66+
'surname': GraphQLArgument(GraphQLBoolean),
67+
}),
68+
'pets': GraphQLField(GraphQLList(Pet)),
69+
'iq': GraphQLField(GraphQLInt),
70+
},
71+
)
72+
73+
Alien = GraphQLObjectType(
74+
name='Alien',
75+
is_type_of=lambda *args: True,
76+
interfaces=[Being, Intelligent],
77+
fields={
78+
'iq': GraphQLField(GraphQLInt),
79+
'name': GraphQLField(GraphQLString, {
80+
'surname': GraphQLField(GraphQLBoolean),
81+
}),
82+
'numEyes': GraphQLField(GraphQLInt),
83+
},
84+
)
85+
86+
DogOrHuman = GraphQLUnionType('DogOrHuman', [Dog, Human])
87+
88+
HumanOrAlien = GraphQLUnionType('HumanOrAlien', [Human, Alien])
89+
5890
FurColor = GraphQLEnumType('FurColor', {
5991
'BROWN': GraphQLEnumValue(0),
6092
'BLACK': GraphQLEnumValue(1),
@@ -121,6 +153,7 @@
121153
'dog': GraphQLField(Dog),
122154
'pet': GraphQLField(Pet),
123155
'catOrDog': GraphQLField(CatOrDog),
156+
'humanOrAlien': GraphQLField(HumanOrAlien),
124157
'complicatedArgs': GraphQLField(ComplicatedArgs),
125158
})
126159

0 commit comments

Comments
 (0)