Skip to content

Commit

Permalink
UniqueDirectivesPerLocation: check for directive uniqueness in extens…
Browse files Browse the repository at this point in the history
…ions

Fixes #2440 #2442
  • Loading branch information
IvanGoncharov committed Feb 8, 2020
1 parent f662f95 commit 736a835
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 45 deletions.
134 changes: 104 additions & 30 deletions src/validation/__tests__/UniqueDirectivesPerLocationRule-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,22 +201,12 @@ describe('Validate: Directives Are Unique Per Location', () => {
SCHEMA | SCALAR | OBJECT | INTERFACE | UNION | INPUT_OBJECT
schema @nonRepeatable @nonRepeatable { query: Dummy }
extend schema @nonRepeatable @nonRepeatable
scalar TestScalar @nonRepeatable @nonRepeatable
extend scalar TestScalar @nonRepeatable @nonRepeatable
type TestObject @nonRepeatable @nonRepeatable
extend type TestObject @nonRepeatable @nonRepeatable
interface TestInterface @nonRepeatable @nonRepeatable
extend interface TestInterface @nonRepeatable @nonRepeatable
union TestUnion @nonRepeatable @nonRepeatable
extend union TestUnion @nonRepeatable @nonRepeatable
input TestInput @nonRepeatable @nonRepeatable
extend input TestInput @nonRepeatable @nonRepeatable
`).to.deep.equal([
{
message:
Expand All @@ -230,24 +220,32 @@ describe('Validate: Directives Are Unique Per Location', () => {
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 6, column: 21 },
{ line: 6, column: 36 },
{ line: 7, column: 25 },
{ line: 7, column: 40 },
],
},
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 8, column: 25 },
{ line: 8, column: 40 },
{ line: 8, column: 23 },
{ line: 8, column: 38 },
],
},
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 9, column: 32 },
{ line: 9, column: 47 },
{ line: 9, column: 31 },
{ line: 9, column: 46 },
],
},
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 10, column: 23 },
{ line: 10, column: 38 },
],
},
{
Expand All @@ -258,60 +256,136 @@ describe('Validate: Directives Are Unique Per Location', () => {
{ line: 11, column: 38 },
],
},
]);
});

it('duplicate directives on SDL extensions', () => {
expectSDLErrors(`
directive @nonRepeatable on
SCHEMA | SCALAR | OBJECT | INTERFACE | UNION | INPUT_OBJECT
extend schema @nonRepeatable @nonRepeatable
extend scalar TestScalar @nonRepeatable @nonRepeatable
extend type TestObject @nonRepeatable @nonRepeatable
extend interface TestInterface @nonRepeatable @nonRepeatable
extend union TestUnion @nonRepeatable @nonRepeatable
extend input TestInput @nonRepeatable @nonRepeatable
`).to.deep.equal([
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 12, column: 30 },
{ line: 12, column: 45 },
{ line: 5, column: 21 },
{ line: 5, column: 36 },
],
},
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 14, column: 31 },
{ line: 14, column: 46 },
{ line: 7, column: 32 },
{ line: 7, column: 47 },
],
},
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 15, column: 38 },
{ line: 15, column: 53 },
{ line: 8, column: 30 },
{ line: 8, column: 45 },
],
},
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 17, column: 23 },
{ line: 17, column: 38 },
{ line: 9, column: 38 },
{ line: 9, column: 53 },
],
},
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 18, column: 30 },
{ line: 18, column: 45 },
{ line: 10, column: 30 },
{ line: 10, column: 45 },
],
},
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 20, column: 23 },
{ line: 20, column: 38 },
{ line: 11, column: 30 },
{ line: 11, column: 45 },
],
},
]);
});

it('duplicate directives between SDL definitions and extensions', () => {
expectSDLErrors(`
directive @nonRepeatable on SCHEMA
schema @nonRepeatable { query: Dummy }
extend schema @nonRepeatable
`).to.deep.equal([
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 4, column: 14 },
{ line: 5, column: 21 },
],
},
]);

expectSDLErrors(`
directive @nonRepeatable on SCALAR
scalar TestScalar @nonRepeatable
extend scalar TestScalar @nonRepeatable
scalar TestScalar @nonRepeatable
`).to.deep.equal([
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 4, column: 25 },
{ line: 5, column: 32 },
],
},
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 4, column: 25 },
{ line: 6, column: 25 },
],
},
]);

expectSDLErrors(`
directive @nonRepeatable on OBJECT
extend type TestObject @nonRepeatable
type TestObject @nonRepeatable
extend type TestObject @nonRepeatable
`).to.deep.equal([
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 4, column: 30 },
{ line: 5, column: 23 },
],
},
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 21, column: 30 },
{ line: 21, column: 45 },
{ line: 4, column: 30 },
{ line: 6, column: 30 },
],
},
]);
Expand Down
54 changes: 39 additions & 15 deletions src/validation/rules/UniqueDirectivesPerLocationRule.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { GraphQLError } from '../../error/GraphQLError';

import { Kind } from '../../language/kinds';
import { type ASTVisitor } from '../../language/visitor';
import {
isTypeDefinitionNode,
isTypeExtensionNode,
} from '../../language/predicates';

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

Expand Down Expand Up @@ -38,27 +42,47 @@ export function UniqueDirectivesPerLocationRule(
}
}

const schemaDirectives = Object.create(null);
const typeDirectivesMap = Object.create(null);

return {
// Many different AST nodes may contain directives. Rather than listing
// them all, just listen for entering any node, and check to see if it
// defines any directives.
enter(node) {
if (node.directives != null) {
const knownDirectives = Object.create(null);
for (const directive of node.directives) {
const directiveName = directive.name.value;
if (node.directives == null) {
return;
}

let seenDirectives;
if (
node.kind === Kind.SCHEMA_DEFINITION ||
node.kind === Kind.SCHEMA_EXTENSION
) {
seenDirectives = schemaDirectives;
} else if (isTypeDefinitionNode(node) || isTypeExtensionNode(node)) {
const typeName = node.name.value;
seenDirectives = typeDirectivesMap[typeName];
if (seenDirectives === undefined) {
typeDirectivesMap[typeName] = seenDirectives = Object.create(null);
}
} else {
seenDirectives = Object.create(null);
}

for (const directive of node.directives) {
const directiveName = directive.name.value;

if (uniqueDirectiveMap[directiveName]) {
if (knownDirectives[directiveName]) {
context.reportError(
new GraphQLError(
`The directive "@${directiveName}" can only be used once at this location.`,
[knownDirectives[directiveName], directive],
),
);
} else {
knownDirectives[directiveName] = directive;
}
if (uniqueDirectiveMap[directiveName]) {
if (seenDirectives[directiveName]) {
context.reportError(
new GraphQLError(
`The directive "@${directiveName}" can only be used once at this location.`,
[seenDirectives[directiveName], directive],
),
);
} else {
seenDirectives[directiveName] = directive;
}
}
}
Expand Down

0 comments on commit 736a835

Please sign in to comment.