Skip to content

Commit ad9c519

Browse files
BoDmartinbonninjerelmilleryaacovCR
authored
Add support for directives on directive definitions (#4521)
Allow directives on directive definitions, based on [this spec PR](graphql/graphql-spec#1206) which introduces this syntax: ```graphql directive @onDirective on DIRECTIVE_DEFINITION directive @foo @onDirective on OBJECT directive @baz @deprecated(reason: "...") on OBJECT extend directive @quux @deprecated(reason: "...") ``` Co-authored-by: Martin Bonnin <martin@mbonnin.net> Co-authored-by: Jerel Miller <jerelmiller@gmail.com> Co-authored-by: Yaacov Rydzinski <yaacovCR@gmail.com>
1 parent db2987c commit ad9c519

31 files changed

Lines changed: 772 additions & 17 deletions

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ export type {
312312
UnionTypeExtensionNode,
313313
EnumTypeExtensionNode,
314314
InputObjectTypeExtensionNode,
315+
DirectiveExtensionNode,
315316
// Schema Coordinates
316317
SchemaCoordinateNode,
317318
TypeCoordinateNode,

src/language/__tests__/predicates-test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ describe('AST node predicates', () => {
3939
'InputObjectTypeDefinition',
4040
'DirectiveDefinition',
4141
'SchemaExtension',
42+
'DirectiveExtension',
4243
'ScalarTypeExtension',
4344
'ObjectTypeExtension',
4445
'InterfaceTypeExtension',
@@ -123,6 +124,7 @@ describe('AST node predicates', () => {
123124
it('isTypeSystemExtensionNode', () => {
124125
expect(filterNodes(isTypeSystemExtensionNode)).to.deep.equal([
125126
'SchemaExtension',
127+
'DirectiveExtension',
126128
'ScalarTypeExtension',
127129
'ObjectTypeExtension',
128130
'InterfaceTypeExtension',

src/language/__tests__/printer-test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,21 @@ describe('Printer: Query document', () => {
122122
`);
123123
});
124124

125+
it('Experimental: prints directives on directives', () => {
126+
const queryASTWithVariableDirective = parse(
127+
`
128+
directive @foo @bar on FIELD_DEFINITION
129+
extend directive @foo @baz
130+
`,
131+
{ experimentalDirectivesOnDirectiveDefinitions: true },
132+
);
133+
expect(print(queryASTWithVariableDirective)).to.equal(dedent`
134+
directive @foo @bar on FIELD_DEFINITION
135+
136+
extend directive @foo @baz
137+
`);
138+
});
139+
125140
it('Legacy: correctly prints fragment defined variables', () => {
126141
const fragmentWithVariable = parse(
127142
`

src/language/__tests__/schema-parser-test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,6 +1029,7 @@ input Hello {
10291029
{
10301030
kind: 'DirectiveDefinition',
10311031
description: undefined,
1032+
directives: [],
10321033
name: {
10331034
kind: 'Name',
10341035
value: 'foo',
@@ -1065,6 +1066,7 @@ input Hello {
10651066
{
10661067
kind: 'DirectiveDefinition',
10671068
description: undefined,
1069+
directives: [],
10681070
name: {
10691071
kind: 'Name',
10701072
value: 'foo',

src/language/ast.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ export type ASTNode =
180180
| UnionTypeExtensionNode
181181
| EnumTypeExtensionNode
182182
| InputObjectTypeExtensionNode
183+
| DirectiveExtensionNode
183184
| TypeCoordinateNode
184185
| MemberCoordinateNode
185186
| ArgumentCoordinateNode
@@ -280,10 +281,18 @@ export const QueryDocumentKeys: {
280281
EnumValueDefinition: ['description', 'name', 'directives'],
281282
InputObjectTypeDefinition: ['description', 'name', 'directives', 'fields'],
282283

283-
DirectiveDefinition: ['description', 'name', 'arguments', 'locations'],
284+
DirectiveDefinition: [
285+
'description',
286+
'name',
287+
'arguments',
288+
'directives',
289+
'locations',
290+
],
284291

285292
SchemaExtension: ['directives', 'operationTypes'],
286293

294+
DirectiveExtension: ['name', 'directives'],
295+
287296
ScalarTypeExtension: ['name', 'directives'],
288297
ObjectTypeExtension: ['name', 'interfaces', 'directives', 'fields'],
289298
InterfaceTypeExtension: ['name', 'interfaces', 'directives', 'fields'],
@@ -686,13 +695,17 @@ export interface DirectiveDefinitionNode {
686695
readonly description?: StringValueNode;
687696
readonly name: NameNode;
688697
readonly arguments?: ReadonlyArray<InputValueDefinitionNode>;
698+
readonly directives?: ReadonlyArray<ConstDirectiveNode>;
689699
readonly repeatable: boolean;
690700
readonly locations: ReadonlyArray<NameNode>;
691701
}
692702

693703
/** Type System Extensions */
694704

695-
export type TypeSystemExtensionNode = SchemaExtensionNode | TypeExtensionNode;
705+
export type TypeSystemExtensionNode =
706+
| SchemaExtensionNode
707+
| TypeExtensionNode
708+
| DirectiveExtensionNode;
696709

697710
export interface SchemaExtensionNode {
698711
readonly kind: Kind.SCHEMA_EXTENSION;
@@ -760,6 +773,13 @@ export interface InputObjectTypeExtensionNode {
760773
readonly fields?: ReadonlyArray<InputValueDefinitionNode>;
761774
}
762775

776+
export interface DirectiveExtensionNode {
777+
readonly kind: Kind.DIRECTIVE_EXTENSION;
778+
readonly loc?: Location;
779+
readonly name: NameNode;
780+
readonly directives?: ReadonlyArray<ConstDirectiveNode>;
781+
}
782+
763783
/** Schema Coordinates */
764784

765785
export type SchemaCoordinateNode =

src/language/directiveLocation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ enum DirectiveLocation {
2323
ENUM_VALUE = 'ENUM_VALUE',
2424
INPUT_OBJECT = 'INPUT_OBJECT',
2525
INPUT_FIELD_DEFINITION = 'INPUT_FIELD_DEFINITION',
26+
DIRECTIVE_DEFINITION = 'DIRECTIVE_DEFINITION',
2627
}
2728
export { DirectiveLocation };
2829

src/language/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export type {
9696
UnionTypeExtensionNode,
9797
EnumTypeExtensionNode,
9898
InputObjectTypeExtensionNode,
99+
DirectiveExtensionNode,
99100
// Schema Coordinates
100101
SchemaCoordinateNode,
101102
TypeCoordinateNode,

src/language/kinds.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ enum Kind {
5858

5959
/** Type System Extensions */
6060
SCHEMA_EXTENSION = 'SchemaExtension',
61+
DIRECTIVE_EXTENSION = 'DirectiveExtension',
6162

6263
/** Type Extensions */
6364
SCALAR_TYPE_EXTENSION = 'ScalarTypeExtension',

src/language/parser.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
DirectiveArgumentCoordinateNode,
1818
DirectiveCoordinateNode,
1919
DirectiveDefinitionNode,
20+
DirectiveExtensionNode,
2021
DirectiveNode,
2122
DocumentNode,
2223
EnumTypeDefinitionNode,
@@ -112,6 +113,18 @@ export interface ParseOptions {
112113
*/
113114
allowLegacyFragmentVariables?: boolean;
114115

116+
/**
117+
* EXPERIMENTAL:
118+
*
119+
* If enabled, the parser will parse directives on directive definitions.
120+
* This syntax is not part of the GraphQL specification and may change.
121+
*
122+
* ```graphql
123+
* directive @foo @bar on FIELD
124+
* ```
125+
*/
126+
experimentalDirectivesOnDirectiveDefinitions?: boolean;
127+
115128
/**
116129
* You may override the Lexer class used to lex the source; this is used by
117130
* schema coordinates to introduce a lexer with a restricted syntax.
@@ -1184,6 +1197,7 @@ export class Parser {
11841197
* - UnionTypeExtension
11851198
* - EnumTypeExtension
11861199
* - InputObjectTypeDefinition
1200+
* - DirectiveDefinitionExtension
11871201
*/
11881202
parseTypeSystemExtension(): TypeSystemExtensionNode {
11891203
const keywordToken = this._lexer.lookahead();
@@ -1204,6 +1218,11 @@ export class Parser {
12041218
return this.parseEnumTypeExtension();
12051219
case 'input':
12061220
return this.parseInputObjectTypeExtension();
1221+
case 'directive':
1222+
if (this._options.experimentalDirectivesOnDirectiveDefinitions) {
1223+
return this.parseDirectiveDefinitionExtension();
1224+
}
1225+
break;
12071226
}
12081227
}
12091228

@@ -1386,6 +1405,23 @@ export class Parser {
13861405
});
13871406
}
13881407

1408+
parseDirectiveDefinitionExtension(): DirectiveExtensionNode {
1409+
const start = this._lexer.token;
1410+
this.expectKeyword('extend');
1411+
this.expectKeyword('directive');
1412+
this.expectToken(TokenKind.AT);
1413+
const name = this.parseName();
1414+
const directives = this.parseConstDirectives();
1415+
if (directives.length === 0) {
1416+
throw this.unexpected();
1417+
}
1418+
return this.node<DirectiveExtensionNode>(start, {
1419+
kind: Kind.DIRECTIVE_EXTENSION,
1420+
name,
1421+
directives,
1422+
});
1423+
}
1424+
13891425
/**
13901426
* ```
13911427
* DirectiveDefinition :
@@ -1399,6 +1435,10 @@ export class Parser {
13991435
this.expectToken(TokenKind.AT);
14001436
const name = this.parseName();
14011437
const args = this.parseArgumentDefs();
1438+
const directives = this._options
1439+
.experimentalDirectivesOnDirectiveDefinitions
1440+
? this.parseConstDirectives()
1441+
: [];
14021442
const repeatable = this.expectOptionalKeyword('repeatable');
14031443
this.expectKeyword('on');
14041444
const locations = this.parseDirectiveLocations();
@@ -1407,6 +1447,7 @@ export class Parser {
14071447
description,
14081448
name,
14091449
arguments: args,
1450+
directives,
14101451
repeatable,
14111452
locations,
14121453
});
@@ -1447,6 +1488,7 @@ export class Parser {
14471488
* `ENUM_VALUE`
14481489
* `INPUT_OBJECT`
14491490
* `INPUT_FIELD_DEFINITION`
1491+
* `DIRECTIVE_DEFINITION`
14501492
*/
14511493
parseDirectiveLocation(): NameNode {
14521494
const start = this._lexer.token;

src/language/predicates.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,11 @@ export function isTypeDefinitionNode(
9898
export function isTypeSystemExtensionNode(
9999
node: ASTNode,
100100
): node is TypeSystemExtensionNode {
101-
return node.kind === Kind.SCHEMA_EXTENSION || isTypeExtensionNode(node);
101+
return (
102+
node.kind === Kind.SCHEMA_EXTENSION ||
103+
node.kind === Kind.DIRECTIVE_EXTENSION ||
104+
isTypeExtensionNode(node)
105+
);
102106
}
103107

104108
export function isTypeExtensionNode(node: ASTNode): node is TypeExtensionNode {

0 commit comments

Comments
 (0)