Skip to content

Commit d93c1fc

Browse files
fix(compiler): fix validation for fragment spread of interface without implementers (#896)
* fix(compiler): add failing test for spread of empty interface `Intf` has no implementations. As written in the spec, doing a `... on Intf` fragment spread should never work, as the set of possible types is empty and can never intersect with the parent type. However, implementations like graphql-js and graphql-go have an early check, accepting the fragment if the type condition is equal to the parent type. This tests reproduces that. We may want to align with graphql-js and graphql-go rather than the spec here for compatibility? Though it's not something that's likely to happen in the real world. Ref graphql/graphql-spec#1109 * fix(compiler): always accept spreading ...Ty when parent type is Ty
1 parent 55002fd commit d93c1fc

7 files changed

+289
-24
lines changed

crates/apollo-compiler/src/validation/fragment.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ fn validate_fragment_spread_type(
5757
selection: &executable::Selection,
5858
context: OperationValidationContext<'_>,
5959
) {
60+
// Treat a spread that's just literally on the parent type as always valid:
61+
// by spec text, it shouldn't be, but graphql-{js,java,go} and others all do this.
62+
// See https://github.com/graphql/graphql-spec/issues/1109
63+
if type_condition == against_type {
64+
return;
65+
}
66+
6067
// Another diagnostic will be raised if the type condition was wrong.
6168
// We reduce noise by silencing other issues with the fragment.
6269
let Some(type_condition_definition) = schema.types.get(type_condition) else {

crates/apollo-compiler/test_data/diagnostics/0091_recursive_interface_definition.graphql

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ interface A implements B {
44
interface B implements A {
55
name: String
66
}
7-
fragment recursive on A {
8-
name
7+
type Impl implements A & B {
8+
name: String
99
}
10-
1110
type Query {
1211
get: A
1312
}
13+
14+
fragment recursive on A {
15+
name
16+
}
17+
1418
query {
1519
get { ...recursive }
1620
}

crates/apollo-compiler/test_data/diagnostics/0091_recursive_interface_definition.txt

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,4 @@ Error: interface `B` declares that it implements `A`, but to do so it must also
2323
│ │
2424
│ ╰─────── B must also be implemented here
2525
───╯
26-
Error: fragment `recursive` with type condition `A` cannot be applied to `A`
27-
╭─[0091_recursive_interface_definition.graphql:15:9]
28-
29-
1 │ ╭───▶ interface A implements B {
30-
┆ ┆
31-
3 │ ├───▶ }
32-
│ │
33-
│ ╰───────── type condition `A` is not assignable to this type
34-
35-
7 │ ╭─▶ fragment recursive on A {
36-
┆ ┆
37-
9 │ ├─▶ }
38-
│ │
39-
│ ╰─────── fragment declared with type condition `A` here
40-
41-
15 │ get { ...recursive }
42-
│ ──────┬─────
43-
│ ╰─────── fragment `recursive` cannot be applied
44-
────╯
4526

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
type Query {
2+
intf: Intf
3+
}
4+
interface Intf {
5+
field: Int
6+
}
7+
8+
query SelectDirectly {
9+
intf { field }
10+
}
11+
12+
query UsingInlineFragment {
13+
intf {
14+
... on Intf { field }
15+
}
16+
}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
Schema {
2+
sources: {
3+
1: SourceFile {
4+
path: "built_in.graphql",
5+
source_text: include_str!("built_in.graphql"),
6+
},
7+
45: SourceFile {
8+
path: "0116_interface_without_implementations.graphql",
9+
source_text: "type Query {\n intf: Intf\n}\ninterface Intf {\n field: Int\n}\n\nquery SelectDirectly {\n intf { field }\n}\n\nquery UsingInlineFragment {\n intf {\n ... on Intf { field }\n }\n}\n",
10+
},
11+
},
12+
schema_definition: SchemaDefinition {
13+
description: None,
14+
directives: [],
15+
query: Some(
16+
ComponentName {
17+
origin: Definition,
18+
name: "Query",
19+
},
20+
),
21+
mutation: None,
22+
subscription: None,
23+
},
24+
directive_definitions: {
25+
"skip": built_in_directive!("skip"),
26+
"include": built_in_directive!("include"),
27+
"deprecated": built_in_directive!("deprecated"),
28+
"specifiedBy": built_in_directive!("specifiedBy"),
29+
},
30+
types: {
31+
"__Schema": built_in_type!("__Schema"),
32+
"__Type": built_in_type!("__Type"),
33+
"__TypeKind": built_in_type!("__TypeKind"),
34+
"__Field": built_in_type!("__Field"),
35+
"__InputValue": built_in_type!("__InputValue"),
36+
"__EnumValue": built_in_type!("__EnumValue"),
37+
"__Directive": built_in_type!("__Directive"),
38+
"__DirectiveLocation": built_in_type!("__DirectiveLocation"),
39+
"Int": built_in_type!("Int"),
40+
"Float": built_in_type!("Float"),
41+
"String": built_in_type!("String"),
42+
"Boolean": built_in_type!("Boolean"),
43+
"ID": built_in_type!("ID"),
44+
"Query": Object(
45+
0..27 @45 ObjectType {
46+
description: None,
47+
name: "Query",
48+
implements_interfaces: {},
49+
directives: [],
50+
fields: {
51+
"intf": Component {
52+
origin: Definition,
53+
node: 15..25 @45 FieldDefinition {
54+
description: None,
55+
name: "intf",
56+
arguments: [],
57+
ty: Named(
58+
"Intf",
59+
),
60+
directives: [],
61+
},
62+
},
63+
},
64+
},
65+
),
66+
"Intf": Interface(
67+
28..59 @45 InterfaceType {
68+
description: None,
69+
name: "Intf",
70+
implements_interfaces: {},
71+
directives: [],
72+
fields: {
73+
"field": Component {
74+
origin: Definition,
75+
node: 47..57 @45 FieldDefinition {
76+
description: None,
77+
name: "field",
78+
arguments: [],
79+
ty: Named(
80+
"Int",
81+
),
82+
directives: [],
83+
},
84+
},
85+
},
86+
},
87+
),
88+
},
89+
}
90+
ExecutableDocument {
91+
sources: {
92+
1: SourceFile {
93+
path: "built_in.graphql",
94+
source_text: include_str!("built_in.graphql"),
95+
},
96+
45: SourceFile {
97+
path: "0116_interface_without_implementations.graphql",
98+
source_text: "type Query {\n intf: Intf\n}\ninterface Intf {\n field: Int\n}\n\nquery SelectDirectly {\n intf { field }\n}\n\nquery UsingInlineFragment {\n intf {\n ... on Intf { field }\n }\n}\n",
99+
},
100+
},
101+
operations: OperationMap {
102+
anonymous: None,
103+
named: {
104+
"SelectDirectly": 61..102 @45 Operation {
105+
operation_type: Query,
106+
name: Some(
107+
"SelectDirectly",
108+
),
109+
variables: [],
110+
directives: [],
111+
selection_set: SelectionSet {
112+
ty: "Query",
113+
selections: [
114+
Field(
115+
86..100 @45 Field {
116+
definition: 15..25 @45 FieldDefinition {
117+
description: None,
118+
name: "intf",
119+
arguments: [],
120+
ty: Named(
121+
"Intf",
122+
),
123+
directives: [],
124+
},
125+
alias: None,
126+
name: "intf",
127+
arguments: [],
128+
directives: [],
129+
selection_set: SelectionSet {
130+
ty: "Intf",
131+
selections: [
132+
Field(
133+
93..98 @45 Field {
134+
definition: 47..57 @45 FieldDefinition {
135+
description: None,
136+
name: "field",
137+
arguments: [],
138+
ty: Named(
139+
"Int",
140+
),
141+
directives: [],
142+
},
143+
alias: None,
144+
name: "field",
145+
arguments: [],
146+
directives: [],
147+
selection_set: SelectionSet {
148+
ty: "Int",
149+
selections: [],
150+
},
151+
},
152+
),
153+
],
154+
},
155+
},
156+
),
157+
],
158+
},
159+
},
160+
"UsingInlineFragment": 104..172 @45 Operation {
161+
operation_type: Query,
162+
name: Some(
163+
"UsingInlineFragment",
164+
),
165+
variables: [],
166+
directives: [],
167+
selection_set: SelectionSet {
168+
ty: "Query",
169+
selections: [
170+
Field(
171+
134..170 @45 Field {
172+
definition: 15..25 @45 FieldDefinition {
173+
description: None,
174+
name: "intf",
175+
arguments: [],
176+
ty: Named(
177+
"Intf",
178+
),
179+
directives: [],
180+
},
181+
alias: None,
182+
name: "intf",
183+
arguments: [],
184+
directives: [],
185+
selection_set: SelectionSet {
186+
ty: "Intf",
187+
selections: [
188+
InlineFragment(
189+
145..166 @45 InlineFragment {
190+
type_condition: Some(
191+
"Intf",
192+
),
193+
directives: [],
194+
selection_set: SelectionSet {
195+
ty: "Intf",
196+
selections: [
197+
Field(
198+
159..164 @45 Field {
199+
definition: 47..57 @45 FieldDefinition {
200+
description: None,
201+
name: "field",
202+
arguments: [],
203+
ty: Named(
204+
"Int",
205+
),
206+
directives: [],
207+
},
208+
alias: None,
209+
name: "field",
210+
arguments: [],
211+
directives: [],
212+
selection_set: SelectionSet {
213+
ty: "Int",
214+
selections: [],
215+
},
216+
},
217+
),
218+
],
219+
},
220+
},
221+
),
222+
],
223+
},
224+
},
225+
),
226+
],
227+
},
228+
},
229+
},
230+
},
231+
fragments: {},
232+
}

crates/apollo-compiler/test_data/serializer/diagnostics/0091_recursive_interface_definition.graphql

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@ interface B implements A {
66
name: String
77
}
88

9-
fragment recursive on A {
10-
name
9+
type Impl implements A & B {
10+
name: String
1111
}
1212

1313
type Query {
1414
get: A
1515
}
1616

17+
fragment recursive on A {
18+
name
19+
}
20+
1721
query {
1822
get {
1923
...recursive
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
type Query {
2+
intf: Intf
3+
}
4+
5+
interface Intf {
6+
field: Int
7+
}
8+
9+
query SelectDirectly {
10+
intf {
11+
field
12+
}
13+
}
14+
15+
query UsingInlineFragment {
16+
intf {
17+
... on Intf {
18+
field
19+
}
20+
}
21+
}

0 commit comments

Comments
 (0)