Skip to content

Commit

Permalink
Update all validation rules: tests pass
Browse files Browse the repository at this point in the history
  • Loading branch information
mjmahone committed Jan 5, 2023
1 parent 76fb6a6 commit 686fe56
Show file tree
Hide file tree
Showing 14 changed files with 666 additions and 207 deletions.
1 change: 1 addition & 0 deletions src/language/directiveLocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export enum DirectiveLocation {
FRAGMENT_SPREAD = 'FRAGMENT_SPREAD',
INLINE_FRAGMENT = 'INLINE_FRAGMENT',
VARIABLE_DEFINITION = 'VARIABLE_DEFINITION',
FRAGMENT_ARGUMENT_DEFINITION = 'FRAGMENT_ARGUMENT_DEFINITION',
/** Type System Definitions */
SCHEMA = 'SCHEMA',
SCALAR = 'SCALAR',
Expand Down
107 changes: 107 additions & 0 deletions src/utilities/__tests__/TypeInfo-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,4 +515,111 @@ describe('visitWithTypeInfo', () => {
['leave', 'SelectionSet', null, 'Human', 'Human'],
]);
});

it('supports traversals of fragment arguments', () => {
const typeInfo = new TypeInfo(testSchema);

const ast = parse(`
query {
...Foo(x: 4)
}
fragment Foo(
"Human to get"
$x: ID!
) on QueryRoot {
human(id: $x) { name }
}
`);

const visited: Array<any> = [];
visit(
ast,
visitWithTypeInfo(typeInfo, {
enter(node) {
const type = typeInfo.getType();
const inputType = typeInfo.getInputType();
visited.push([
'enter',
node.kind,
node.kind === 'Name' ? node.value : null,
String(type),
String(inputType),
]);
},
leave(node) {
const type = typeInfo.getType();
const inputType = typeInfo.getInputType();
visited.push([
'leave',
node.kind,
node.kind === 'Name' ? node.value : null,
String(type),
String(inputType),
]);
},
}),
);

expect(visited).to.deep.equal([
['enter', 'Document', null, 'undefined', 'undefined'],
['enter', 'OperationDefinition', null, 'QueryRoot', 'undefined'],
['enter', 'SelectionSet', null, 'QueryRoot', 'undefined'],
['enter', 'FragmentSpread', null, 'QueryRoot', 'undefined'],
['enter', 'Name', 'Foo', 'QueryRoot', 'undefined'],
['leave', 'Name', 'Foo', 'QueryRoot', 'undefined'],
['enter', 'Argument', null, 'QueryRoot', 'ID!'],
['enter', 'Name', 'x', 'QueryRoot', 'ID!'],
['leave', 'Name', 'x', 'QueryRoot', 'ID!'],
['enter', 'IntValue', null, 'QueryRoot', 'ID!'],
['leave', 'IntValue', null, 'QueryRoot', 'ID!'],
['leave', 'Argument', null, 'QueryRoot', 'ID!'],
['leave', 'FragmentSpread', null, 'QueryRoot', 'undefined'],
['leave', 'SelectionSet', null, 'QueryRoot', 'undefined'],
['leave', 'OperationDefinition', null, 'QueryRoot', 'undefined'],
['enter', 'FragmentDefinition', null, 'QueryRoot', 'undefined'],
['enter', 'Name', 'Foo', 'QueryRoot', 'undefined'],
['leave', 'Name', 'Foo', 'QueryRoot', 'undefined'],
['enter', 'FragmentArgumentDefinition', null, 'QueryRoot', 'ID!'],
['enter', 'StringValue', null, 'QueryRoot', 'ID!'],
['leave', 'StringValue', null, 'QueryRoot', 'ID!'],
['enter', 'Variable', null, 'QueryRoot', 'ID!'],
['enter', 'Name', 'x', 'QueryRoot', 'ID!'],
['leave', 'Name', 'x', 'QueryRoot', 'ID!'],
['leave', 'Variable', null, 'QueryRoot', 'ID!'],
['enter', 'NonNullType', null, 'QueryRoot', 'ID!'],
['enter', 'NamedType', null, 'QueryRoot', 'ID!'],
['enter', 'Name', 'ID', 'QueryRoot', 'ID!'],
['leave', 'Name', 'ID', 'QueryRoot', 'ID!'],
['leave', 'NamedType', null, 'QueryRoot', 'ID!'],
['leave', 'NonNullType', null, 'QueryRoot', 'ID!'],
['leave', 'FragmentArgumentDefinition', null, 'QueryRoot', 'ID!'],
['enter', 'NamedType', null, 'QueryRoot', 'undefined'],
['enter', 'Name', 'QueryRoot', 'QueryRoot', 'undefined'],
['leave', 'Name', 'QueryRoot', 'QueryRoot', 'undefined'],
['leave', 'NamedType', null, 'QueryRoot', 'undefined'],
['enter', 'SelectionSet', null, 'QueryRoot', 'undefined'],
['enter', 'Field', null, 'Human', 'undefined'],
['enter', 'Name', 'human', 'Human', 'undefined'],
['leave', 'Name', 'human', 'Human', 'undefined'],
['enter', 'Argument', null, 'Human', 'ID'],
['enter', 'Name', 'id', 'Human', 'ID'],
['leave', 'Name', 'id', 'Human', 'ID'],
['enter', 'Variable', null, 'Human', 'ID'],
['enter', 'Name', 'x', 'Human', 'ID'],
['leave', 'Name', 'x', 'Human', 'ID'],
['leave', 'Variable', null, 'Human', 'ID'],
['leave', 'Argument', null, 'Human', 'ID'],
['enter', 'SelectionSet', null, 'Human', 'undefined'],
['enter', 'Field', null, 'String', 'undefined'],
['enter', 'Name', 'name', 'String', 'undefined'],
['leave', 'Name', 'name', 'String', 'undefined'],
['leave', 'Field', null, 'String', 'undefined'],
['leave', 'SelectionSet', null, 'Human', 'undefined'],
['leave', 'Field', null, 'Human', 'undefined'],
['leave', 'SelectionSet', null, 'QueryRoot', 'undefined'],
['leave', 'FragmentDefinition', null, 'QueryRoot', 'undefined'],
['leave', 'Document', null, 'undefined', 'undefined'],
]);
});
});
14 changes: 11 additions & 3 deletions src/validation/__tests__/KnownDirectivesRule-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const schemaWithDirectives = buildSchema(`
directive @onSubscription on SUBSCRIPTION
directive @onField on FIELD
directive @onFragmentDefinition on FRAGMENT_DEFINITION
directive @onFragmentArgumentDefinition on FRAGMENT_ARGUMENT_DEFINITION
directive @onFragmentSpread on FRAGMENT_SPREAD
directive @onInlineFragment on INLINE_FRAGMENT
directive @onVariableDefinition on VARIABLE_DEFINITION
Expand Down Expand Up @@ -150,7 +151,9 @@ describe('Validate: Known directives', () => {
someField @onField
}
fragment Frag on Human @onFragmentDefinition {
fragment Frag(
$arg: Int @onFragmentArgumentDefinition
) on Human @onFragmentDefinition {
name @onField
}
`);
Expand All @@ -175,7 +178,7 @@ describe('Validate: Known directives', () => {
someField @onQuery
}
fragment Frag on Human @onQuery {
fragment Frag($arg: Int @onField) on Human @onQuery {
name @onQuery
}
`).toDeepEqual([
Expand Down Expand Up @@ -219,9 +222,14 @@ describe('Validate: Known directives', () => {
message: 'Directive "@onQuery" may not be used on FIELD.',
locations: [{ column: 19, line: 16 }],
},
{
message:
'Directive "@onField" may not be used on FRAGMENT_ARGUMENT_DEFINITION.',
locations: [{ column: 31, line: 19 }],
},
{
message: 'Directive "@onQuery" may not be used on FRAGMENT_DEFINITION.',
locations: [{ column: 30, line: 19 }],
locations: [{ column: 50, line: 19 }],
},
{
message: 'Directive "@onQuery" may not be used on FIELD.',
Expand Down
120 changes: 120 additions & 0 deletions src/validation/__tests__/NoUnusedFragmentArgumentsRule-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { describe, it } from 'mocha';

import { NoUnusedFragmentArgumentsRule } from '../rules/NoUnusedFragmentArgumentsRule.js';

import { expectValidationErrors } from './harness.js';

function expectErrors(queryStr: string) {
return expectValidationErrors(NoUnusedFragmentArgumentsRule, queryStr);
}

function expectValid(queryStr: string) {
expectErrors(queryStr).toDeepEqual([]);
}

describe('Validate: No unused fragment arguments', () => {
it('uses all arguments', () => {
expectValid(`
fragment Foo($a: String, $b: String, $c: String) on Type {
field(a: $a, b: $b, c: $c)
}
`);
});

it('uses all arguments deeply', () => {
expectValid(`
fragment Foo($a: String, $b: String, $c: String) on Type {
field(a: $a) {
field(b: $b) {
field(c: $c)
}
}
}
`);
});

it('uses all arguments deeply in inline fragments', () => {
expectValid(`
fragment Foo($a: String, $b: String, $c: String) on Type {
... on Type {
field(a: $a) {
field(b: $b) {
... on Type {
field(c: $c)
}
}
}
}
}
`);
});

it('argument not used', () => {
expectErrors(`
fragment Foo($a: String, $b: String, $c: String) on Type {
field(a: $a, b: $b)
}
`).toDeepEqual([
{
message: 'Argument "$c" is never used in fragment "Foo".',
locations: [{ line: 2, column: 44 }],
},
]);
});

it('query passes in unused argument', () => {
expectErrors(`
query Q($c: String) {
type {
...Foo(a: "", b: "", c: $c)
}
}
fragment Foo($a: String, $b: String, $c: String) on Type {
field(a: $a, b: $b)
}
`).toDeepEqual([
{
message: 'Argument "$c" is never used in fragment "Foo".',
locations: [{ line: 7, column: 44 }],
},
]);
});

it('child fragment uses a variable of the same name', () => {
expectErrors(`
query Q($a: String) {
type {
...Foo
}
}
fragment Foo($a: String) on Type {
...Bar
}
fragment Bar on Type {
field(a: $a)
}
`).toDeepEqual([
{
message: 'Argument "$a" is never used in fragment "Foo".',
locations: [{ line: 7, column: 20 }],
},
]);
});

it('multiple arguments not used', () => {
expectErrors(`
fragment Foo($a: String, $b: String, $c: String) on Type {
field(b: $b)
}
`).toDeepEqual([
{
message: 'Argument "$a" is never used in fragment "Foo".',
locations: [{ line: 2, column: 20 }],
},
{
message: 'Argument "$c" is never used in fragment "Foo".',
locations: [{ line: 2, column: 44 }],
},
]);
});
});
3 changes: 3 additions & 0 deletions src/validation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ export { NoUndefinedVariablesRule } from './rules/NoUndefinedVariablesRule.js';
// Spec Section: "Fragments must be used"
export { NoUnusedFragmentsRule } from './rules/NoUnusedFragmentsRule.js';

// Spec Section: "All Fragment Arguments Used"
export { NoUnusedFragmentArgumentsRule } from './rules/NoUnusedFragmentArgumentsRule.js';

// Spec Section: "All Variables Used"
export { NoUnusedVariablesRule } from './rules/NoUnusedVariablesRule.js';

Expand Down
2 changes: 2 additions & 0 deletions src/validation/rules/KnownDirectivesRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ function getDirectiveLocationForASTPath(
return DirectiveLocation.INLINE_FRAGMENT;
case Kind.FRAGMENT_DEFINITION:
return DirectiveLocation.FRAGMENT_DEFINITION;
case Kind.FRAGMENT_ARGUMENT_DEFINITION:
return DirectiveLocation.FRAGMENT_ARGUMENT_DEFINITION;
case Kind.VARIABLE_DEFINITION:
return DirectiveLocation.VARIABLE_DEFINITION;
case Kind.SCHEMA_DEFINITION:
Expand Down
5 changes: 4 additions & 1 deletion src/validation/rules/NoUndefinedVariablesRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ export function NoUndefinedVariablesRule(
);

const usages = context.getRecursiveVariableUsages(operation);
for (const { node } of usages) {
for (const { node, fragmentArgDef } of usages) {
if (fragmentArgDef) {
continue;
}
const varName = node.name.value;
if (!variableNameDefined.has(varName)) {
context.reportError(
Expand Down
40 changes: 40 additions & 0 deletions src/validation/rules/NoUnusedFragmentArgumentsRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { GraphQLError } from '../../error/GraphQLError.js';

import type { ASTVisitor } from '../../language/visitor.js';

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

/**
* No unused variables
*
* A GraphQL fragment is only valid if all arguments defined by it
* are used within the same fragment.
*
* See https://spec.graphql.org/draft/#sec-All-Variables-Used
*/
export function NoUnusedFragmentArgumentsRule(
context: ValidationContext,
): ASTVisitor {
return {
FragmentDefinition(fragment) {
const usages = context.getVariableUsages(fragment);
const argumentNameUsed = new Set<string>(
usages.map(({ node }) => node.name.value),
);
// FIXME: https://github.com/graphql/graphql-js/issues/2203
/* c8 ignore next */
const argumentDefinitions = fragment.arguments ?? [];
for (const argDef of argumentDefinitions) {
const argName = argDef.variable.name.value;
if (!argumentNameUsed.has(argName)) {
context.reportError(
new GraphQLError(
`Argument "$${argName}" is never used in fragment "${fragment.name.value}".`,
{ nodes: argDef },
),
);
}
}
},
};
}
5 changes: 4 additions & 1 deletion src/validation/rules/NoUnusedVariablesRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ export function NoUnusedVariablesRule(context: ValidationContext): ASTVisitor {
OperationDefinition(operation) {
const usages = context.getRecursiveVariableUsages(operation);
const variableNameUsed = new Set<string>(
usages.map(({ node }) => node.name.value),
usages
// Skip variables used as fragment arguments
.filter(({ fragmentArgDef }) => !fragmentArgDef)
.map(({ node }) => node.name.value),
);

// FIXME: https://github.com/graphql/graphql-js/issues/2203
Expand Down
Loading

0 comments on commit 686fe56

Please sign in to comment.