Skip to content

Commit 7819043

Browse files
committed
Add typeinfo
1 parent d82c439 commit 7819043

File tree

2 files changed

+284
-7
lines changed

2 files changed

+284
-7
lines changed

src/utilities/TypeInfo.ts

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import type { Maybe } from '../jsutils/Maybe';
2+
import type { ObjMap } from '../jsutils/ObjMap';
23

3-
import type { ASTNode, FieldNode } from '../language/ast';
4+
import type {
5+
ASTNode,
6+
FieldNode,
7+
FragmentDefinitionNode,
8+
FragmentSpreadNode,
9+
} from '../language/ast';
410
import { isNode } from '../language/ast';
511
import { Kind } from '../language/kinds';
612
import type { ASTVisitor } from '../language/visitor';
@@ -37,6 +43,7 @@ import {
3743
import type { GraphQLSchema } from '../type/schema';
3844

3945
import { typeFromAST } from './typeFromAST';
46+
import { valueFromAST } from './valueFromAST';
4047

4148
/**
4249
* TypeInfo is a utility class which, given a GraphQL schema, can keep track
@@ -53,6 +60,8 @@ export class TypeInfo {
5360
private _directive: Maybe<GraphQLDirective>;
5461
private _argument: Maybe<GraphQLArgument>;
5562
private _enumValue: Maybe<GraphQLEnumValue>;
63+
private _fragmentSpread: Maybe<FragmentSpreadNode>;
64+
private _fragmentDefinitions: ObjMap<FragmentDefinitionNode>;
5665
private _getFieldDef: GetFieldDefFn;
5766

5867
constructor(
@@ -75,6 +84,8 @@ export class TypeInfo {
7584
this._directive = null;
7685
this._argument = null;
7786
this._enumValue = null;
87+
this._fragmentSpread = null;
88+
this._fragmentDefinitions = {};
7889
this._getFieldDef = getFieldDefFn ?? getFieldDef;
7990
if (initialType) {
8091
if (isInputType(initialType)) {
@@ -148,6 +159,17 @@ export class TypeInfo {
148159
// checked before continuing since TypeInfo is used as part of validation
149160
// which occurs before guarantees of schema and document validity.
150161
switch (node.kind) {
162+
case Kind.DOCUMENT: {
163+
// A document's fragment definitions are type signatures
164+
// referenced via fragment spreads. Ensure we can use definitions
165+
// before visiting their call sites.
166+
for (const astNode of node.definitions) {
167+
if (astNode.kind === Kind.FRAGMENT_DEFINITION) {
168+
this._fragmentDefinitions[astNode.name.value] = astNode;
169+
}
170+
}
171+
break;
172+
}
151173
case Kind.SELECTION_SET: {
152174
const namedType: unknown = getNamedType(this.getType());
153175
this._parentTypeStack.push(
@@ -177,6 +199,10 @@ export class TypeInfo {
177199
this._typeStack.push(isObjectType(rootType) ? rootType : undefined);
178200
break;
179201
}
202+
case Kind.FRAGMENT_SPREAD: {
203+
this._fragmentSpread = node;
204+
break;
205+
}
180206
case Kind.INLINE_FRAGMENT:
181207
case Kind.FRAGMENT_DEFINITION: {
182208
const typeConditionAST = node.typeCondition;
@@ -196,15 +222,48 @@ export class TypeInfo {
196222
case Kind.ARGUMENT: {
197223
let argDef;
198224
let argType: unknown;
199-
const fieldOrDirective = this.getDirective() ?? this.getFieldDef();
200-
if (fieldOrDirective) {
201-
argDef = fieldOrDirective.args.find(
202-
(arg) => arg.name === node.name.value,
225+
const directive = this.getDirective();
226+
const fragmentSpread = this._fragmentSpread;
227+
const fieldDef = this.getFieldDef();
228+
if (directive) {
229+
argDef = directive.args.find((arg) => arg.name === node.name.value);
230+
} else if (fragmentSpread) {
231+
const fragmentDef = this._fragmentDefinitions[fragmentSpread.name.value]
232+
const fragVarDef = fragmentDef?.variableDefinitions?.find(
233+
(varDef) => varDef.variable.name.value === node.name.value,
203234
);
204-
if (argDef) {
205-
argType = argDef.type;
235+
236+
if (fragVarDef) {
237+
const fragVarType = typeFromAST(schema, fragVarDef.type);
238+
if (isInputType(fragVarType)) {
239+
const fragVarDefault = fragVarDef.defaultValue
240+
? valueFromAST(fragVarDef.defaultValue, fragVarType)
241+
: undefined;
242+
243+
const schemaArgDef: GraphQLArgument = {
244+
name: fragVarDef.variable.name.value,
245+
type: fragVarType,
246+
defaultValue: fragVarDefault,
247+
description: undefined,
248+
deprecationReason: undefined,
249+
extensions: {},
250+
astNode: {
251+
...fragVarDef,
252+
kind: Kind.INPUT_VALUE_DEFINITION,
253+
name: fragVarDef.variable.name,
254+
},
255+
};
256+
argDef = schemaArgDef;
257+
}
206258
}
259+
} else if (fieldDef) {
260+
argDef = fieldDef.args.find((arg) => arg.name === node.name.value);
261+
}
262+
263+
if (argDef) {
264+
argType = argDef.type;
207265
}
266+
208267
this._argument = argDef;
209268
this._defaultValueStack.push(argDef ? argDef.defaultValue : undefined);
210269
this._inputTypeStack.push(isInputType(argType) ? argType : undefined);
@@ -254,6 +313,9 @@ export class TypeInfo {
254313

255314
leave(node: ASTNode) {
256315
switch (node.kind) {
316+
case Kind.DOCUMENT:
317+
this._fragmentDefinitions = {};
318+
break;
257319
case Kind.SELECTION_SET:
258320
this._parentTypeStack.pop();
259321
break;
@@ -264,6 +326,9 @@ export class TypeInfo {
264326
case Kind.DIRECTIVE:
265327
this._directive = null;
266328
break;
329+
case Kind.FRAGMENT_SPREAD:
330+
this._fragmentSpread = null;
331+
break;
267332
case Kind.OPERATION_DEFINITION:
268333
case Kind.INLINE_FRAGMENT:
269334
case Kind.FRAGMENT_DEFINITION:

src/utilities/__tests__/TypeInfo-test.ts

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,4 +457,216 @@ describe('visitWithTypeInfo', () => {
457457
['leave', 'SelectionSet', null, 'Human', 'Human'],
458458
]);
459459
});
460+
461+
it('supports traversals of fragment arguments', () => {
462+
const typeInfo = new TypeInfo(testSchema);
463+
464+
const ast = parse(
465+
`
466+
query {
467+
...Foo(x: 4)
468+
}
469+
fragment Foo(
470+
$x: ID!
471+
) on QueryRoot {
472+
human(id: $x) { name }
473+
}
474+
`,
475+
{ experimentalFragmentArguments: true },
476+
);
477+
478+
const visited: Array<any> = [];
479+
visit(
480+
ast,
481+
visitWithTypeInfo(typeInfo, {
482+
enter(node) {
483+
const type = typeInfo.getType();
484+
const inputType = typeInfo.getInputType();
485+
visited.push([
486+
'enter',
487+
node.kind,
488+
node.kind === 'Name' ? node.value : null,
489+
String(type),
490+
String(inputType),
491+
]);
492+
},
493+
leave(node) {
494+
const type = typeInfo.getType();
495+
const inputType = typeInfo.getInputType();
496+
visited.push([
497+
'leave',
498+
node.kind,
499+
node.kind === 'Name' ? node.value : null,
500+
String(type),
501+
String(inputType),
502+
]);
503+
},
504+
}),
505+
);
506+
507+
expect(visited).to.deep.equal([
508+
['enter', 'Document', null, 'undefined', 'undefined'],
509+
['enter', 'OperationDefinition', null, 'QueryRoot', 'undefined'],
510+
['enter', 'SelectionSet', null, 'QueryRoot', 'undefined'],
511+
['enter', 'FragmentSpread', null, 'QueryRoot', 'undefined'],
512+
['enter', 'Name', 'Foo', 'QueryRoot', 'undefined'],
513+
['leave', 'Name', 'Foo', 'QueryRoot', 'undefined'],
514+
['enter', 'Argument', null, 'QueryRoot', 'ID!'],
515+
['enter', 'Name', 'x', 'QueryRoot', 'ID!'],
516+
['leave', 'Name', 'x', 'QueryRoot', 'ID!'],
517+
['enter', 'IntValue', null, 'QueryRoot', 'ID!'],
518+
['leave', 'IntValue', null, 'QueryRoot', 'ID!'],
519+
['leave', 'Argument', null, 'QueryRoot', 'ID!'],
520+
['leave', 'FragmentSpread', null, 'QueryRoot', 'undefined'],
521+
['leave', 'SelectionSet', null, 'QueryRoot', 'undefined'],
522+
['leave', 'OperationDefinition', null, 'QueryRoot', 'undefined'],
523+
['enter', 'FragmentDefinition', null, 'QueryRoot', 'undefined'],
524+
['enter', 'Name', 'Foo', 'QueryRoot', 'undefined'],
525+
['leave', 'Name', 'Foo', 'QueryRoot', 'undefined'],
526+
['enter', 'VariableDefinition', null, 'QueryRoot', 'ID!'],
527+
['enter', 'Variable', null, 'QueryRoot', 'ID!'],
528+
['enter', 'Name', 'x', 'QueryRoot', 'ID!'],
529+
['leave', 'Name', 'x', 'QueryRoot', 'ID!'],
530+
['leave', 'Variable', null, 'QueryRoot', 'ID!'],
531+
['enter', 'NonNullType', null, 'QueryRoot', 'ID!'],
532+
['enter', 'NamedType', null, 'QueryRoot', 'ID!'],
533+
['enter', 'Name', 'ID', 'QueryRoot', 'ID!'],
534+
['leave', 'Name', 'ID', 'QueryRoot', 'ID!'],
535+
['leave', 'NamedType', null, 'QueryRoot', 'ID!'],
536+
['leave', 'NonNullType', null, 'QueryRoot', 'ID!'],
537+
['leave', 'VariableDefinition', null, 'QueryRoot', 'ID!'],
538+
['enter', 'NamedType', null, 'QueryRoot', 'undefined'],
539+
['enter', 'Name', 'QueryRoot', 'QueryRoot', 'undefined'],
540+
['leave', 'Name', 'QueryRoot', 'QueryRoot', 'undefined'],
541+
['leave', 'NamedType', null, 'QueryRoot', 'undefined'],
542+
['enter', 'SelectionSet', null, 'QueryRoot', 'undefined'],
543+
['enter', 'Field', null, 'Human', 'undefined'],
544+
['enter', 'Name', 'human', 'Human', 'undefined'],
545+
['leave', 'Name', 'human', 'Human', 'undefined'],
546+
['enter', 'Argument', null, 'Human', 'ID'],
547+
['enter', 'Name', 'id', 'Human', 'ID'],
548+
['leave', 'Name', 'id', 'Human', 'ID'],
549+
['enter', 'Variable', null, 'Human', 'ID'],
550+
['enter', 'Name', 'x', 'Human', 'ID'],
551+
['leave', 'Name', 'x', 'Human', 'ID'],
552+
['leave', 'Variable', null, 'Human', 'ID'],
553+
['leave', 'Argument', null, 'Human', 'ID'],
554+
['enter', 'SelectionSet', null, 'Human', 'undefined'],
555+
['enter', 'Field', null, 'String', 'undefined'],
556+
['enter', 'Name', 'name', 'String', 'undefined'],
557+
['leave', 'Name', 'name', 'String', 'undefined'],
558+
['leave', 'Field', null, 'String', 'undefined'],
559+
['leave', 'SelectionSet', null, 'Human', 'undefined'],
560+
['leave', 'Field', null, 'Human', 'undefined'],
561+
['leave', 'SelectionSet', null, 'QueryRoot', 'undefined'],
562+
['leave', 'FragmentDefinition', null, 'QueryRoot', 'undefined'],
563+
['leave', 'Document', null, 'undefined', 'undefined'],
564+
]);
565+
});
566+
567+
it('supports traversals of fragment arguments with default-value', () => {
568+
const typeInfo = new TypeInfo(testSchema);
569+
570+
const ast = parse(
571+
`
572+
query {
573+
...Foo(x: null)
574+
}
575+
fragment Foo(
576+
$x: ID = 4
577+
) on QueryRoot {
578+
human(id: $x) { name }
579+
}
580+
`,
581+
{ experimentalFragmentArguments: true },
582+
);
583+
584+
const visited: Array<any> = [];
585+
visit(
586+
ast,
587+
visitWithTypeInfo(typeInfo, {
588+
enter(node) {
589+
const type = typeInfo.getType();
590+
const inputType = typeInfo.getInputType();
591+
visited.push([
592+
'enter',
593+
node.kind,
594+
node.kind === 'Name' ? node.value : null,
595+
String(type),
596+
String(inputType),
597+
]);
598+
},
599+
leave(node) {
600+
const type = typeInfo.getType();
601+
const inputType = typeInfo.getInputType();
602+
visited.push([
603+
'leave',
604+
node.kind,
605+
node.kind === 'Name' ? node.value : null,
606+
String(type),
607+
String(inputType),
608+
]);
609+
},
610+
}),
611+
);
612+
613+
expect(visited).to.deep.equal([
614+
['enter', 'Document', null, 'undefined', 'undefined'],
615+
['enter', 'OperationDefinition', null, 'QueryRoot', 'undefined'],
616+
['enter', 'SelectionSet', null, 'QueryRoot', 'undefined'],
617+
['enter', 'FragmentSpread', null, 'QueryRoot', 'undefined'],
618+
['enter', 'Name', 'Foo', 'QueryRoot', 'undefined'],
619+
['leave', 'Name', 'Foo', 'QueryRoot', 'undefined'],
620+
['enter', 'Argument', null, 'QueryRoot', 'ID'],
621+
['enter', 'Name', 'x', 'QueryRoot', 'ID'],
622+
['leave', 'Name', 'x', 'QueryRoot', 'ID'],
623+
['enter', 'NullValue', null, 'QueryRoot', 'ID'],
624+
['leave', 'NullValue', null, 'QueryRoot', 'ID'],
625+
['leave', 'Argument', null, 'QueryRoot', 'ID'],
626+
['leave', 'FragmentSpread', null, 'QueryRoot', 'undefined'],
627+
['leave', 'SelectionSet', null, 'QueryRoot', 'undefined'],
628+
['leave', 'OperationDefinition', null, 'QueryRoot', 'undefined'],
629+
['enter', 'FragmentDefinition', null, 'QueryRoot', 'undefined'],
630+
['enter', 'Name', 'Foo', 'QueryRoot', 'undefined'],
631+
['leave', 'Name', 'Foo', 'QueryRoot', 'undefined'],
632+
['enter', 'VariableDefinition', null, 'QueryRoot', 'ID'],
633+
['enter', 'Variable', null, 'QueryRoot', 'ID'],
634+
['enter', 'Name', 'x', 'QueryRoot', 'ID'],
635+
['leave', 'Name', 'x', 'QueryRoot', 'ID'],
636+
['leave', 'Variable', null, 'QueryRoot', 'ID'],
637+
['enter', 'NamedType', null, 'QueryRoot', 'ID'],
638+
['enter', 'Name', 'ID', 'QueryRoot', 'ID'],
639+
['leave', 'Name', 'ID', 'QueryRoot', 'ID'],
640+
['leave', 'NamedType', null, 'QueryRoot', 'ID'],
641+
['enter', 'IntValue', null, 'QueryRoot', 'ID'],
642+
['leave', 'IntValue', null, 'QueryRoot', 'ID'],
643+
['leave', 'VariableDefinition', null, 'QueryRoot', 'ID'],
644+
['enter', 'NamedType', null, 'QueryRoot', 'undefined'],
645+
['enter', 'Name', 'QueryRoot', 'QueryRoot', 'undefined'],
646+
['leave', 'Name', 'QueryRoot', 'QueryRoot', 'undefined'],
647+
['leave', 'NamedType', null, 'QueryRoot', 'undefined'],
648+
['enter', 'SelectionSet', null, 'QueryRoot', 'undefined'],
649+
['enter', 'Field', null, 'Human', 'undefined'],
650+
['enter', 'Name', 'human', 'Human', 'undefined'],
651+
['leave', 'Name', 'human', 'Human', 'undefined'],
652+
['enter', 'Argument', null, 'Human', 'ID'],
653+
['enter', 'Name', 'id', 'Human', 'ID'],
654+
['leave', 'Name', 'id', 'Human', 'ID'],
655+
['enter', 'Variable', null, 'Human', 'ID'],
656+
['enter', 'Name', 'x', 'Human', 'ID'],
657+
['leave', 'Name', 'x', 'Human', 'ID'],
658+
['leave', 'Variable', null, 'Human', 'ID'],
659+
['leave', 'Argument', null, 'Human', 'ID'],
660+
['enter', 'SelectionSet', null, 'Human', 'undefined'],
661+
['enter', 'Field', null, 'String', 'undefined'],
662+
['enter', 'Name', 'name', 'String', 'undefined'],
663+
['leave', 'Name', 'name', 'String', 'undefined'],
664+
['leave', 'Field', null, 'String', 'undefined'],
665+
['leave', 'SelectionSet', null, 'Human', 'undefined'],
666+
['leave', 'Field', null, 'Human', 'undefined'],
667+
['leave', 'SelectionSet', null, 'QueryRoot', 'undefined'],
668+
['leave', 'FragmentDefinition', null, 'QueryRoot', 'undefined'],
669+
['leave', 'Document', null, 'undefined', 'undefined'],
670+
]);
671+
});
460672
});

0 commit comments

Comments
 (0)