Skip to content

Commit b553a76

Browse files
committed
Add validation for duplicate directive usages
1 parent b3c3832 commit b553a76

File tree

2 files changed

+265
-29
lines changed

2 files changed

+265
-29
lines changed

src/type/__tests__/validation-test.js

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1827,3 +1827,172 @@ describe('Objects must adhere to Interface they implement', () => {
18271827
]);
18281828
});
18291829
});
1830+
1831+
describe('Type System: Schema directives must validate', () => {
1832+
it('accepts a Schema with valid directives', () => {
1833+
const schema = buildSchema(`
1834+
schema @testA @testB {
1835+
query: Query
1836+
}
1837+
1838+
type Query @testA @testB {
1839+
test: AnInterface @testB
1840+
}
1841+
1842+
directive @testA on SCHEMA | OBJECT | INTERFACE | UNION | SCALAR | INPUT_OBJECT
1843+
directive @testB on SCHEMA | OBJECT | INTERFACE | UNION | SCALAR | INPUT_OBJECT
1844+
directive @testC on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION
1845+
directive @testD on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION
1846+
1847+
interface AnInterface @testA {
1848+
field: String! @testB
1849+
}
1850+
1851+
type TypeA implements AnInterface @testA {
1852+
field(arg: SomeInput @testC): String! @testC @testD
1853+
}
1854+
1855+
type TypeB @testB @testA {
1856+
scalar_field: SomeScalar @testC
1857+
enum_field: SomeEnum @testC @testD
1858+
}
1859+
1860+
union SomeUnion @testA = TypeA | TypeB
1861+
1862+
scalar SomeScalar @testA @testB
1863+
1864+
enum SomeEnum @testA @testB {
1865+
SOME_VALUE @testC
1866+
}
1867+
1868+
input SomeInput @testA @testB {
1869+
some_input_field: String @testC
1870+
}
1871+
`);
1872+
expect(validateSchema(schema)).to.deep.equal([]);
1873+
});
1874+
1875+
it('rejects a Schema with directive defined multiple times', () => {
1876+
const schema = buildSchema(`
1877+
type Query {
1878+
test: String
1879+
}
1880+
1881+
directive @testA on SCHEMA
1882+
directive @testA on SCHEMA
1883+
`);
1884+
expect(validateSchema(schema)).to.deep.equal([
1885+
{
1886+
message: 'Directive @testA defined multiple times.',
1887+
locations: [{ line: 6, column: 7 }, { line: 7, column: 7 }],
1888+
},
1889+
]);
1890+
});
1891+
1892+
it('rejects a Schema with same schema directive used twice', () => {
1893+
const schema = buildSchema(`
1894+
schema @testA @testA {
1895+
query: Query
1896+
}
1897+
type Query {
1898+
test: String
1899+
}
1900+
1901+
directive @testA on SCHEMA
1902+
`);
1903+
expect(validateSchema(schema)).to.deep.equal([
1904+
{
1905+
message: 'Directive @testA used twice at the same location.',
1906+
locations: [{ line: 2, column: 14 }, { line: 2, column: 21 }],
1907+
},
1908+
]);
1909+
});
1910+
1911+
it('rejects a Schema with same definition directive used twice', () => {
1912+
const schema = buildSchema(`
1913+
directive @testA on OBJECT | INTERFACE | UNION | SCALAR | INPUT_OBJECT
1914+
1915+
type Query implements SomeInterface @testA @testA {
1916+
test: String
1917+
}
1918+
1919+
interface SomeInterface @testA @testA {
1920+
test: String
1921+
}
1922+
1923+
union SomeUnion @testA @testA = Query
1924+
1925+
scalar SomeScalar @testA @testA
1926+
1927+
enum SomeEnum @testA @testA {
1928+
SOME_VALUE
1929+
}
1930+
1931+
input SomeInput @testA @testA {
1932+
some_input_field: String
1933+
}
1934+
`);
1935+
expect(validateSchema(schema)).to.deep.equal([
1936+
{
1937+
message: 'Directive @testA used twice at the same location.',
1938+
locations: [{ line: 4, column: 43 }, { line: 4, column: 50 }],
1939+
},
1940+
{
1941+
message: 'Directive @testA used twice at the same location.',
1942+
locations: [{ line: 8, column: 31 }, { line: 8, column: 38 }],
1943+
},
1944+
{
1945+
message: 'Directive @testA used twice at the same location.',
1946+
locations: [{ line: 12, column: 23 }, { line: 12, column: 30 }],
1947+
},
1948+
{
1949+
message: 'Directive @testA used twice at the same location.',
1950+
locations: [{ line: 14, column: 25 }, { line: 14, column: 32 }],
1951+
},
1952+
{
1953+
message: 'Directive @testA used twice at the same location.',
1954+
locations: [{ line: 16, column: 21 }, { line: 16, column: 28 }],
1955+
},
1956+
{
1957+
message: 'Directive @testA used twice at the same location.',
1958+
locations: [{ line: 20, column: 23 }, { line: 20, column: 30 }],
1959+
},
1960+
]);
1961+
});
1962+
1963+
it('rejects a Schema with same field and arg directive used twice', () => {
1964+
const schema = buildSchema(`
1965+
directive @testA on FIELD_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION
1966+
1967+
type Query implements SomeInterface {
1968+
test(arg: String @testA @testA): String @testA @testA
1969+
}
1970+
1971+
interface SomeInterface {
1972+
test: String @testA @testA
1973+
}
1974+
1975+
input SomeInput {
1976+
some_input_field: String @testA @testA
1977+
}
1978+
`);
1979+
expect(validateSchema(schema)).to.deep.equal([
1980+
{
1981+
message: 'Directive @testA used twice at the same location.',
1982+
locations: [{ line: 5, column: 26 }, { line: 5, column: 33 }],
1983+
},
1984+
{
1985+
message: 'Directive @testA used twice at the same location.',
1986+
locations: [{ line: 5, column: 49 }, { line: 5, column: 56 }],
1987+
},
1988+
{
1989+
message: 'Directive @testA used twice at the same location.',
1990+
locations: [{ line: 9, column: 22 }, { line: 9, column: 29 }],
1991+
},
1992+
{
1993+
message: 'Directive @testA used twice at the same location.',
1994+
locations: [{ line: 13, column: 34 }, { line: 13, column: 41 }],
1995+
},
1996+
]);
1997+
});
1998+
});

src/type/validate.js

Lines changed: 96 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,44 +7,46 @@
77
* @flow strict
88
*/
99

10-
import {
11-
isObjectType,
12-
isInterfaceType,
13-
isUnionType,
14-
isEnumType,
15-
isInputObjectType,
16-
isNonNullType,
17-
isNamedType,
18-
isInputType,
19-
isOutputType,
20-
} from './definition';
2110
import type {
22-
GraphQLObjectType,
23-
GraphQLInterfaceType,
24-
GraphQLUnionType,
11+
ASTNode,
12+
DirectiveNode,
13+
EnumValueDefinitionNode,
14+
FieldDefinitionNode,
15+
InputValueDefinitionNode,
16+
NamedTypeNode,
17+
TypeNode,
18+
} from '../language/ast';
19+
import type {
2520
GraphQLEnumType,
2621
GraphQLInputObjectType,
22+
GraphQLInterfaceType,
23+
GraphQLObjectType,
24+
GraphQLUnionType,
2725
} from './definition';
28-
import { isDirective } from './directives';
2926
import type { GraphQLDirective } from './directives';
30-
import { isIntrospectionType } from './introspection';
31-
import { isSchema } from './schema';
3227
import type { GraphQLSchema } from './schema';
33-
import inspect from '../jsutils/inspect';
28+
29+
import { GraphQLError } from '../error/GraphQLError';
3430
import find from '../jsutils/find';
31+
import inspect from '../jsutils/inspect';
3532
import invariant from '../jsutils/invariant';
3633
import objectValues from '../jsutils/objectValues';
37-
import { GraphQLError } from '../error/GraphQLError';
38-
import type {
39-
ASTNode,
40-
FieldDefinitionNode,
41-
EnumValueDefinitionNode,
42-
InputValueDefinitionNode,
43-
NamedTypeNode,
44-
TypeNode,
45-
} from '../language/ast';
4634
import { isValidNameError } from '../utilities/assertValidName';
4735
import { isEqualType, isTypeSubTypeOf } from '../utilities/typeComparators';
36+
import {
37+
isEnumType,
38+
isInputObjectType,
39+
isInputType,
40+
isInterfaceType,
41+
isNamedType,
42+
isNonNullType,
43+
isObjectType,
44+
isOutputType,
45+
isUnionType,
46+
} from './definition';
47+
import { isDirective } from './directives';
48+
import { isIntrospectionType } from './introspection';
49+
import { isSchema } from './schema';
4850

4951
/**
5052
* Implements the "Type Validation" sub-sections of the specification's
@@ -70,7 +72,13 @@ export function validateSchema(
7072
// Validate the schema, producing a list of errors.
7173
const context = new SchemaValidationContext(schema);
7274
validateRootTypes(context);
73-
validateDirectives(context);
75+
validateDirectiveDefinitions(context);
76+
77+
// Validate directives that are used on the schema
78+
if (schema.astNode && schema.astNode.directives) {
79+
validateNoDuplicateDirectives(context, schema.astNode.directives);
80+
}
81+
7482
validateTypes(context);
7583

7684
// Persist the results of validation before returning to ensure validation
@@ -165,7 +173,10 @@ function getOperationTypeNode(
165173
return type.astNode;
166174
}
167175

168-
function validateDirectives(context: SchemaValidationContext): void {
176+
function validateDirectiveDefinitions(context: SchemaValidationContext): void {
177+
// Ensure no directive is defined multiple times
178+
const directiveDefinitions = new Map();
179+
169180
for (const directive of context.schema.getDirectives()) {
170181
// Ensure all directives are in fact GraphQL directives.
171182
if (!isDirective(directive)) {
@@ -175,6 +186,9 @@ function validateDirectives(context: SchemaValidationContext): void {
175186
);
176187
continue;
177188
}
189+
const existingDefinitions = directiveDefinitions.get(directive.name) || [];
190+
existingDefinitions.push(directive);
191+
directiveDefinitions.set(directive.name, existingDefinitions);
178192

179193
// Ensure they are named correctly.
180194
validateName(context, directive);
@@ -209,6 +223,15 @@ function validateDirectives(context: SchemaValidationContext): void {
209223
}
210224
}
211225
}
226+
227+
for (const [directiveName, directiveList] of directiveDefinitions) {
228+
if (directiveList.length > 1) {
229+
context.reportError(
230+
`Directive @${directiveName} defined multiple times.`,
231+
directiveList.map(directive => directive.astNode).filter(Boolean),
232+
);
233+
}
234+
}
212235
}
213236

214237
function validateName(
@@ -239,6 +262,8 @@ function validateTypes(context: SchemaValidationContext): void {
239262
continue;
240263
}
241264

265+
validateNoDuplicateDirectives(context, type.getDirectives());
266+
242267
// Ensure it is named correctly (excluding introspection types).
243268
if (!isIntrospectionType(type)) {
244269
validateName(context, type);
@@ -266,6 +291,28 @@ function validateTypes(context: SchemaValidationContext): void {
266291
}
267292
}
268293

294+
function validateNoDuplicateDirectives(
295+
context: SchemaValidationContext,
296+
directives: $ReadOnlyArray<DirectiveNode>,
297+
): void {
298+
const directivesNamed = new Map();
299+
for (const directive of directives) {
300+
const directiveName = directive.name.value;
301+
const existingNodes = directivesNamed.get(directiveName) || [];
302+
existingNodes.push(directive);
303+
directivesNamed.set(directiveName, existingNodes);
304+
}
305+
306+
for (const [directiveName, directiveList] of directivesNamed) {
307+
if (directiveList.length > 1) {
308+
context.reportError(
309+
`Directive @${directiveName} used twice at the same location.`,
310+
directiveList,
311+
);
312+
}
313+
}
314+
}
315+
269316
function validateFields(
270317
context: SchemaValidationContext,
271318
type: GraphQLObjectType | GraphQLInterfaceType,
@@ -329,6 +376,16 @@ function validateFields(
329376
getFieldArgTypeNode(type, field.name, argName),
330377
);
331378
}
379+
380+
// Ensure argument definition directives are valid
381+
if (arg.astNode && arg.astNode.directives) {
382+
validateNoDuplicateDirectives(context, arg.astNode.directives);
383+
}
384+
}
385+
386+
// Ensure any directives are valid
387+
if (field.astNode && field.astNode.directives) {
388+
validateNoDuplicateDirectives(context, field.astNode.directives);
332389
}
333390
}
334391
}
@@ -520,6 +577,11 @@ function validateEnumValues(
520577
enumValue.astNode,
521578
);
522579
}
580+
581+
// Ensure valid directives
582+
if (enumValue.astNode && enumValue.astNode.directives) {
583+
validateNoDuplicateDirectives(context, enumValue.astNode.directives);
584+
}
523585
}
524586
}
525587

@@ -551,6 +613,11 @@ function validateInputFields(
551613
field.astNode && field.astNode.type,
552614
);
553615
}
616+
617+
// Ensure valid directives
618+
if (field.astNode && field.astNode.directives) {
619+
validateNoDuplicateDirectives(context, field.astNode.directives);
620+
}
554621
}
555622
}
556623

0 commit comments

Comments
 (0)