Skip to content

Commit 4aa2d0a

Browse files
UniqueDirectivesPerLocation: check for directive uniqueness in… (#2446)
Fixes #2440 #2442
1 parent f662f95 commit 4aa2d0a

File tree

2 files changed

+143
-45
lines changed

2 files changed

+143
-45
lines changed

src/validation/__tests__/UniqueDirectivesPerLocationRule-test.js

Lines changed: 104 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -201,22 +201,12 @@ describe('Validate: Directives Are Unique Per Location', () => {
201201
SCHEMA | SCALAR | OBJECT | INTERFACE | UNION | INPUT_OBJECT
202202
203203
schema @nonRepeatable @nonRepeatable { query: Dummy }
204-
extend schema @nonRepeatable @nonRepeatable
205204
206205
scalar TestScalar @nonRepeatable @nonRepeatable
207-
extend scalar TestScalar @nonRepeatable @nonRepeatable
208-
209206
type TestObject @nonRepeatable @nonRepeatable
210-
extend type TestObject @nonRepeatable @nonRepeatable
211-
212207
interface TestInterface @nonRepeatable @nonRepeatable
213-
extend interface TestInterface @nonRepeatable @nonRepeatable
214-
215208
union TestUnion @nonRepeatable @nonRepeatable
216-
extend union TestUnion @nonRepeatable @nonRepeatable
217-
218209
input TestInput @nonRepeatable @nonRepeatable
219-
extend input TestInput @nonRepeatable @nonRepeatable
220210
`).to.deep.equal([
221211
{
222212
message:
@@ -230,24 +220,32 @@ describe('Validate: Directives Are Unique Per Location', () => {
230220
message:
231221
'The directive "@nonRepeatable" can only be used once at this location.',
232222
locations: [
233-
{ line: 6, column: 21 },
234-
{ line: 6, column: 36 },
223+
{ line: 7, column: 25 },
224+
{ line: 7, column: 40 },
235225
],
236226
},
237227
{
238228
message:
239229
'The directive "@nonRepeatable" can only be used once at this location.',
240230
locations: [
241-
{ line: 8, column: 25 },
242-
{ line: 8, column: 40 },
231+
{ line: 8, column: 23 },
232+
{ line: 8, column: 38 },
243233
],
244234
},
245235
{
246236
message:
247237
'The directive "@nonRepeatable" can only be used once at this location.',
248238
locations: [
249-
{ line: 9, column: 32 },
250-
{ line: 9, column: 47 },
239+
{ line: 9, column: 31 },
240+
{ line: 9, column: 46 },
241+
],
242+
},
243+
{
244+
message:
245+
'The directive "@nonRepeatable" can only be used once at this location.',
246+
locations: [
247+
{ line: 10, column: 23 },
248+
{ line: 10, column: 38 },
251249
],
252250
},
253251
{
@@ -258,60 +256,136 @@ describe('Validate: Directives Are Unique Per Location', () => {
258256
{ line: 11, column: 38 },
259257
],
260258
},
259+
]);
260+
});
261+
262+
it('duplicate directives on SDL extensions', () => {
263+
expectSDLErrors(`
264+
directive @nonRepeatable on
265+
SCHEMA | SCALAR | OBJECT | INTERFACE | UNION | INPUT_OBJECT
266+
267+
extend schema @nonRepeatable @nonRepeatable
268+
269+
extend scalar TestScalar @nonRepeatable @nonRepeatable
270+
extend type TestObject @nonRepeatable @nonRepeatable
271+
extend interface TestInterface @nonRepeatable @nonRepeatable
272+
extend union TestUnion @nonRepeatable @nonRepeatable
273+
extend input TestInput @nonRepeatable @nonRepeatable
274+
`).to.deep.equal([
261275
{
262276
message:
263277
'The directive "@nonRepeatable" can only be used once at this location.',
264278
locations: [
265-
{ line: 12, column: 30 },
266-
{ line: 12, column: 45 },
279+
{ line: 5, column: 21 },
280+
{ line: 5, column: 36 },
267281
],
268282
},
269283
{
270284
message:
271285
'The directive "@nonRepeatable" can only be used once at this location.',
272286
locations: [
273-
{ line: 14, column: 31 },
274-
{ line: 14, column: 46 },
287+
{ line: 7, column: 32 },
288+
{ line: 7, column: 47 },
275289
],
276290
},
277291
{
278292
message:
279293
'The directive "@nonRepeatable" can only be used once at this location.',
280294
locations: [
281-
{ line: 15, column: 38 },
282-
{ line: 15, column: 53 },
295+
{ line: 8, column: 30 },
296+
{ line: 8, column: 45 },
283297
],
284298
},
285299
{
286300
message:
287301
'The directive "@nonRepeatable" can only be used once at this location.',
288302
locations: [
289-
{ line: 17, column: 23 },
290-
{ line: 17, column: 38 },
303+
{ line: 9, column: 38 },
304+
{ line: 9, column: 53 },
291305
],
292306
},
293307
{
294308
message:
295309
'The directive "@nonRepeatable" can only be used once at this location.',
296310
locations: [
297-
{ line: 18, column: 30 },
298-
{ line: 18, column: 45 },
311+
{ line: 10, column: 30 },
312+
{ line: 10, column: 45 },
299313
],
300314
},
301315
{
302316
message:
303317
'The directive "@nonRepeatable" can only be used once at this location.',
304318
locations: [
305-
{ line: 20, column: 23 },
306-
{ line: 20, column: 38 },
319+
{ line: 11, column: 30 },
320+
{ line: 11, column: 45 },
321+
],
322+
},
323+
]);
324+
});
325+
326+
it('duplicate directives between SDL definitions and extensions', () => {
327+
expectSDLErrors(`
328+
directive @nonRepeatable on SCHEMA
329+
330+
schema @nonRepeatable { query: Dummy }
331+
extend schema @nonRepeatable
332+
`).to.deep.equal([
333+
{
334+
message:
335+
'The directive "@nonRepeatable" can only be used once at this location.',
336+
locations: [
337+
{ line: 4, column: 14 },
338+
{ line: 5, column: 21 },
339+
],
340+
},
341+
]);
342+
343+
expectSDLErrors(`
344+
directive @nonRepeatable on SCALAR
345+
346+
scalar TestScalar @nonRepeatable
347+
extend scalar TestScalar @nonRepeatable
348+
scalar TestScalar @nonRepeatable
349+
`).to.deep.equal([
350+
{
351+
message:
352+
'The directive "@nonRepeatable" can only be used once at this location.',
353+
locations: [
354+
{ line: 4, column: 25 },
355+
{ line: 5, column: 32 },
356+
],
357+
},
358+
{
359+
message:
360+
'The directive "@nonRepeatable" can only be used once at this location.',
361+
locations: [
362+
{ line: 4, column: 25 },
363+
{ line: 6, column: 25 },
364+
],
365+
},
366+
]);
367+
368+
expectSDLErrors(`
369+
directive @nonRepeatable on OBJECT
370+
371+
extend type TestObject @nonRepeatable
372+
type TestObject @nonRepeatable
373+
extend type TestObject @nonRepeatable
374+
`).to.deep.equal([
375+
{
376+
message:
377+
'The directive "@nonRepeatable" can only be used once at this location.',
378+
locations: [
379+
{ line: 4, column: 30 },
380+
{ line: 5, column: 23 },
307381
],
308382
},
309383
{
310384
message:
311385
'The directive "@nonRepeatable" can only be used once at this location.',
312386
locations: [
313-
{ line: 21, column: 30 },
314-
{ line: 21, column: 45 },
387+
{ line: 4, column: 30 },
388+
{ line: 6, column: 30 },
315389
],
316390
},
317391
]);

src/validation/rules/UniqueDirectivesPerLocationRule.js

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import { GraphQLError } from '../../error/GraphQLError';
44

55
import { Kind } from '../../language/kinds';
66
import { type ASTVisitor } from '../../language/visitor';
7+
import {
8+
isTypeDefinitionNode,
9+
isTypeExtensionNode,
10+
} from '../../language/predicates';
711

812
import { specifiedDirectives } from '../../type/directives';
913

@@ -38,27 +42,47 @@ export function UniqueDirectivesPerLocationRule(
3842
}
3943
}
4044

45+
const schemaDirectives = Object.create(null);
46+
const typeDirectivesMap = Object.create(null);
47+
4148
return {
4249
// Many different AST nodes may contain directives. Rather than listing
4350
// them all, just listen for entering any node, and check to see if it
4451
// defines any directives.
4552
enter(node) {
46-
if (node.directives != null) {
47-
const knownDirectives = Object.create(null);
48-
for (const directive of node.directives) {
49-
const directiveName = directive.name.value;
53+
if (node.directives == null) {
54+
return;
55+
}
56+
57+
let seenDirectives;
58+
if (
59+
node.kind === Kind.SCHEMA_DEFINITION ||
60+
node.kind === Kind.SCHEMA_EXTENSION
61+
) {
62+
seenDirectives = schemaDirectives;
63+
} else if (isTypeDefinitionNode(node) || isTypeExtensionNode(node)) {
64+
const typeName = node.name.value;
65+
seenDirectives = typeDirectivesMap[typeName];
66+
if (seenDirectives === undefined) {
67+
typeDirectivesMap[typeName] = seenDirectives = Object.create(null);
68+
}
69+
} else {
70+
seenDirectives = Object.create(null);
71+
}
72+
73+
for (const directive of node.directives) {
74+
const directiveName = directive.name.value;
5075

51-
if (uniqueDirectiveMap[directiveName]) {
52-
if (knownDirectives[directiveName]) {
53-
context.reportError(
54-
new GraphQLError(
55-
`The directive "@${directiveName}" can only be used once at this location.`,
56-
[knownDirectives[directiveName], directive],
57-
),
58-
);
59-
} else {
60-
knownDirectives[directiveName] = directive;
61-
}
76+
if (uniqueDirectiveMap[directiveName]) {
77+
if (seenDirectives[directiveName]) {
78+
context.reportError(
79+
new GraphQLError(
80+
`The directive "@${directiveName}" can only be used once at this location.`,
81+
[seenDirectives[directiveName], directive],
82+
),
83+
);
84+
} else {
85+
seenDirectives[directiveName] = directive;
6286
}
6387
}
6488
}

0 commit comments

Comments
 (0)