diff --git a/src/validation/__tests__/UniqueDirectivesPerLocation-test.js b/src/validation/__tests__/UniqueDirectivesPerLocation-test.js new file mode 100644 index 0000000000..dc1311d250 --- /dev/null +++ b/src/validation/__tests__/UniqueDirectivesPerLocation-test.js @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import { describe, it } from 'mocha'; +import { expectPassesRule, expectFailsRule } from './harness'; +import { + UniqueDirectivesPerLocation, + duplicateDirectiveMessage, +} from '../rules/UniqueDirectivesPerLocation'; + + +function duplicateDirective(directiveName, l1, c1, l2, c2) { + return { + message: duplicateDirectiveMessage(directiveName), + locations: [ { line: l1, column: c1 }, { line: l2, column: c2 } ], + }; +} + +describe('Validate: Directives Are Unique Per Location', () => { + + it('no directives', () => { + expectPassesRule(UniqueDirectivesPerLocation, ` + fragment Test on Type { + field + } + `); + }); + + it('unique directives in different locations', () => { + expectPassesRule(UniqueDirectivesPerLocation, ` + fragment Test on Type @directiveA { + field @directiveB + } + `); + }); + + it('unique directives in same locations', () => { + expectPassesRule(UniqueDirectivesPerLocation, ` + fragment Test on Type @directiveA @directiveB { + field @directiveA @directiveB + } + `); + }); + + it('same directives in different locations', () => { + expectPassesRule(UniqueDirectivesPerLocation, ` + fragment Test on Type @directiveA { + field @directiveA + } + `); + }); + + it('same directives in similar locations', () => { + expectPassesRule(UniqueDirectivesPerLocation, ` + fragment Test on Type { + field @directive + field @directive + } + `); + }); + + it('duplicate directives in one location', () => { + expectFailsRule(UniqueDirectivesPerLocation, ` + fragment Test on Type { + field @directive @directive + } + `, [ + duplicateDirective('directive', 3, 15, 3, 26) + ]); + }); + + it('many duplicate directives in one location', () => { + expectFailsRule(UniqueDirectivesPerLocation, ` + fragment Test on Type { + field @directive @directive @directive + } + `, [ + duplicateDirective('directive', 3, 15, 3, 26), + duplicateDirective('directive', 3, 15, 3, 37) + ]); + }); + + it('different duplicate directives in one location', () => { + expectFailsRule(UniqueDirectivesPerLocation, ` + fragment Test on Type { + field @directiveA @directiveB @directiveA @directiveB + } + `, [ + duplicateDirective('directiveA', 3, 15, 3, 39), + duplicateDirective('directiveB', 3, 27, 3, 51) + ]); + }); + + it('duplicate directives in many locations', () => { + expectFailsRule(UniqueDirectivesPerLocation, ` + fragment Test on Type @directive @directive { + field @directive @directive + } + `, [ + duplicateDirective('directive', 2, 29, 2, 40), + duplicateDirective('directive', 3, 15, 3, 26) + ]); + }); + +}); diff --git a/src/validation/rules/UniqueDirectivesPerLocation.js b/src/validation/rules/UniqueDirectivesPerLocation.js new file mode 100644 index 0000000000..5ce29e8a7a --- /dev/null +++ b/src/validation/rules/UniqueDirectivesPerLocation.js @@ -0,0 +1,48 @@ +/* @flow */ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import type { ValidationContext } from '../index'; +import { GraphQLError } from '../../error'; + + +export function duplicateDirectiveMessage(directiveName: string): string { + return `The directive "${directiveName}" can only be used once at ` + + 'this location.'; +} + +/** + * Unique directive names per location + * + * A GraphQL document is only valid if all directives at a given location + * are uniquely named. + */ +export function UniqueDirectivesPerLocation(context: ValidationContext): any { + 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) { + const knownDirectives = Object.create(null); + node.directives.forEach(directive => { + const directiveName = directive.name.value; + if (knownDirectives[directiveName]) { + context.reportError(new GraphQLError( + duplicateDirectiveMessage(directiveName), + [ knownDirectives[directiveName], directive ] + )); + } else { + knownDirectives[directiveName] = directive; + } + }); + } + } + }; +} diff --git a/src/validation/specifiedRules.js b/src/validation/specifiedRules.js index c1287ef83a..c5b5ee828e 100644 --- a/src/validation/specifiedRules.js +++ b/src/validation/specifiedRules.js @@ -56,6 +56,11 @@ import { NoUnusedVariables } from './rules/NoUnusedVariables'; // Spec Section: "Directives Are Defined" import { KnownDirectives } from './rules/KnownDirectives'; +// Spec Section: "Directives Are Unique Per Location" +import { + UniqueDirectivesPerLocation +} from './rules/UniqueDirectivesPerLocation'; + // Spec Section: "Argument Names" import { KnownArgumentNames } from './rules/KnownArgumentNames'; @@ -104,6 +109,7 @@ export const specifiedRules: Array<(context: ValidationContext) => any> = [ NoUndefinedVariables, NoUnusedVariables, KnownDirectives, + UniqueDirectivesPerLocation, KnownArgumentNames, UniqueArgumentNames, ArgumentsOfCorrectType,