Skip to content

Commit

Permalink
Add typeinfo functionality as a run-up to supporting the new validation
Browse files Browse the repository at this point in the history
rules

Co-authored-by: mjmahone <[email protected]>
  • Loading branch information
JoviDeCroock and mjmahone committed Aug 29, 2024
1 parent c3f01bd commit 2fb5f1d
Show file tree
Hide file tree
Showing 2 changed files with 307 additions and 9 deletions.
99 changes: 90 additions & 9 deletions src/utilities/TypeInfo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { Maybe } from '../jsutils/Maybe.js';

import type { ASTNode, FieldNode } from '../language/ast.js';
import type {
ASTNode,
FieldNode,
FragmentDefinitionNode,
FragmentSpreadNode,
} from '../language/ast.js';
import { isNode } from '../language/ast.js';
import { Kind } from '../language/kinds.js';
import type { ASTVisitor } from '../language/visitor.js';
Expand Down Expand Up @@ -31,6 +36,7 @@ import type { GraphQLDirective } from '../type/directives.js';
import type { GraphQLSchema } from '../type/schema.js';

import { typeFromAST } from './typeFromAST.js';
import { valueFromAST } from './valueFromAST.js';

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

constructor(
Expand All @@ -69,6 +77,8 @@ export class TypeInfo {
this._directive = null;
this._argument = null;
this._enumValue = null;
this._fragmentSpread = null;
this._fragmentDefinitions = new Map();
this._getFieldDef = getFieldDefFn ?? getFieldDef;
if (initialType) {
if (isInputType(initialType)) {
Expand Down Expand Up @@ -130,6 +140,17 @@ export class TypeInfo {
// checked before continuing since TypeInfo is used as part of validation
// which occurs before guarantees of schema and document validity.
switch (node.kind) {
case Kind.DOCUMENT: {
// A document's fragment definitions are type signatures
// referenced via fragment spreads. Ensure we can use definitions
// before visiting their call sites.
for (const astNode of node.definitions) {
if (astNode.kind === Kind.FRAGMENT_DEFINITION) {
this._fragmentDefinitions.set(astNode.name.value, astNode);
}
}
break;
}
case Kind.SELECTION_SET: {
const namedType: unknown = getNamedType(this.getType());
this._parentTypeStack.push(
Expand Down Expand Up @@ -159,6 +180,10 @@ export class TypeInfo {
this._typeStack.push(isObjectType(rootType) ? rootType : undefined);
break;
}
case Kind.FRAGMENT_SPREAD: {
this._fragmentSpread = node;
break;
}
case Kind.INLINE_FRAGMENT:
case Kind.FRAGMENT_DEFINITION: {
const typeConditionAST = node.typeCondition;
Expand All @@ -175,18 +200,67 @@ export class TypeInfo {
);
break;
}
case Kind.ARGUMENT: {
case Kind.FRAGMENT_ARGUMENT: {
let argDef;
let argType: unknown;
const fieldOrDirective = this.getDirective() ?? this.getFieldDef();
if (fieldOrDirective) {
argDef = fieldOrDirective.args.find(
(arg) => arg.name === node.name.value,
);
if (argDef) {
argType = argDef.type;
const fragmentSpread = this._fragmentSpread;

const fragmentDef = this._fragmentDefinitions.get(
fragmentSpread!.name.value,

Check failure on line 209 in src/utilities/TypeInfo.ts

View workflow job for this annotation

GitHub Actions / ci / Lint source files

Forbidden non-null assertion

Check failure on line 209 in src/utilities/TypeInfo.ts

View workflow job for this annotation

GitHub Actions / ci / Lint source files

Forbidden non-null assertion
);
const fragVarDef = fragmentDef?.variableDefinitions?.find(
(varDef) => varDef.variable.name.value === node.name.value,
);
if (fragVarDef) {
const fragVarType = typeFromAST(schema, fragVarDef.type);
if (isInputType(fragVarType)) {
const fragVarDefault = fragVarDef.defaultValue
? valueFromAST(fragVarDef.defaultValue, fragVarType)
: undefined;

// Minor hack: transform the FragmentArgDef
// into a schema Argument definition, to
// enable visiting identically to field/directive args
const schemaArgDef: GraphQLArgument = {
name: fragVarDef.variable.name.value,
type: fragVarType,
defaultValue: fragVarDefault,
description: undefined,
deprecationReason: undefined,
extensions: {},
astNode: {
...fragVarDef,
kind: Kind.INPUT_VALUE_DEFINITION,
name: fragVarDef.variable.name,
},
};
argDef = schemaArgDef;
}
}

if (argDef) {
argType = argDef.type;
}

this._argument = argDef;
this._defaultValueStack.push(argDef ? argDef.defaultValue : undefined);
this._inputTypeStack.push(isInputType(argType) ? argType : undefined);
break;
}
case Kind.ARGUMENT: {
let argDef;
let argType: unknown;
const directive = this.getDirective();
const fieldDef = this.getFieldDef();
if (directive) {
argDef = directive.args.find((arg) => arg.name === node.name.value);
} else if (fieldDef) {
argDef = fieldDef.args.find((arg) => arg.name === node.name.value);
}
if (argDef) {
argType = argDef.type;
}

this._argument = argDef;
this._defaultValueStack.push(argDef ? argDef.defaultValue : undefined);
this._inputTypeStack.push(isInputType(argType) ? argType : undefined);
Expand Down Expand Up @@ -236,6 +310,9 @@ export class TypeInfo {

leave(node: ASTNode) {
switch (node.kind) {
case Kind.DOCUMENT:
this._fragmentDefinitions = new Map();
break;
case Kind.SELECTION_SET:
this._parentTypeStack.pop();
break;
Expand All @@ -246,6 +323,9 @@ export class TypeInfo {
case Kind.DIRECTIVE:
this._directive = null;
break;
case Kind.FRAGMENT_SPREAD:
this._fragmentSpread = null;
break;
case Kind.OPERATION_DEFINITION:
case Kind.INLINE_FRAGMENT:
case Kind.FRAGMENT_DEFINITION:
Expand All @@ -254,6 +334,7 @@ export class TypeInfo {
case Kind.VARIABLE_DEFINITION:
this._inputTypeStack.pop();
break;
case Kind.FRAGMENT_ARGUMENT:
case Kind.ARGUMENT:
this._argument = null;
this._defaultValueStack.pop();
Expand Down
217 changes: 217 additions & 0 deletions src/utilities/__tests__/TypeInfo-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,4 +515,221 @@ describe('visitWithTypeInfo', () => {
['leave', 'SelectionSet', null, 'Human', 'Human'],
]);
});

it('supports traversals of fragment arguments', () => {
const typeInfo = new TypeInfo(testSchema);

const ast = parse(
`
query {
...Foo(x: 4)
...Bar
}
fragment Foo(
$x: ID!
) on QueryRoot {
human(id: $x) { name }
}
`,
{ experimentalFragmentArguments: true },
);

const visited: Array<any> = [];
visit(
ast,
visitWithTypeInfo(typeInfo, {
enter(node) {
const type = typeInfo.getType();
const inputType = typeInfo.getInputType();
visited.push([
'enter',
node.kind,
node.kind === 'Name' ? node.value : null,
String(type),
String(inputType),
]);
},
leave(node) {
const type = typeInfo.getType();
const inputType = typeInfo.getInputType();
visited.push([
'leave',
node.kind,
node.kind === 'Name' ? node.value : null,
String(type),
String(inputType),
]);
},
}),
);

expect(visited).to.deep.equal([
['enter', 'Document', null, 'undefined', 'undefined'],
['enter', 'OperationDefinition', null, 'QueryRoot', 'undefined'],
['enter', 'SelectionSet', null, 'QueryRoot', 'undefined'],
['enter', 'FragmentSpread', null, 'QueryRoot', 'undefined'],
['enter', 'Name', 'Foo', 'QueryRoot', 'undefined'],
['leave', 'Name', 'Foo', 'QueryRoot', 'undefined'],
['enter', 'FragmentArgument', null, 'QueryRoot', 'ID!'],
['enter', 'Name', 'x', 'QueryRoot', 'ID!'],
['leave', 'Name', 'x', 'QueryRoot', 'ID!'],
['enter', 'IntValue', null, 'QueryRoot', 'ID!'],
['leave', 'IntValue', null, 'QueryRoot', 'ID!'],
['leave', 'FragmentArgument', null, 'QueryRoot', 'ID!'],
['leave', 'FragmentSpread', null, 'QueryRoot', 'undefined'],
['enter', 'FragmentSpread', null, 'QueryRoot', 'undefined'],
['enter', 'Name', 'Bar', 'QueryRoot', 'undefined'],
['leave', 'Name', 'Bar', 'QueryRoot', 'undefined'],
['leave', 'FragmentSpread', null, 'QueryRoot', 'undefined'],
['leave', 'SelectionSet', null, 'QueryRoot', 'undefined'],
['leave', 'OperationDefinition', null, 'QueryRoot', 'undefined'],
['enter', 'FragmentDefinition', null, 'QueryRoot', 'undefined'],
['enter', 'Name', 'Foo', 'QueryRoot', 'undefined'],
['leave', 'Name', 'Foo', 'QueryRoot', 'undefined'],
['enter', 'VariableDefinition', null, 'QueryRoot', 'ID!'],
['enter', 'Variable', null, 'QueryRoot', 'ID!'],
['enter', 'Name', 'x', 'QueryRoot', 'ID!'],
['leave', 'Name', 'x', 'QueryRoot', 'ID!'],
['leave', 'Variable', null, 'QueryRoot', 'ID!'],
['enter', 'NonNullType', null, 'QueryRoot', 'ID!'],
['enter', 'NamedType', null, 'QueryRoot', 'ID!'],
['enter', 'Name', 'ID', 'QueryRoot', 'ID!'],
['leave', 'Name', 'ID', 'QueryRoot', 'ID!'],
['leave', 'NamedType', null, 'QueryRoot', 'ID!'],
['leave', 'NonNullType', null, 'QueryRoot', 'ID!'],
['leave', 'VariableDefinition', null, 'QueryRoot', 'ID!'],
['enter', 'NamedType', null, 'QueryRoot', 'undefined'],
['enter', 'Name', 'QueryRoot', 'QueryRoot', 'undefined'],
['leave', 'Name', 'QueryRoot', 'QueryRoot', 'undefined'],
['leave', 'NamedType', null, 'QueryRoot', 'undefined'],
['enter', 'SelectionSet', null, 'QueryRoot', 'undefined'],
['enter', 'Field', null, 'Human', 'undefined'],
['enter', 'Name', 'human', 'Human', 'undefined'],
['leave', 'Name', 'human', 'Human', 'undefined'],
['enter', 'Argument', null, 'Human', 'ID'],
['enter', 'Name', 'id', 'Human', 'ID'],
['leave', 'Name', 'id', 'Human', 'ID'],
['enter', 'Variable', null, 'Human', 'ID'],
['enter', 'Name', 'x', 'Human', 'ID'],
['leave', 'Name', 'x', 'Human', 'ID'],
['leave', 'Variable', null, 'Human', 'ID'],
['leave', 'Argument', null, 'Human', 'ID'],
['enter', 'SelectionSet', null, 'Human', 'undefined'],
['enter', 'Field', null, 'String', 'undefined'],
['enter', 'Name', 'name', 'String', 'undefined'],
['leave', 'Name', 'name', 'String', 'undefined'],
['leave', 'Field', null, 'String', 'undefined'],
['leave', 'SelectionSet', null, 'Human', 'undefined'],
['leave', 'Field', null, 'Human', 'undefined'],
['leave', 'SelectionSet', null, 'QueryRoot', 'undefined'],
['leave', 'FragmentDefinition', null, 'QueryRoot', 'undefined'],
['leave', 'Document', null, 'undefined', 'undefined'],
]);
});

it('supports traversals of fragment arguments with default-value', () => {
const typeInfo = new TypeInfo(testSchema);

const ast = parse(
`
query {
...Foo(x: null)
}
fragment Foo(
$x: ID = 4
) on QueryRoot {
human(id: $x) { name }
}
`,
{ experimentalFragmentArguments: true },
);

const visited: Array<any> = [];
visit(
ast,
visitWithTypeInfo(typeInfo, {
enter(node) {
const type = typeInfo.getType();
const inputType = typeInfo.getInputType();
visited.push([
'enter',
node.kind,
node.kind === 'Name' ? node.value : null,
String(type),
String(inputType),
]);
},
leave(node) {
const type = typeInfo.getType();
const inputType = typeInfo.getInputType();
visited.push([
'leave',
node.kind,
node.kind === 'Name' ? node.value : null,
String(type),
String(inputType),
]);
},
}),
);

expect(visited).to.deep.equal([
['enter', 'Document', null, 'undefined', 'undefined'],
['enter', 'OperationDefinition', null, 'QueryRoot', 'undefined'],
['enter', 'SelectionSet', null, 'QueryRoot', 'undefined'],
['enter', 'FragmentSpread', null, 'QueryRoot', 'undefined'],
['enter', 'Name', 'Foo', 'QueryRoot', 'undefined'],
['leave', 'Name', 'Foo', 'QueryRoot', 'undefined'],
['enter', 'FragmentArgument', null, 'QueryRoot', 'ID'],
['enter', 'Name', 'x', 'QueryRoot', 'ID'],
['leave', 'Name', 'x', 'QueryRoot', 'ID'],
['enter', 'NullValue', null, 'QueryRoot', 'ID'],
['leave', 'NullValue', null, 'QueryRoot', 'ID'],
['leave', 'FragmentArgument', null, 'QueryRoot', 'ID'],
['leave', 'FragmentSpread', null, 'QueryRoot', 'undefined'],
['leave', 'SelectionSet', null, 'QueryRoot', 'undefined'],
['leave', 'OperationDefinition', null, 'QueryRoot', 'undefined'],
['enter', 'FragmentDefinition', null, 'QueryRoot', 'undefined'],
['enter', 'Name', 'Foo', 'QueryRoot', 'undefined'],
['leave', 'Name', 'Foo', 'QueryRoot', 'undefined'],
['enter', 'VariableDefinition', null, 'QueryRoot', 'ID'],
['enter', 'Variable', null, 'QueryRoot', 'ID'],
['enter', 'Name', 'x', 'QueryRoot', 'ID'],
['leave', 'Name', 'x', 'QueryRoot', 'ID'],
['leave', 'Variable', null, 'QueryRoot', 'ID'],
['enter', 'NamedType', null, 'QueryRoot', 'ID'],
['enter', 'Name', 'ID', 'QueryRoot', 'ID'],
['leave', 'Name', 'ID', 'QueryRoot', 'ID'],
['leave', 'NamedType', null, 'QueryRoot', 'ID'],
['enter', 'IntValue', null, 'QueryRoot', 'ID'],
['leave', 'IntValue', null, 'QueryRoot', 'ID'],
['leave', 'VariableDefinition', null, 'QueryRoot', 'ID'],
['enter', 'NamedType', null, 'QueryRoot', 'undefined'],
['enter', 'Name', 'QueryRoot', 'QueryRoot', 'undefined'],
['leave', 'Name', 'QueryRoot', 'QueryRoot', 'undefined'],
['leave', 'NamedType', null, 'QueryRoot', 'undefined'],
['enter', 'SelectionSet', null, 'QueryRoot', 'undefined'],
['enter', 'Field', null, 'Human', 'undefined'],
['enter', 'Name', 'human', 'Human', 'undefined'],
['leave', 'Name', 'human', 'Human', 'undefined'],
['enter', 'Argument', null, 'Human', 'ID'],
['enter', 'Name', 'id', 'Human', 'ID'],
['leave', 'Name', 'id', 'Human', 'ID'],
['enter', 'Variable', null, 'Human', 'ID'],
['enter', 'Name', 'x', 'Human', 'ID'],
['leave', 'Name', 'x', 'Human', 'ID'],
['leave', 'Variable', null, 'Human', 'ID'],
['leave', 'Argument', null, 'Human', 'ID'],
['enter', 'SelectionSet', null, 'Human', 'undefined'],
['enter', 'Field', null, 'String', 'undefined'],
['enter', 'Name', 'name', 'String', 'undefined'],
['leave', 'Name', 'name', 'String', 'undefined'],
['leave', 'Field', null, 'String', 'undefined'],
['leave', 'SelectionSet', null, 'Human', 'undefined'],
['leave', 'Field', null, 'Human', 'undefined'],
['leave', 'SelectionSet', null, 'QueryRoot', 'undefined'],
['leave', 'FragmentDefinition', null, 'QueryRoot', 'undefined'],
['leave', 'Document', null, 'undefined', 'undefined'],
]);
});
});

0 comments on commit 2fb5f1d

Please sign in to comment.