Skip to content

Commit

Permalink
Add 'UniqueArgumentDefinitionNamesRule'
Browse files Browse the repository at this point in the history
  • Loading branch information
IvanGoncharov committed Jul 1, 2021
1 parent 4493ca3 commit 2643c70
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -357,6 +357,7 @@ export {
UniqueTypeNamesRule,
UniqueEnumValueNamesRule,
UniqueFieldDefinitionNamesRule,
UniqueArgumentDefinitionNamesRule,
UniqueDirectiveNamesRule,
PossibleTypeExtensionsRule,
/** Custom validation rules */
Expand Down
120 changes: 120 additions & 0 deletions src/validation/__tests__/UniqueArgumentDefinitionNamesRule-test.ts
@@ -0,0 +1,120 @@
import { describe, it } from 'mocha';

import { UniqueArgumentDefinitionNamesRule } from '../rules/UniqueArgumentDefinitionNamesRule';

import { expectSDLValidationErrors } from './harness';

function expectSDLErrors(sdlStr: string) {
return expectSDLValidationErrors(
undefined,
UniqueArgumentDefinitionNamesRule,
sdlStr,
);
}

function expectValidSDL(sdlStr: string) {
expectSDLErrors(sdlStr).to.deep.equal([]);
}

describe('Validate: Unique argument definition names', () => {
it('no args', () => {
expectValidSDL(`
type SomeObject {
someField: String
}
interface SomeInterface {
someField: String
}
directive @someDirective on QUERY
`);
});

it('one argument', () => {
expectValidSDL(`
type SomeObject {
someField(foo: String): String
}
interface SomeInterface {
someField(foo: String): String
}
directive @someDirective(foo: String) on QUERY
`);
});

it('multiple arguments', () => {
expectValidSDL(`
type SomeObject {
someField(
foo: String
bar: String
): String
}
interface SomeInterface {
someField(
foo: String
bar: String
): String
}
directive @someDirective(
foo: String
bar: String
) on QUERY
`);
});

it('duplicating arguments', () => {
expectSDLErrors(`
type SomeObject {
someField(
foo: String
bar: String
foo: String
): String
}
interface SomeInterface {
someField(
foo: String
bar: String
foo: String
): String
}
directive @someDirective(
foo: String
bar: String
foo: String
) on QUERY
`).to.deep.equal([
{
message:
'Argument "SomeObject.someField(foo:)" can only be defined once.',
locations: [
{ line: 4, column: 11 },
{ line: 6, column: 11 },
],
},
{
message:
'Argument "SomeInterface.someField(foo:)" can only be defined once.',
locations: [
{ line: 12, column: 11 },
{ line: 14, column: 11 },
],
},
{
message: 'Argument "@someDirective(foo:)" can only be defined once.',
locations: [
{ line: 19, column: 9 },
{ line: 21, column: 9 },
],
},
]);
});
});
1 change: 1 addition & 0 deletions src/validation/index.ts
Expand Up @@ -90,6 +90,7 @@ export { UniqueOperationTypesRule } from './rules/UniqueOperationTypesRule';
export { UniqueTypeNamesRule } from './rules/UniqueTypeNamesRule';
export { UniqueEnumValueNamesRule } from './rules/UniqueEnumValueNamesRule';
export { UniqueFieldDefinitionNamesRule } from './rules/UniqueFieldDefinitionNamesRule';
export { UniqueArgumentDefinitionNamesRule } from './rules/UniqueArgumentDefinitionNamesRule';
export { UniqueDirectiveNamesRule } from './rules/UniqueDirectiveNamesRule';
export { PossibleTypeExtensionsRule } from './rules/PossibleTypeExtensionsRule';

Expand Down
91 changes: 91 additions & 0 deletions src/validation/rules/UniqueArgumentDefinitionNamesRule.ts
@@ -0,0 +1,91 @@
import { GraphQLError } from '../../error/GraphQLError';

import type { ASTVisitor } from '../../language/visitor';
import type {
NameNode,
FieldDefinitionNode,
InputValueDefinitionNode,
} from '../../language/ast';

import type { SDLValidationContext } from '../ValidationContext';

/**
* Unique argument definition names
*
* A GraphQL Object or Interface type is only valid if all its fields have uniquely named arguments.
* A GraphQL Directive is only valid if all its arguments are uniquely named.
*/
export function UniqueArgumentDefinitionNamesRule(
context: SDLValidationContext,
): ASTVisitor {
return {
DirectiveDefinition(directiveNode) {
// istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203')
const argumentNodes = directiveNode.arguments ?? [];

return checkArgUniqueness(`@${directiveNode.name.value}`, argumentNodes);
},
InterfaceTypeDefinition: checkArgUniquenessPerField,
InterfaceTypeExtension: checkArgUniquenessPerField,
ObjectTypeDefinition: checkArgUniquenessPerField,
ObjectTypeExtension: checkArgUniquenessPerField,
};

function checkArgUniquenessPerField(typeNode: {
readonly name: NameNode;
readonly fields?: ReadonlyArray<FieldDefinitionNode>;
}) {
const typeName = typeNode.name.value;

// istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203')
const fieldNodes = typeNode.fields ?? [];

for (const fieldDef of fieldNodes) {
const fieldName = fieldDef.name.value;

// istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203')
const argumentNodes = fieldDef.arguments ?? [];

checkArgUniqueness(`${typeName}.${fieldName}`, argumentNodes);
}

return false;
}

function checkArgUniqueness(
parentName: string,
argumentNodes: ReadonlyArray<InputValueDefinitionNode>,
) {
const seenArgs = groupBy(argumentNodes, (arg) => arg.name.value);

for (const [argName, argNodes] of seenArgs) {
if (argNodes.length > 1) {
context.reportError(
new GraphQLError(
`Argument "${parentName}(${argName}:)" can only be defined once.`,
argNodes.map((node) => node.name),
),
);
}
}

return false;
}
}

function groupBy<K, T>(
list: ReadonlyArray<T>,
keyFn: (item: T) => K,
): Map<K, Array<T>> {
const result = new Map<K, Array<T>>();
for (const item of list) {
const key = keyFn(item);
const group = result.get(key);
if (group === undefined) {
result.set(key, [item]);
} else {
group.push(item);
}
}
return result;
}
2 changes: 2 additions & 0 deletions src/validation/specifiedRules.ts
Expand Up @@ -90,6 +90,7 @@ import { UniqueOperationTypesRule } from './rules/UniqueOperationTypesRule';
import { UniqueTypeNamesRule } from './rules/UniqueTypeNamesRule';
import { UniqueEnumValueNamesRule } from './rules/UniqueEnumValueNamesRule';
import { UniqueFieldDefinitionNamesRule } from './rules/UniqueFieldDefinitionNamesRule';
import { UniqueArgumentDefinitionNamesRule } from './rules/UniqueArgumentDefinitionNamesRule';
import { UniqueDirectiveNamesRule } from './rules/UniqueDirectiveNamesRule';
import { PossibleTypeExtensionsRule } from './rules/PossibleTypeExtensionsRule';

Expand Down Expand Up @@ -138,6 +139,7 @@ export const specifiedSDLRules: ReadonlyArray<SDLValidationRule> =
UniqueTypeNamesRule,
UniqueEnumValueNamesRule,
UniqueFieldDefinitionNamesRule,
UniqueArgumentDefinitionNamesRule,
UniqueDirectiveNamesRule,
KnownTypeNamesRule,
KnownDirectivesRule,
Expand Down

0 comments on commit 2643c70

Please sign in to comment.