Skip to content

Commit b422131

Browse files
Add typeinfo functionality as a run-up to supporting the new validation
rules Co-authored-by: mjmahone <[email protected]>
1 parent 3918bb5 commit b422131

File tree

2 files changed

+291
-7
lines changed

2 files changed

+291
-7
lines changed

src/utilities/TypeInfo.ts

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import type { Maybe } from '../jsutils/Maybe.js';
22

3-
import type { ASTNode, FieldNode } from '../language/ast.js';
3+
import type {
4+
ASTNode,
5+
FieldNode,
6+
FragmentDefinitionNode,
7+
FragmentSpreadNode,
8+
} from '../language/ast.js';
49
import { isNode } from '../language/ast.js';
510
import { Kind } from '../language/kinds.js';
611
import type { ASTVisitor } from '../language/visitor.js';
@@ -31,6 +36,7 @@ import type { GraphQLDirective } from '../type/directives.js';
3136
import type { GraphQLSchema } from '../type/schema.js';
3237

3338
import { typeFromAST } from './typeFromAST.js';
39+
import { valueFromAST } from './valueFromAST.js';
3440

3541
/**
3642
* TypeInfo is a utility class which, given a GraphQL schema, can keep track
@@ -47,6 +53,8 @@ export class TypeInfo {
4753
private _directive: Maybe<GraphQLDirective>;
4854
private _argument: Maybe<GraphQLArgument>;
4955
private _enumValue: Maybe<GraphQLEnumValue>;
56+
private _fragmentSpread: Maybe<FragmentSpreadNode>;
57+
private _fragmentDefinitions: Map<string, FragmentDefinitionNode>;
5058
private _getFieldDef: GetFieldDefFn;
5159

5260
constructor(
@@ -69,6 +77,8 @@ export class TypeInfo {
6977
this._directive = null;
7078
this._argument = null;
7179
this._enumValue = null;
80+
this._fragmentSpread = null;
81+
this._fragmentDefinitions = new Map();
7282
this._getFieldDef = getFieldDefFn ?? getFieldDef;
7383
if (initialType) {
7484
if (isInputType(initialType)) {
@@ -130,6 +140,17 @@ export class TypeInfo {
130140
// checked before continuing since TypeInfo is used as part of validation
131141
// which occurs before guarantees of schema and document validity.
132142
switch (node.kind) {
143+
case Kind.DOCUMENT: {
144+
// A document's fragment definitions are type signatures
145+
// referenced via fragment spreads. Ensure we can use definitions
146+
// before visiting their call sites.
147+
for (const astNode of node.definitions) {
148+
if (astNode.kind === Kind.FRAGMENT_DEFINITION) {
149+
this._fragmentDefinitions.set(astNode.name.value, astNode);
150+
}
151+
}
152+
break;
153+
}
133154
case Kind.SELECTION_SET: {
134155
const namedType: unknown = getNamedType(this.getType());
135156
this._parentTypeStack.push(
@@ -159,6 +180,10 @@ export class TypeInfo {
159180
this._typeStack.push(isObjectType(rootType) ? rootType : undefined);
160181
break;
161182
}
183+
case Kind.FRAGMENT_SPREAD: {
184+
this._fragmentSpread = node;
185+
break;
186+
}
162187
case Kind.INLINE_FRAGMENT:
163188
case Kind.FRAGMENT_DEFINITION: {
164189
const typeConditionAST = node.typeCondition;
@@ -178,15 +203,51 @@ export class TypeInfo {
178203
case Kind.ARGUMENT: {
179204
let argDef;
180205
let argType: unknown;
181-
const fieldOrDirective = this.getDirective() ?? this.getFieldDef();
182-
if (fieldOrDirective) {
183-
argDef = fieldOrDirective.args.find(
184-
(arg) => arg.name === node.name.value,
206+
const directive = this.getDirective();
207+
const fragmentSpread = this._fragmentSpread;
208+
const fieldDef = this.getFieldDef();
209+
if (directive) {
210+
argDef = directive.args.find((arg) => arg.name === node.name.value);
211+
} else if (fragmentSpread) {
212+
const fragmentDef = this._fragmentDefinitions.get(
213+
fragmentSpread.name.value,
185214
);
186-
if (argDef) {
187-
argType = argDef.type;
215+
const fragVarDef = fragmentDef?.variableDefinitions?.find(
216+
(varDef) => varDef.variable.name.value === node.name.value,
217+
);
218+
if (fragVarDef) {
219+
const fragVarType = typeFromAST(schema, fragVarDef.type);
220+
if (isInputType(fragVarType)) {
221+
const fragVarDefault = fragVarDef.defaultValue
222+
? valueFromAST(fragVarDef.defaultValue, fragVarType)
223+
: undefined;
224+
225+
// Minor hack: transform the FragmentArgDef
226+
// into a schema Argument definition, to
227+
// enable visiting identically to field/directive args
228+
const schemaArgDef: GraphQLArgument = {
229+
name: fragVarDef.variable.name.value,
230+
type: fragVarType,
231+
defaultValue: fragVarDefault,
232+
description: undefined,
233+
deprecationReason: undefined,
234+
extensions: {},
235+
astNode: {
236+
...fragVarDef,
237+
kind: Kind.INPUT_VALUE_DEFINITION,
238+
name: fragVarDef.variable.name,
239+
},
240+
};
241+
argDef = schemaArgDef;
242+
}
188243
}
244+
} else if (fieldDef) {
245+
argDef = fieldDef.args.find((arg) => arg.name === node.name.value);
246+
}
247+
if (argDef) {
248+
argType = argDef.type;
189249
}
250+
190251
this._argument = argDef;
191252
this._defaultValueStack.push(argDef ? argDef.defaultValue : undefined);
192253
this._inputTypeStack.push(isInputType(argType) ? argType : undefined);
@@ -236,6 +297,9 @@ export class TypeInfo {
236297

237298
leave(node: ASTNode) {
238299
switch (node.kind) {
300+
case Kind.DOCUMENT:
301+
this._fragmentDefinitions = new Map();
302+
break;
239303
case Kind.SELECTION_SET:
240304
this._parentTypeStack.pop();
241305
break;
@@ -246,6 +310,9 @@ export class TypeInfo {
246310
case Kind.DIRECTIVE:
247311
this._directive = null;
248312
break;
313+
case Kind.FRAGMENT_SPREAD:
314+
this._fragmentSpread = null;
315+
break;
249316
case Kind.OPERATION_DEFINITION:
250317
case Kind.INLINE_FRAGMENT:
251318
case Kind.FRAGMENT_DEFINITION:

src/utilities/__tests__/TypeInfo-test.ts

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,4 +515,221 @@ describe('visitWithTypeInfo', () => {
515515
['leave', 'SelectionSet', null, 'Human', 'Human'],
516516
]);
517517
});
518+
519+
it('supports traversals of fragment arguments', () => {
520+
const typeInfo = new TypeInfo(testSchema);
521+
522+
const ast = parse(
523+
`
524+
query {
525+
...Foo(x: 4)
526+
...Bar
527+
}
528+
fragment Foo(
529+
$x: ID!
530+
) on QueryRoot {
531+
human(id: $x) { name }
532+
}
533+
`,
534+
{ experimentalFragmentArguments: true },
535+
);
536+
537+
const visited: Array<any> = [];
538+
visit(
539+
ast,
540+
visitWithTypeInfo(typeInfo, {
541+
enter(node) {
542+
const type = typeInfo.getType();
543+
const inputType = typeInfo.getInputType();
544+
visited.push([
545+
'enter',
546+
node.kind,
547+
node.kind === 'Name' ? node.value : null,
548+
String(type),
549+
String(inputType),
550+
]);
551+
},
552+
leave(node) {
553+
const type = typeInfo.getType();
554+
const inputType = typeInfo.getInputType();
555+
visited.push([
556+
'leave',
557+
node.kind,
558+
node.kind === 'Name' ? node.value : null,
559+
String(type),
560+
String(inputType),
561+
]);
562+
},
563+
}),
564+
);
565+
566+
expect(visited).to.deep.equal([
567+
['enter', 'Document', null, 'undefined', 'undefined'],
568+
['enter', 'OperationDefinition', null, 'QueryRoot', 'undefined'],
569+
['enter', 'SelectionSet', null, 'QueryRoot', 'undefined'],
570+
['enter', 'FragmentSpread', null, 'QueryRoot', 'undefined'],
571+
['enter', 'Name', 'Foo', 'QueryRoot', 'undefined'],
572+
['leave', 'Name', 'Foo', 'QueryRoot', 'undefined'],
573+
['enter', 'Argument', null, 'QueryRoot', 'ID!'],
574+
['enter', 'Name', 'x', 'QueryRoot', 'ID!'],
575+
['leave', 'Name', 'x', 'QueryRoot', 'ID!'],
576+
['enter', 'IntValue', null, 'QueryRoot', 'ID!'],
577+
['leave', 'IntValue', null, 'QueryRoot', 'ID!'],
578+
['leave', 'Argument', null, 'QueryRoot', 'ID!'],
579+
['leave', 'FragmentSpread', null, 'QueryRoot', 'undefined'],
580+
['enter', 'FragmentSpread', null, 'QueryRoot', 'undefined'],
581+
['enter', 'Name', 'Bar', 'QueryRoot', 'undefined'],
582+
['leave', 'Name', 'Bar', 'QueryRoot', 'undefined'],
583+
['leave', 'FragmentSpread', null, 'QueryRoot', 'undefined'],
584+
['leave', 'SelectionSet', null, 'QueryRoot', 'undefined'],
585+
['leave', 'OperationDefinition', null, 'QueryRoot', 'undefined'],
586+
['enter', 'FragmentDefinition', null, 'QueryRoot', 'undefined'],
587+
['enter', 'Name', 'Foo', 'QueryRoot', 'undefined'],
588+
['leave', 'Name', 'Foo', 'QueryRoot', 'undefined'],
589+
['enter', 'VariableDefinition', null, 'QueryRoot', 'ID!'],
590+
['enter', 'Variable', null, 'QueryRoot', 'ID!'],
591+
['enter', 'Name', 'x', 'QueryRoot', 'ID!'],
592+
['leave', 'Name', 'x', 'QueryRoot', 'ID!'],
593+
['leave', 'Variable', null, 'QueryRoot', 'ID!'],
594+
['enter', 'NonNullType', null, 'QueryRoot', 'ID!'],
595+
['enter', 'NamedType', null, 'QueryRoot', 'ID!'],
596+
['enter', 'Name', 'ID', 'QueryRoot', 'ID!'],
597+
['leave', 'Name', 'ID', 'QueryRoot', 'ID!'],
598+
['leave', 'NamedType', null, 'QueryRoot', 'ID!'],
599+
['leave', 'NonNullType', null, 'QueryRoot', 'ID!'],
600+
['leave', 'VariableDefinition', null, 'QueryRoot', 'ID!'],
601+
['enter', 'NamedType', null, 'QueryRoot', 'undefined'],
602+
['enter', 'Name', 'QueryRoot', 'QueryRoot', 'undefined'],
603+
['leave', 'Name', 'QueryRoot', 'QueryRoot', 'undefined'],
604+
['leave', 'NamedType', null, 'QueryRoot', 'undefined'],
605+
['enter', 'SelectionSet', null, 'QueryRoot', 'undefined'],
606+
['enter', 'Field', null, 'Human', 'undefined'],
607+
['enter', 'Name', 'human', 'Human', 'undefined'],
608+
['leave', 'Name', 'human', 'Human', 'undefined'],
609+
['enter', 'Argument', null, 'Human', 'ID'],
610+
['enter', 'Name', 'id', 'Human', 'ID'],
611+
['leave', 'Name', 'id', 'Human', 'ID'],
612+
['enter', 'Variable', null, 'Human', 'ID'],
613+
['enter', 'Name', 'x', 'Human', 'ID'],
614+
['leave', 'Name', 'x', 'Human', 'ID'],
615+
['leave', 'Variable', null, 'Human', 'ID'],
616+
['leave', 'Argument', null, 'Human', 'ID'],
617+
['enter', 'SelectionSet', null, 'Human', 'undefined'],
618+
['enter', 'Field', null, 'String', 'undefined'],
619+
['enter', 'Name', 'name', 'String', 'undefined'],
620+
['leave', 'Name', 'name', 'String', 'undefined'],
621+
['leave', 'Field', null, 'String', 'undefined'],
622+
['leave', 'SelectionSet', null, 'Human', 'undefined'],
623+
['leave', 'Field', null, 'Human', 'undefined'],
624+
['leave', 'SelectionSet', null, 'QueryRoot', 'undefined'],
625+
['leave', 'FragmentDefinition', null, 'QueryRoot', 'undefined'],
626+
['leave', 'Document', null, 'undefined', 'undefined'],
627+
]);
628+
});
629+
630+
it('supports traversals of fragment arguments with default-value', () => {
631+
const typeInfo = new TypeInfo(testSchema);
632+
633+
const ast = parse(
634+
`
635+
query {
636+
...Foo(x: null)
637+
}
638+
fragment Foo(
639+
$x: ID = 4
640+
) on QueryRoot {
641+
human(id: $x) { name }
642+
}
643+
`,
644+
{ experimentalFragmentArguments: true },
645+
);
646+
647+
const visited: Array<any> = [];
648+
visit(
649+
ast,
650+
visitWithTypeInfo(typeInfo, {
651+
enter(node) {
652+
const type = typeInfo.getType();
653+
const inputType = typeInfo.getInputType();
654+
visited.push([
655+
'enter',
656+
node.kind,
657+
node.kind === 'Name' ? node.value : null,
658+
String(type),
659+
String(inputType),
660+
]);
661+
},
662+
leave(node) {
663+
const type = typeInfo.getType();
664+
const inputType = typeInfo.getInputType();
665+
visited.push([
666+
'leave',
667+
node.kind,
668+
node.kind === 'Name' ? node.value : null,
669+
String(type),
670+
String(inputType),
671+
]);
672+
},
673+
}),
674+
);
675+
676+
expect(visited).to.deep.equal([
677+
['enter', 'Document', null, 'undefined', 'undefined'],
678+
['enter', 'OperationDefinition', null, 'QueryRoot', 'undefined'],
679+
['enter', 'SelectionSet', null, 'QueryRoot', 'undefined'],
680+
['enter', 'FragmentSpread', null, 'QueryRoot', 'undefined'],
681+
['enter', 'Name', 'Foo', 'QueryRoot', 'undefined'],
682+
['leave', 'Name', 'Foo', 'QueryRoot', 'undefined'],
683+
['enter', 'Argument', null, 'QueryRoot', 'ID'],
684+
['enter', 'Name', 'x', 'QueryRoot', 'ID'],
685+
['leave', 'Name', 'x', 'QueryRoot', 'ID'],
686+
['enter', 'NullValue', null, 'QueryRoot', 'ID'],
687+
['leave', 'NullValue', null, 'QueryRoot', 'ID'],
688+
['leave', 'Argument', null, 'QueryRoot', 'ID'],
689+
['leave', 'FragmentSpread', null, 'QueryRoot', 'undefined'],
690+
['leave', 'SelectionSet', null, 'QueryRoot', 'undefined'],
691+
['leave', 'OperationDefinition', null, 'QueryRoot', 'undefined'],
692+
['enter', 'FragmentDefinition', null, 'QueryRoot', 'undefined'],
693+
['enter', 'Name', 'Foo', 'QueryRoot', 'undefined'],
694+
['leave', 'Name', 'Foo', 'QueryRoot', 'undefined'],
695+
['enter', 'VariableDefinition', null, 'QueryRoot', 'ID'],
696+
['enter', 'Variable', null, 'QueryRoot', 'ID'],
697+
['enter', 'Name', 'x', 'QueryRoot', 'ID'],
698+
['leave', 'Name', 'x', 'QueryRoot', 'ID'],
699+
['leave', 'Variable', null, 'QueryRoot', 'ID'],
700+
['enter', 'NamedType', null, 'QueryRoot', 'ID'],
701+
['enter', 'Name', 'ID', 'QueryRoot', 'ID'],
702+
['leave', 'Name', 'ID', 'QueryRoot', 'ID'],
703+
['leave', 'NamedType', null, 'QueryRoot', 'ID'],
704+
['enter', 'IntValue', null, 'QueryRoot', 'ID'],
705+
['leave', 'IntValue', null, 'QueryRoot', 'ID'],
706+
['leave', 'VariableDefinition', null, 'QueryRoot', 'ID'],
707+
['enter', 'NamedType', null, 'QueryRoot', 'undefined'],
708+
['enter', 'Name', 'QueryRoot', 'QueryRoot', 'undefined'],
709+
['leave', 'Name', 'QueryRoot', 'QueryRoot', 'undefined'],
710+
['leave', 'NamedType', null, 'QueryRoot', 'undefined'],
711+
['enter', 'SelectionSet', null, 'QueryRoot', 'undefined'],
712+
['enter', 'Field', null, 'Human', 'undefined'],
713+
['enter', 'Name', 'human', 'Human', 'undefined'],
714+
['leave', 'Name', 'human', 'Human', 'undefined'],
715+
['enter', 'Argument', null, 'Human', 'ID'],
716+
['enter', 'Name', 'id', 'Human', 'ID'],
717+
['leave', 'Name', 'id', 'Human', 'ID'],
718+
['enter', 'Variable', null, 'Human', 'ID'],
719+
['enter', 'Name', 'x', 'Human', 'ID'],
720+
['leave', 'Name', 'x', 'Human', 'ID'],
721+
['leave', 'Variable', null, 'Human', 'ID'],
722+
['leave', 'Argument', null, 'Human', 'ID'],
723+
['enter', 'SelectionSet', null, 'Human', 'undefined'],
724+
['enter', 'Field', null, 'String', 'undefined'],
725+
['enter', 'Name', 'name', 'String', 'undefined'],
726+
['leave', 'Name', 'name', 'String', 'undefined'],
727+
['leave', 'Field', null, 'String', 'undefined'],
728+
['leave', 'SelectionSet', null, 'Human', 'undefined'],
729+
['leave', 'Field', null, 'Human', 'undefined'],
730+
['leave', 'SelectionSet', null, 'QueryRoot', 'undefined'],
731+
['leave', 'FragmentDefinition', null, 'QueryRoot', 'undefined'],
732+
['leave', 'Document', null, 'undefined', 'undefined'],
733+
]);
734+
});
518735
});

0 commit comments

Comments
 (0)