Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ export type {
SelectionNode,
FieldNode,
ArgumentNode,
FragmentArgumentNode,
NullabilityAssertionNode,
NonNullAssertionNode,
ErrorBoundaryNode,
Expand Down
25 changes: 22 additions & 3 deletions src/language/__tests__/parser-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,13 +607,32 @@ describe('Parser', () => {
expect('loc' in result).to.equal(false);
});

it('Legacy: allows parsing fragment defined variables', () => {
it('allows parsing fragment defined variables', () => {
const document = 'fragment a($v: Boolean = false) on t { f(v: $v) }';

expect(() =>
parse(document, { allowLegacyFragmentVariables: true }),
parse(document, { experimentalFragmentArguments: true }),
).to.not.throw();
expect(() => parse(document)).to.throw('Syntax Error');
});

it('disallows parsing fragment defined variables without experimental flag', () => {
const document = 'fragment a($v: Boolean = false) on t { f(v: $v) }';

expect(() => parse(document)).to.throw();
});

it('allows parsing fragment spread arguments', () => {
const document = 'fragment a on t { ...b(v: $v) }';

expect(() =>
parse(document, { experimentalFragmentArguments: true }),
).to.not.throw();
});

it('disallows parsing fragment spread arguments without experimental flag', () => {
const document = 'fragment a on t { ...b(v: $v) }';

expect(() => parse(document)).to.throw();
});

it('contains location that can be Object.toStringified, JSON.stringified, or jsutils.inspected', () => {
Expand Down
44 changes: 36 additions & 8 deletions src/language/__tests__/printer-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,34 +168,62 @@ describe('Printer: Query document', () => {
`);
});

it('Legacy: prints fragment with variable directives', () => {
const queryASTWithVariableDirective = parse(
it('prints fragment with argument definition directives', () => {
const fragmentWithArgumentDefinitionDirective = parse(
'fragment Foo($foo: TestType @test) on TestType @testDirective { id }',
{ allowLegacyFragmentVariables: true },
{ experimentalFragmentArguments: true },
);
expect(print(queryASTWithVariableDirective)).to.equal(dedent`
expect(print(fragmentWithArgumentDefinitionDirective)).to.equal(dedent`
fragment Foo($foo: TestType @test) on TestType @testDirective {
id
}
`);
});

it('Legacy: correctly prints fragment defined variables', () => {
const fragmentWithVariable = parse(
it('correctly prints fragment defined arguments', () => {
const fragmentWithArgumentDefinition = parse(
`
fragment Foo($a: ComplexType, $b: Boolean = false) on TestType {
id
}
`,
{ allowLegacyFragmentVariables: true },
{ experimentalFragmentArguments: true },
);
expect(print(fragmentWithVariable)).to.equal(dedent`
expect(print(fragmentWithArgumentDefinition)).to.equal(dedent`
fragment Foo($a: ComplexType, $b: Boolean = false) on TestType {
id
}
`);
});

it('prints fragment spread with arguments', () => {
const fragmentSpreadWithArguments = parse(
'fragment Foo on TestType { ...Bar(a: {x: $x}, b: true) }',
{ experimentalFragmentArguments: true },
);
expect(print(fragmentSpreadWithArguments)).to.equal(dedent`
fragment Foo on TestType {
...Bar(a: { x: $x }, b: true)
}
`);
});

it('prints fragment spread with multi-line arguments', () => {
const fragmentSpreadWithArguments = parse(
'fragment Foo on TestType { ...Bar(a: {x: $x, y: $y, z: $z, xy: $xy}, b: true, c: "a long string extending arguments over max length") }',
{ experimentalFragmentArguments: true },
);
expect(print(fragmentSpreadWithArguments)).to.equal(dedent`
fragment Foo on TestType {
...Bar(
a: { x: $x, y: $y, z: $z, xy: $xy }
b: true
c: "a long string extending arguments over max length"
)
}
`);
});

it('prints kitchen sink without altering ast', () => {
const ast = parse(kitchenSinkQuery, {
noLocation: true,
Expand Down
48 changes: 46 additions & 2 deletions src/language/__tests__/visitor-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,10 +455,10 @@ describe('Visitor', () => {
]);
});

it('Legacy: visits variables defined in fragments', () => {
it('visits arguments defined on fragments', () => {
const ast = parse('fragment a($v: Boolean = false) on t { f }', {
noLocation: true,
allowLegacyFragmentVariables: true,
experimentalFragmentArguments: true,
});
const visited: Array<any> = [];

Expand Down Expand Up @@ -505,6 +505,50 @@ describe('Visitor', () => {
]);
});

it('visits arguments on fragment spreads', () => {
const ast = parse('fragment a on t { ...s(v: false) }', {
noLocation: true,
experimentalFragmentArguments: true,
});
const visited: Array<any> = [];

visit(ast, {
enter(node) {
checkVisitorFnArgs(ast, arguments);
visited.push(['enter', node.kind, getValue(node)]);
},
leave(node) {
checkVisitorFnArgs(ast, arguments);
visited.push(['leave', node.kind, getValue(node)]);
},
});

expect(visited).to.deep.equal([
['enter', 'Document', undefined],
['enter', 'FragmentDefinition', undefined],
['enter', 'Name', 'a'],
['leave', 'Name', 'a'],
['enter', 'NamedType', undefined],
['enter', 'Name', 't'],
['leave', 'Name', 't'],
['leave', 'NamedType', undefined],
['enter', 'SelectionSet', undefined],
['enter', 'FragmentSpread', undefined],
['enter', 'Name', 's'],
['leave', 'Name', 's'],
['enter', 'FragmentArgument', { kind: 'BooleanValue', value: false }],
['enter', 'Name', 'v'],
['leave', 'Name', 'v'],
['enter', 'BooleanValue', false],
['leave', 'BooleanValue', false],
['leave', 'FragmentArgument', { kind: 'BooleanValue', value: false }],
['leave', 'FragmentSpread', undefined],
['leave', 'SelectionSet', undefined],
['leave', 'FragmentDefinition', undefined],
['leave', 'Document', undefined],
]);
});

it('properly visits the kitchen sink query', () => {
const ast = parse(kitchenSinkQuery, {
experimentalClientControlledNullability: true,
Expand Down
13 changes: 11 additions & 2 deletions src/language/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export type ASTNode =
| SelectionSetNode
| FieldNode
| ArgumentNode
| FragmentArgumentNode
| FragmentSpreadNode
| InlineFragmentNode
| FragmentDefinitionNode
Expand Down Expand Up @@ -221,13 +222,14 @@ export const QueryDocumentKeys: {
'nullabilityAssertion',
],
Argument: ['name', 'value'],
FragmentArgument: ['name', 'value'],
// Note: Client Controlled Nullability is experimental and may be changed
// or removed in the future.
ListNullabilityOperator: ['nullabilityAssertion'],
NonNullAssertion: ['nullabilityAssertion'],
ErrorBoundary: ['nullabilityAssertion'],

FragmentSpread: ['name', 'directives'],
FragmentSpread: ['name', 'arguments', 'directives'],
InlineFragment: ['typeCondition', 'directives', 'selectionSet'],
FragmentDefinition: [
'name',
Expand Down Expand Up @@ -422,12 +424,20 @@ export interface ConstArgumentNode {
readonly value: ConstValueNode;
}

export interface FragmentArgumentNode {
readonly kind: Kind.FRAGMENT_ARGUMENT;
readonly loc?: Location | undefined;
readonly name: NameNode;
readonly value: ValueNode;
}

/** Fragments */

export interface FragmentSpreadNode {
readonly kind: Kind.FRAGMENT_SPREAD;
readonly loc?: Location | undefined;
readonly name: NameNode;
readonly arguments?: ReadonlyArray<FragmentArgumentNode> | undefined;
readonly directives?: ReadonlyArray<DirectiveNode> | undefined;
}

Expand All @@ -443,7 +453,6 @@ export interface FragmentDefinitionNode {
readonly kind: Kind.FRAGMENT_DEFINITION;
readonly loc?: Location | undefined;
readonly name: NameNode;
/** @deprecated variableDefinitions will be removed in v17.0.0 */
readonly variableDefinitions?:
| ReadonlyArray<VariableDefinitionNode>
| undefined;
Expand Down
1 change: 1 addition & 0 deletions src/language/directiveLocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ export enum DirectiveLocation {
ENUM_VALUE = 'ENUM_VALUE',
INPUT_OBJECT = 'INPUT_OBJECT',
INPUT_FIELD_DEFINITION = 'INPUT_FIELD_DEFINITION',
FRAGMENT_VARIABLE_DEFINITION = 'FRAGMENT_VARIABLE_DEFINITION',
}
1 change: 1 addition & 0 deletions src/language/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type {
ErrorBoundaryNode,
ListNullabilityOperatorNode,
ArgumentNode,
FragmentArgumentNode /* for experimental fragment arguments */,
ConstArgumentNode,
FragmentSpreadNode,
InlineFragmentNode,
Expand Down
1 change: 1 addition & 0 deletions src/language/kinds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export enum Kind {
SELECTION_SET = 'SelectionSet',
FIELD = 'Field',
ARGUMENT = 'Argument',
FRAGMENT_ARGUMENT = 'FragmentArgument',

/** Nullability Modifiers */
LIST_NULLABILITY_OPERATOR = 'ListNullabilityOperator',
Expand Down
61 changes: 47 additions & 14 deletions src/language/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
FieldDefinitionNode,
FieldNode,
FloatValueNode,
FragmentArgumentNode,
FragmentDefinitionNode,
FragmentSpreadNode,
InlineFragmentNode,
Expand Down Expand Up @@ -92,21 +93,25 @@ export interface ParseOptions {
maxTokens?: number | undefined;

/**
* @deprecated will be removed in the v17.0.0
* EXPERIMENTAL:
*
* If enabled, the parser will understand and parse variable definitions
* contained in a fragment definition. They'll be represented in the
* `variableDefinitions` field of the FragmentDefinitionNode.
* If enabled, the parser will understand and parse fragment variable definitions
* and arguments on fragment spreads. Fragment variable definitions will be represented
* in the `variableDefinitions` field of the FragmentDefinitionNode.
* Fragment spread arguments will be represented in the `arguments` field of FragmentSpreadNode.
*
* The syntax is identical to normal, query-defined variables. For example:
* For example:
*
* ```graphql
* {
* t { ...A(var: true) }
* }
* fragment A($var: Boolean = false) on T {
* ...
* ...B(x: $var)
* }
* ```
*/
allowLegacyFragmentVariables?: boolean | undefined;
experimentalFragmentArguments?: boolean | undefined;

/**
* EXPERIMENTAL:
Expand Down Expand Up @@ -524,6 +529,12 @@ export class Parser {
return this.optionalMany(TokenKind.PAREN_L, item, TokenKind.PAREN_R);
}

/* experimental */
parseFragmentArguments(): Array<FragmentArgumentNode> {
const item = this.parseFragmentArgument;
return this.optionalMany(TokenKind.PAREN_L, item, TokenKind.PAREN_R);
}

/**
* Argument[Const] : Name : Value[?Const]
*/
Expand All @@ -545,12 +556,25 @@ export class Parser {
return this.parseArgument(true);
}

/* experimental */
parseFragmentArgument(): FragmentArgumentNode {
const start = this._lexer.token;
const name = this.parseName();

this.expectToken(TokenKind.COLON);
return this.node<FragmentArgumentNode>(start, {
kind: Kind.FRAGMENT_ARGUMENT,
name,
value: this.parseValueLiteral(false),
});
}

// Implements the parsing rules in the Fragments section.

/**
* Corresponds to both FragmentSpread and InlineFragment in the spec.
*
* FragmentSpread : ... FragmentName Directives?
* FragmentSpread : ... FragmentName Arguments? Directives?
*
* InlineFragment : ... TypeCondition? Directives? SelectionSet
*/
Expand All @@ -560,9 +584,21 @@ export class Parser {

const hasTypeCondition = this.expectOptionalKeyword('on');
if (!hasTypeCondition && this.peek(TokenKind.NAME)) {
const name = this.parseFragmentName();
if (
this.peek(TokenKind.PAREN_L) &&
this._options.experimentalFragmentArguments
) {
return this.node<FragmentSpreadNode>(start, {
kind: Kind.FRAGMENT_SPREAD,
name,
arguments: this.parseFragmentArguments(),
directives: this.parseDirectives(false),
});
}
return this.node<FragmentSpreadNode>(start, {
kind: Kind.FRAGMENT_SPREAD,
name: this.parseFragmentName(),
name,
directives: this.parseDirectives(false),
});
}
Expand All @@ -576,17 +612,14 @@ export class Parser {

/**
* FragmentDefinition :
* - fragment FragmentName on TypeCondition Directives? SelectionSet
* - fragment FragmentName VariableDefinitions? on TypeCondition Directives? SelectionSet
*
* TypeCondition : NamedType
*/
parseFragmentDefinition(): FragmentDefinitionNode {
const start = this._lexer.token;
this.expectKeyword('fragment');
// Legacy support for defining variables within fragments changes
// the grammar of FragmentDefinition:
// - fragment FragmentName VariableDefinitions? on TypeCondition Directives? SelectionSet
if (this._options.allowLegacyFragmentVariables === true) {
if (this._options.experimentalFragmentArguments === true) {
return this.node<FragmentDefinitionNode>(start, {
kind: Kind.FRAGMENT_DEFINITION,
name: this.parseFragmentName(),
Expand Down
Loading
Loading