Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add 'UniqueArgumentDefinitionNamesRule'
Background graphql/graphql-wg#505
- Loading branch information
1 parent
4493ca3
commit b3595c7
Showing
5 changed files
with
215 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
120 changes: 120 additions & 0 deletions
120
src/validation/__tests__/UniqueArgumentDefinitionNamesRule-test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }, | ||
], | ||
}, | ||
]); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 Interfacase 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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters