Skip to content

Commit e0d4366

Browse files
committed
Detect Reference Loops in defined-types-are-used
In defined-types-are-used - Add support for detecting reference loops (fixes #264). - Add support for custom schema roots.
1 parent ae0ce9b commit e0d4366

File tree

2 files changed

+289
-35
lines changed

2 files changed

+289
-35
lines changed

src/rules/defined_types_are_used.js

Lines changed: 115 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,139 @@
1+
import { Kind } from 'graphql';
12
import { ValidationError } from '../validation_error';
23

4+
// Removes any NonNull or List wrapping
5+
function unwrapType(typeAst) {
6+
let type = typeAst;
7+
while (type.type) {
8+
type = type.type;
9+
}
10+
return type;
11+
}
12+
313
export function DefinedTypesAreUsed(context) {
4-
var ignoredTypes = ['Query', 'Mutation', 'Subscription'];
5-
var definedTypes = [];
6-
var referencedTypes = new Set();
7-
8-
var recordDefinedType = (node) => {
9-
if (ignoredTypes.indexOf(node.name.value) == -1) {
10-
definedTypes.push(node);
11-
}
14+
const typeMap = new Map();
15+
const topLevelTypes = new Set();
16+
17+
let seenSchemaDefinition = false;
18+
let currentType = null;
19+
20+
const visitType = {
21+
enter(node) {
22+
currentType = node.name.value;
23+
24+
if (!typeMap.has(currentType)) {
25+
typeMap.set(currentType, {
26+
node,
27+
interfaces: new Set(),
28+
referencedTypes: new Set(),
29+
});
30+
}
31+
32+
if (node.interfaces) {
33+
for (const interfaceType of node.interfaces) {
34+
typeMap.get(currentType).interfaces.add(interfaceType.name.value);
35+
}
36+
}
37+
38+
if (
39+
node.kind === Kind.UNION_TYPE_DEFINITION ||
40+
node.kind === Kind.UNION_TYPE_EXTENSION
41+
) {
42+
for (const type of node.types) {
43+
typeMap.get(node.name.value).referencedTypes.add(type.name.value);
44+
}
45+
}
46+
},
47+
leave() {
48+
currentType = null;
49+
},
1250
};
1351

1452
return {
15-
ScalarTypeDefinition: recordDefinedType,
16-
ObjectTypeDefinition: recordDefinedType,
17-
InterfaceTypeDefinition: recordDefinedType,
18-
UnionTypeDefinition: recordDefinedType,
19-
EnumTypeDefinition: recordDefinedType,
20-
InputObjectTypeDefinition: recordDefinedType,
21-
22-
NamedType: (node, key, parent, path, ancestors) => {
23-
referencedTypes.add(node.name.value);
53+
SchemaDefinition() {
54+
seenSchemaDefinition = true;
55+
},
56+
ScalarTypeDefinition: visitType,
57+
ObjectTypeDefinition: visitType,
58+
ObjectTypeExtension: visitType,
59+
InterfaceTypeDefinition: visitType,
60+
InterfaceTypeExtension: visitType,
61+
EnumTypeDefinition: visitType,
62+
EnumTypeExtension: visitType,
63+
InputObjectTypeDefinition: visitType,
64+
InputObjectExtension: visitType,
65+
UnionTypeDefinition: visitType,
66+
UnionTypeExtension: visitType,
67+
68+
OperationTypeDefinition(node) {
69+
// Used as a root type in the schema definition
70+
topLevelTypes.add(node.type.name.value);
71+
},
72+
73+
InputValueDefinition(node) {
74+
if (currentType) {
75+
// Used as an input
76+
typeMap
77+
.get(currentType)
78+
.referencedTypes.add(unwrapType(node.type).name.value);
79+
} else {
80+
// Used in a directive's arguments - this doesn't check whether the directive itself is used
81+
topLevelTypes.add(unwrapType(node.type).name.value);
82+
}
83+
},
84+
85+
FieldDefinition(node) {
86+
// Used as the result of a field
87+
typeMap
88+
.get(currentType)
89+
.referencedTypes.add(unwrapType(node.type).name.value);
2490
},
2591

2692
Document: {
27-
leave: (node) => {
28-
definedTypes.forEach((node) => {
29-
if (node.kind == 'ObjectTypeDefinition') {
30-
let implementedInterfaces = node.interfaces.map((node) => {
31-
return node.name.value;
32-
});
33-
34-
let anyReferencedInterfaces = implementedInterfaces.some(
35-
(interfaceName) => {
36-
return referencedTypes.has(interfaceName);
37-
}
38-
);
93+
leave() {
94+
if (!seenSchemaDefinition) {
95+
// If we haven't seen a schema definition these types are used if they exist.
96+
topLevelTypes.add('Query');
97+
topLevelTypes.add('Mutation');
98+
topLevelTypes.add('Subscription');
99+
}
100+
101+
// Interfaces are inverted dependencies, so we have to un-invert them here.
102+
for (const [typeName, { interfaces }] of typeMap.entries()) {
103+
for (const interfaceName of interfaces.values()) {
104+
typeMap.get(interfaceName).referencedTypes.add(typeName);
105+
}
106+
}
39107

40-
if (anyReferencedInterfaces) {
41-
return;
108+
// BFS of types to find the used ones
109+
const referencedTypes = new Set(topLevelTypes);
110+
let typeQueue = [...topLevelTypes];
111+
for (const typeName of typeQueue) {
112+
if (typeMap.has(typeName)) {
113+
for (const childType of typeMap
114+
.get(typeName)
115+
.referencedTypes.values()) {
116+
if (!referencedTypes.has(childType)) {
117+
referencedTypes.add(childType);
118+
typeQueue.push(childType);
119+
}
42120
}
43121
}
122+
}
44123

45-
if (!referencedTypes.has(node.name.value)) {
124+
// For every type in the document, check if it is used.
125+
for (const [name, { node }] of typeMap.entries()) {
126+
if (!referencedTypes.has(name)) {
46127
context.reportError(
47128
new ValidationError(
48129
'defined-types-are-used',
49-
`The type \`${node.name.value}\` is defined in the ` +
130+
`The type \`${name}\` is defined in the ` +
50131
`schema but not used anywhere.`,
51132
[node]
52133
)
53134
);
54135
}
55-
});
136+
}
56137
},
57138
},
58139
};

test/rules/defined_types_are_used.js

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,123 @@ describe('DefinedTypesAreUsed rule', () => {
110110
);
111111
});
112112

113+
it('catches Query type if a custom query type is used', () => {
114+
expectFailsRule(
115+
DefinedTypesAreUsed,
116+
`
117+
extend type Query {
118+
b: String
119+
}
120+
121+
type CustomQuery {
122+
a: String
123+
}
124+
125+
schema {
126+
query: CustomQuery,
127+
}
128+
`,
129+
[
130+
{
131+
message:
132+
'The type `Query` is defined in the schema but not used anywhere.',
133+
locations: [{ line: 2, column: 9 }],
134+
},
135+
]
136+
);
137+
});
138+
139+
it('catches Mutation type if a custom mutation type is used', () => {
140+
expectFailsRule(
141+
DefinedTypesAreUsed,
142+
`
143+
type Mutation {
144+
a: String
145+
}
146+
147+
schema {
148+
query: Query
149+
mutation: Query
150+
}
151+
`,
152+
[
153+
{
154+
message:
155+
'The type `Mutation` is defined in the schema but not used anywhere.',
156+
locations: [{ line: 2, column: 9 }],
157+
},
158+
]
159+
);
160+
});
161+
162+
it('catches Subscription type if a custom subscription type is used', () => {
163+
expectFailsRule(
164+
DefinedTypesAreUsed,
165+
`
166+
type Subscription {
167+
a: String
168+
}
169+
170+
schema {
171+
query: Query
172+
subscription: Query
173+
}
174+
`,
175+
[
176+
{
177+
message:
178+
'The type `Subscription` is defined in the schema but not used anywhere.',
179+
locations: [{ line: 2, column: 9 }],
180+
},
181+
]
182+
);
183+
});
184+
185+
it('catches a self-referential type that is unused', () => {
186+
expectFailsRule(
187+
DefinedTypesAreUsed,
188+
`
189+
type A {
190+
a: A
191+
}
192+
`,
193+
[
194+
{
195+
message:
196+
'The type `A` is defined in the schema but not used anywhere.',
197+
locations: [{ line: 2, column: 9 }],
198+
},
199+
]
200+
);
201+
});
202+
203+
it('catches co-referential types that are unused', () => {
204+
expectFailsRule(
205+
DefinedTypesAreUsed,
206+
`
207+
type A {
208+
b: B
209+
}
210+
211+
type B {
212+
a: A
213+
}
214+
`,
215+
[
216+
{
217+
message:
218+
'The type `A` is defined in the schema but not used anywhere.',
219+
locations: [{ line: 2, column: 9 }],
220+
},
221+
{
222+
message:
223+
'The type `B` is defined in the schema but not used anywhere.',
224+
locations: [{ line: 6, column: 9 }],
225+
},
226+
]
227+
);
228+
});
229+
113230
it('ignores types that are a member of a union', () => {
114231
expectPassesRule(
115232
DefinedTypesAreUsed,
@@ -127,6 +244,24 @@ describe('DefinedTypesAreUsed rule', () => {
127244
);
128245
});
129246

247+
it('ignores types that are a member of an extended union', () => {
248+
expectPassesRule(
249+
DefinedTypesAreUsed,
250+
`
251+
extend type Query {
252+
b: B
253+
}
254+
255+
type A {
256+
a: String
257+
}
258+
259+
union B = Query
260+
extend union B = A
261+
`
262+
);
263+
});
264+
130265
it('ignores types that implement an interface that is used', () => {
131266
expectPassesRule(
132267
DefinedTypesAreUsed,
@@ -146,6 +281,29 @@ describe('DefinedTypesAreUsed rule', () => {
146281
);
147282
});
148283

284+
it('ignores types that implement an interface that is used in an extension', () => {
285+
expectPassesRule(
286+
DefinedTypesAreUsed,
287+
`
288+
extend type Query {
289+
c: Node
290+
}
291+
292+
interface Node {
293+
id: ID!
294+
}
295+
296+
type A {
297+
a: String
298+
}
299+
300+
extend type A implements Node{
301+
id: ID!
302+
}
303+
`
304+
);
305+
});
306+
149307
it('ignores types that are used in field definitions', () => {
150308
expectPassesRule(
151309
DefinedTypesAreUsed,
@@ -167,7 +325,7 @@ describe('DefinedTypesAreUsed rule', () => {
167325
`
168326
extend type Query {
169327
b(date: Date): String
170-
c(c: C): String
328+
c(c: [C!]!): String
171329
}
172330
173331
scalar Date
@@ -179,6 +337,21 @@ describe('DefinedTypesAreUsed rule', () => {
179337
);
180338
});
181339

340+
it('ignores types that are used in directives', () => {
341+
expectPassesRule(
342+
DefinedTypesAreUsed,
343+
`
344+
directive @exampleDirective(argument1: [C!], argument2: Date) on FIELD_DEFINITION
345+
346+
scalar Date
347+
348+
input C {
349+
c: String
350+
}
351+
`
352+
);
353+
});
354+
182355
it('ignores unreferenced Mutation object type', () => {
183356
expectPassesRule(
184357
DefinedTypesAreUsed,

0 commit comments

Comments
 (0)