Skip to content

Commit

Permalink
Implement validation
Browse files Browse the repository at this point in the history
  • Loading branch information
JoviDeCroock committed Aug 8, 2024
1 parent aba2bbb commit 97b9a8f
Show file tree
Hide file tree
Showing 17 changed files with 832 additions and 104 deletions.
30 changes: 28 additions & 2 deletions src/validation/ValidationContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
FragmentSpreadNode,
OperationDefinitionNode,
SelectionSetNode,
VariableDefinitionNode,
VariableNode,
} from '../language/ast';
import { Kind } from '../language/kinds';
Expand All @@ -26,13 +27,15 @@ import type {
import type { GraphQLDirective } from '../type/directives';
import type { GraphQLSchema } from '../type/schema';

import { TypeInfo, visitWithTypeInfo } from '../utilities/TypeInfo';
import type { TypeInfo } from '../utilities/TypeInfo';
import { visitWithTypeInfo } from '../utilities/TypeInfo';

type NodeWithSelectionSet = OperationDefinitionNode | FragmentDefinitionNode;
interface VariableUsage {
readonly node: VariableNode;
readonly type: Maybe<GraphQLInputType>;
readonly defaultValue: Maybe<unknown>;
readonly fragmentVarDef: Maybe<VariableDefinitionNode>;
}

/**
Expand Down Expand Up @@ -199,16 +202,25 @@ export class ValidationContext extends ASTValidationContext {
let usages = this._variableUsages.get(node);
if (!usages) {
const newUsages: Array<VariableUsage> = [];
const typeInfo = new TypeInfo(this._schema);
const typeInfo = this._typeInfo;
const fragmentVariableDefinitions =
node.kind === Kind.FRAGMENT_DEFINITION
? node.variableDefinitions
: undefined;

visit(
node,
visitWithTypeInfo(typeInfo, {
VariableDefinition: () => false,
Variable(variable) {
const fragmentVarDef = fragmentVariableDefinitions?.find(
(varDef) => varDef.variable.name.value === variable.name.value,
);
newUsages.push({
node: variable,
type: typeInfo.getInputType(),
defaultValue: typeInfo.getDefaultValue(),
fragmentVarDef,
});
},
}),
Expand All @@ -233,6 +245,20 @@ export class ValidationContext extends ASTValidationContext {
return usages;
}

getOperationVariableUsages(
operation: OperationDefinitionNode,
): ReadonlyArray<VariableUsage> {
let usages = this._recursiveVariableUsages.get(operation);
if (!usages) {
usages = this.getVariableUsages(operation);
for (const frag of this.getRecursivelyReferencedFragments(operation)) {
usages = usages.concat(this.getVariableUsages(frag));
}
this._recursiveVariableUsages.set(operation, usages);
}
return usages.filter(({ fragmentVarDef }) => !fragmentVarDef);
}

getType(): Maybe<GraphQLOutputType> {
return this._typeInfo.getType();
}
Expand Down
63 changes: 63 additions & 0 deletions src/validation/__tests__/NoUndefinedVariablesRule-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,4 +404,67 @@ describe('Validate: No undefined variables', () => {
},
]);
});

it('fragment defined arguments are not undefined variables', () => {
expectValid(`
query Foo {
...FragA
}
fragment FragA($a: String) on Type {
field1(a: $a)
}
`);
});

it('defined variables used as fragment arguments are not undefined variables', () => {
expectValid(`
query Foo($b: String) {
...FragA(a: $b)
}
fragment FragA($a: String) on Type {
field1
}
`);
});

it('variables used as fragment arguments may be undefined variables', () => {
expectErrors(`
query Foo {
...FragA(a: $a)
}
fragment FragA($a: String) on Type {
field1
}
`).toDeepEqual([
{
message: 'Variable "$a" is not defined by operation "Foo".',
locations: [
{ line: 3, column: 21 },
{ line: 2, column: 7 },
],
},
]);
});

it('variables shadowed by parent fragment arguments are still undefined variables', () => {
expectErrors(`
query Foo {
...FragA
}
fragment FragA($a: String) on Type {
...FragB
}
fragment FragB on Type {
field1(a: $a)
}
`).toDeepEqual([
{
message: 'Variable "$a" is not defined by operation "Foo".',
locations: [
{ line: 9, column: 19 },
{ line: 2, column: 7 },
],
},
]);
});
});
37 changes: 37 additions & 0 deletions src/validation/__tests__/NoUnusedFragmentVariablesRule-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, it } from 'mocha';

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

import { expectValidationErrors } from './harness';

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

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

describe('Validate: No unused variables', () => {
it('fragment defined arguments are not unused variables', () => {
expectValid(`
query Foo {
...FragA
}
fragment FragA($a: String) on Type {
field1(a: $a)
}
`);
});

it('defined variables used as fragment arguments are not unused variables', () => {
expectErrors(`
query Foo($b: String) {
...FragA(a: $b)
}
fragment FragA($a: String) on Type {
field1
}
`);
});
});
22 changes: 22 additions & 0 deletions src/validation/__tests__/NoUnusedVariablesRule-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,4 +230,26 @@ describe('Validate: No unused variables', () => {
},
]);
});

it('fragment defined arguments are not unused variables', () => {
expectValid(`
query Foo {
...FragA
}
fragment FragA($a: String) on Type {
field1(a: $a)
}
`);
});

it('defined variables used as fragment arguments are not unused variables', () => {
expectValid(`
query Foo($b: String) {
...FragA(a: $b)
}
fragment FragA($a: String) on Type {
field1
}
`);
});
});
132 changes: 132 additions & 0 deletions src/validation/__tests__/OverlappingFieldsCanBeMergedRule-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1138,4 +1138,136 @@ describe('Validate: Overlapping fields can be merged', () => {
},
]);
});

describe('fragment arguments must produce fields that can be merged', () => {
it('allows conflicting spreads at different depths', () => {
expectValid(`
query ValidDifferingFragmentArgs($command1: DogCommand, $command2: DogCommand) {
dog {
...DoesKnowCommand(command: $command1)
mother {
...DoesKnowCommand(command: $command2)
}
}
}
fragment DoesKnowCommand($command: DogCommand) on Dog {
doesKnowCommand(dogCommand: $command)
}
`);
});

it('encounters conflict in fragments', () => {
expectErrors(`
{
...WithArgs(x: 3)
...WithArgs(x: 4)
}
fragment WithArgs($x: Int) on Type {
a(x: $x)
}
`).toDeepEqual([
{
message:
'Spreads "WithArgs" conflict because WithArgs(x: 3) and WithArgs(x: 4) have different fragment arguments.',
locations: [
{ line: 3, column: 11 },
{ line: 4, column: 11 },
],
},
]);
});

it('encounters conflict in fragment - field no args', () => {
expectErrors(`
query ($y: Int = 1) {
a(x: $y)
...WithArgs
}
fragment WithArgs on Type {
a(x: $y)
}
`).toDeepEqual([]);
});

it('encounters conflict in fragment/field', () => {
expectErrors(`
query ($y: Int = 1) {
a(x: $y)
...WithArgs(x: $y)
}
fragment WithArgs($x: Int) on Type {
a(x: $x)
}
`).toDeepEqual([]);
});

// This is currently not validated, should we?
it('encounters nested field conflict in fragments that could otherwise merge', () => {
expectErrors(`
query ValidDifferingFragmentArgs($command1: DogCommand, $command2: DogCommand) {
dog {
...DoesKnowCommandNested(command: $command1)
mother {
...DoesKnowCommandNested(command: $command2)
}
}
}
fragment DoesKnowCommandNested($command: DogCommand) on Dog {
doesKnowCommand(dogCommand: $command)
mother {
doesKnowCommand(dogCommand: $command)
}
}
`).toDeepEqual([
{
message:
'Fields "mother" conflict because subfields "doesKnowCommand" conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional.',
locations: [
{ line: 5, column: 13 },
{ line: 13, column: 13 },
{ line: 12, column: 11 },
{ line: 11, column: 11 },
],
},
]);
});

it('encounters nested conflict in fragments', () => {
expectErrors(`
{
connection {
edges {
...WithArgs(x: 3)
}
}
...Connection
}
fragment Connection on Type {
connection {
edges {
...WithArgs(x: 4)
}
}
}
fragment WithArgs($x: Int) on Type {
a(x: $x)
}
`).toDeepEqual([
{
message:
'Spreads "WithArgs" conflict because WithArgs(x: 3) and WithArgs(x: 4) have different fragment arguments.',
locations: [
{
column: 15,
line: 5,
},
{
column: 15,
line: 13,
},
],
},
]);
});
});
});
Loading

0 comments on commit 97b9a8f

Please sign in to comment.