Skip to content

Commit b3595c7

Browse files
committed
Add 'UniqueArgumentDefinitionNamesRule'
Background graphql/graphql-wg#505
1 parent 4493ca3 commit b3595c7

File tree

5 files changed

+215
-0
lines changed

5 files changed

+215
-0
lines changed

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,7 @@ export {
357357
UniqueTypeNamesRule,
358358
UniqueEnumValueNamesRule,
359359
UniqueFieldDefinitionNamesRule,
360+
UniqueArgumentDefinitionNamesRule,
360361
UniqueDirectiveNamesRule,
361362
PossibleTypeExtensionsRule,
362363
/** Custom validation rules */
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { describe, it } from 'mocha';
2+
3+
import { UniqueArgumentDefinitionNamesRule } from '../rules/UniqueArgumentDefinitionNamesRule';
4+
5+
import { expectSDLValidationErrors } from './harness';
6+
7+
function expectSDLErrors(sdlStr: string) {
8+
return expectSDLValidationErrors(
9+
undefined,
10+
UniqueArgumentDefinitionNamesRule,
11+
sdlStr,
12+
);
13+
}
14+
15+
function expectValidSDL(sdlStr: string) {
16+
expectSDLErrors(sdlStr).to.deep.equal([]);
17+
}
18+
19+
describe('Validate: Unique argument definition names', () => {
20+
it('no args', () => {
21+
expectValidSDL(`
22+
type SomeObject {
23+
someField: String
24+
}
25+
26+
interface SomeInterface {
27+
someField: String
28+
}
29+
30+
directive @someDirective on QUERY
31+
`);
32+
});
33+
34+
it('one argument', () => {
35+
expectValidSDL(`
36+
type SomeObject {
37+
someField(foo: String): String
38+
}
39+
40+
interface SomeInterface {
41+
someField(foo: String): String
42+
}
43+
44+
directive @someDirective(foo: String) on QUERY
45+
`);
46+
});
47+
48+
it('multiple arguments', () => {
49+
expectValidSDL(`
50+
type SomeObject {
51+
someField(
52+
foo: String
53+
bar: String
54+
): String
55+
}
56+
57+
interface SomeInterface {
58+
someField(
59+
foo: String
60+
bar: String
61+
): String
62+
}
63+
64+
directive @someDirective(
65+
foo: String
66+
bar: String
67+
) on QUERY
68+
`);
69+
});
70+
71+
it('duplicating arguments', () => {
72+
expectSDLErrors(`
73+
type SomeObject {
74+
someField(
75+
foo: String
76+
bar: String
77+
foo: String
78+
): String
79+
}
80+
81+
interface SomeInterface {
82+
someField(
83+
foo: String
84+
bar: String
85+
foo: String
86+
): String
87+
}
88+
89+
directive @someDirective(
90+
foo: String
91+
bar: String
92+
foo: String
93+
) on QUERY
94+
`).to.deep.equal([
95+
{
96+
message:
97+
'Argument "SomeObject.someField(foo:)" can only be defined once.',
98+
locations: [
99+
{ line: 4, column: 11 },
100+
{ line: 6, column: 11 },
101+
],
102+
},
103+
{
104+
message:
105+
'Argument "SomeInterface.someField(foo:)" can only be defined once.',
106+
locations: [
107+
{ line: 12, column: 11 },
108+
{ line: 14, column: 11 },
109+
],
110+
},
111+
{
112+
message: 'Argument "@someDirective(foo:)" can only be defined once.',
113+
locations: [
114+
{ line: 19, column: 9 },
115+
{ line: 21, column: 9 },
116+
],
117+
},
118+
]);
119+
});
120+
});

src/validation/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export { UniqueOperationTypesRule } from './rules/UniqueOperationTypesRule';
9090
export { UniqueTypeNamesRule } from './rules/UniqueTypeNamesRule';
9191
export { UniqueEnumValueNamesRule } from './rules/UniqueEnumValueNamesRule';
9292
export { UniqueFieldDefinitionNamesRule } from './rules/UniqueFieldDefinitionNamesRule';
93+
export { UniqueArgumentDefinitionNamesRule } from './rules/UniqueArgumentDefinitionNamesRule';
9394
export { UniqueDirectiveNamesRule } from './rules/UniqueDirectiveNamesRule';
9495
export { PossibleTypeExtensionsRule } from './rules/PossibleTypeExtensionsRule';
9596

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { GraphQLError } from '../../error/GraphQLError';
2+
3+
import type { ASTVisitor } from '../../language/visitor';
4+
import type {
5+
NameNode,
6+
FieldDefinitionNode,
7+
InputValueDefinitionNode,
8+
} from '../../language/ast';
9+
10+
import type { SDLValidationContext } from '../ValidationContext';
11+
12+
/**
13+
* Unique argument definition names
14+
*
15+
* A GraphQL Object or Interfacase type is only valid if all its fields have uniquely named arguments.
16+
* A GraphQL Directive is only valid if all its arguments are uniquely named.
17+
*/
18+
export function UniqueArgumentDefinitionNamesRule(
19+
context: SDLValidationContext,
20+
): ASTVisitor {
21+
return {
22+
DirectiveDefinition(directiveNode) {
23+
// istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203')
24+
const argumentNodes = directiveNode.arguments ?? [];
25+
26+
return checkArgUniqueness(`@${directiveNode.name.value}`, argumentNodes);
27+
},
28+
InterfaceTypeDefinition: checkArgUniquenessPerField,
29+
InterfaceTypeExtension: checkArgUniquenessPerField,
30+
ObjectTypeDefinition: checkArgUniquenessPerField,
31+
ObjectTypeExtension: checkArgUniquenessPerField,
32+
};
33+
34+
function checkArgUniquenessPerField(typeNode: {
35+
readonly name: NameNode;
36+
readonly fields?: ReadonlyArray<FieldDefinitionNode>;
37+
}) {
38+
const typeName = typeNode.name.value;
39+
40+
// istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203')
41+
const fieldNodes = typeNode.fields ?? [];
42+
43+
for (const fieldDef of fieldNodes) {
44+
const fieldName = fieldDef.name.value;
45+
46+
// istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203')
47+
const argumentNodes = fieldDef.arguments ?? [];
48+
49+
checkArgUniqueness(`${typeName}.${fieldName}`, argumentNodes);
50+
}
51+
52+
return false;
53+
}
54+
55+
function checkArgUniqueness(
56+
parentName: string,
57+
argumentNodes: ReadonlyArray<InputValueDefinitionNode>,
58+
) {
59+
const seenArgs = groupBy(argumentNodes, (arg) => arg.name.value);
60+
61+
for (const [argName, argNodes] of seenArgs) {
62+
if (argNodes.length > 1) {
63+
context.reportError(
64+
new GraphQLError(
65+
`Argument "${parentName}(${argName}:)" can only be defined once.`,
66+
argNodes.map((node) => node.name),
67+
),
68+
);
69+
}
70+
}
71+
72+
return false;
73+
}
74+
}
75+
76+
function groupBy<K, T>(
77+
list: ReadonlyArray<T>,
78+
keyFn: (item: T) => K,
79+
): Map<K, Array<T>> {
80+
const result = new Map<K, Array<T>>();
81+
for (const item of list) {
82+
const key = keyFn(item);
83+
const group = result.get(key);
84+
if (group === undefined) {
85+
result.set(key, [item]);
86+
} else {
87+
group.push(item);
88+
}
89+
}
90+
return result;
91+
}

src/validation/specifiedRules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ import { UniqueOperationTypesRule } from './rules/UniqueOperationTypesRule';
9090
import { UniqueTypeNamesRule } from './rules/UniqueTypeNamesRule';
9191
import { UniqueEnumValueNamesRule } from './rules/UniqueEnumValueNamesRule';
9292
import { UniqueFieldDefinitionNamesRule } from './rules/UniqueFieldDefinitionNamesRule';
93+
import { UniqueArgumentDefinitionNamesRule } from './rules/UniqueArgumentDefinitionNamesRule';
9394
import { UniqueDirectiveNamesRule } from './rules/UniqueDirectiveNamesRule';
9495
import { PossibleTypeExtensionsRule } from './rules/PossibleTypeExtensionsRule';
9596

@@ -138,6 +139,7 @@ export const specifiedSDLRules: ReadonlyArray<SDLValidationRule> =
138139
UniqueTypeNamesRule,
139140
UniqueEnumValueNamesRule,
140141
UniqueFieldDefinitionNamesRule,
142+
UniqueArgumentDefinitionNamesRule,
141143
UniqueDirectiveNamesRule,
142144
KnownTypeNamesRule,
143145
KnownDirectivesRule,

0 commit comments

Comments
 (0)