Skip to content

Commit

Permalink
Flow type visitor and validation rules. (#1155)
Browse files Browse the repository at this point in the history
Inspired by #1145
  • Loading branch information
leebyron committed Dec 16, 2017
1 parent b283d9b commit 1f97618
Show file tree
Hide file tree
Showing 33 changed files with 301 additions and 115 deletions.
6 changes: 6 additions & 0 deletions src/index.js
Expand Up @@ -190,10 +190,16 @@ export {
export type {
Lexer,
ParseOptions,
// Visitor utilities
ASTVisitor,
Visitor,
VisitFn,
VisitorKeyMap,
// AST nodes
Location,
Token,
ASTNode,
ASTKindToNode,
NameNode,
DocumentNode,
DefinitionNode,
Expand Down
48 changes: 48 additions & 0 deletions src/language/ast.js
Expand Up @@ -159,6 +159,54 @@ export type ASTNode =
| InputObjectTypeExtensionNode
| DirectiveDefinitionNode;

/**
* Utility type listing all nodes indexed by their kind.
*/
export type ASTKindToNode = {
Name: NameNode,
Document: DocumentNode,
OperationDefinition: OperationDefinitionNode,
VariableDefinition: VariableDefinitionNode,
Variable: VariableNode,
SelectionSet: SelectionSetNode,
Field: FieldNode,
Argument: ArgumentNode,
FragmentSpread: FragmentSpreadNode,
InlineFragment: InlineFragmentNode,
FragmentDefinition: FragmentDefinitionNode,
IntValue: IntValueNode,
FloatValue: FloatValueNode,
StringValue: StringValueNode,
BooleanValue: BooleanValueNode,
NullValue: NullValueNode,
EnumValue: EnumValueNode,
ListValue: ListValueNode,
ObjectValue: ObjectValueNode,
ObjectField: ObjectFieldNode,
Directive: DirectiveNode,
NamedType: NamedTypeNode,
ListType: ListTypeNode,
NonNullType: NonNullTypeNode,
SchemaDefinition: SchemaDefinitionNode,
OperationTypeDefinition: OperationTypeDefinitionNode,
ScalarTypeDefinition: ScalarTypeDefinitionNode,
ObjectTypeDefinition: ObjectTypeDefinitionNode,
FieldDefinition: FieldDefinitionNode,
InputValueDefinition: InputValueDefinitionNode,
InterfaceTypeDefinition: InterfaceTypeDefinitionNode,
UnionTypeDefinition: UnionTypeDefinitionNode,
EnumTypeDefinition: EnumTypeDefinitionNode,
EnumValueDefinition: EnumValueDefinitionNode,
InputObjectTypeDefinition: InputObjectTypeDefinitionNode,
ScalarTypeExtension: ScalarTypeExtensionNode,
ObjectTypeExtension: ObjectTypeExtensionNode,
InterfaceTypeExtension: InterfaceTypeExtensionNode,
UnionTypeExtension: UnionTypeExtensionNode,
EnumTypeExtension: EnumTypeExtensionNode,
InputObjectTypeExtension: InputObjectTypeExtensionNode,
DirectiveDefinition: DirectiveDefinitionNode,
};

// Name

export type NameNode = {
Expand Down
2 changes: 2 additions & 0 deletions src/language/index.js
Expand Up @@ -22,6 +22,7 @@ export {
getVisitFn,
BREAK,
} from './visitor';
export type { ASTVisitor, Visitor, VisitFn, VisitorKeyMap } from './visitor';

export type { Lexer } from './lexer';
export type { ParseOptions } from './parser';
Expand All @@ -30,6 +31,7 @@ export type {
Location,
Token,
ASTNode,
ASTKindToNode,
// Each kind of AST node
NameNode,
DocumentNode,
Expand Down
92 changes: 77 additions & 15 deletions src/language/visitor.js
Expand Up @@ -3,7 +3,56 @@
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import type { ASTNode, ASTKindToNode } from './ast';
import type { TypeInfo } from '../utilities/TypeInfo';

/**
* A visitor is provided to visit, it contains the collection of
* relevant functions to be called during the visitor's traversal.
*/
export type ASTVisitor = Visitor<ASTKindToNode>;
export type Visitor<KindToNode, Nodes = $Values<KindToNode>> =
| EnterLeave<
| VisitFn<Nodes>
| ShapeMap<KindToNode, <Node>(Node) => VisitFn<Nodes, Node>>,
>
| ShapeMap<
KindToNode,
<Node>(Node) => VisitFn<Nodes, Node> | EnterLeave<VisitFn<Nodes, Node>>,
>;
type EnterLeave<T> = {| +enter?: T, +leave?: T |};
type ShapeMap<O, F> = $Shape<$ObjMap<O, F>>;

/**
* A visitor is comprised of visit functions, which are called on each node
* during the visitor's traversal.
*/
export type VisitFn<TAnyNode, TVisitedNode: TAnyNode = TAnyNode> = (
// The current node being visiting.
node: TVisitedNode,
// The index or key to this node from the parent node or Array.
key: string | number | void,
// The parent immediately above this node, which may be an Array.
parent: TAnyNode | $ReadOnlyArray<TAnyNode> | void,
// The key path to get to this node from the root node.
path: $ReadOnlyArray<string | number>,
// All nodes and Arrays visited before reaching this node.
// These correspond to array indices in `path`.
// Note: ancestors includes arrays which contain the visited node.
ancestors: $ReadOnlyArray<TAnyNode | $ReadOnlyArray<TAnyNode>>,
) => any;

/**
* A KeyMap describes each the traversable properties of each kind of node.
*/
export type VisitorKeyMap<KindToNode> = $ObjMap<
KindToNode,
<T>(T) => $ReadOnlyArray<$Keys<T>>,
>;

export const QueryDocumentKeys = {
Name: [],
Expand Down Expand Up @@ -172,24 +221,28 @@ export const BREAK = {};
* }
* })
*/
export function visit(root, visitor, keyMap) {
const visitorKeys = keyMap || QueryDocumentKeys;

let stack;
export function visit(
root: ASTNode,
visitor: Visitor<ASTKindToNode>,
visitorKeys: VisitorKeyMap<ASTKindToNode> = QueryDocumentKeys,
): mixed {
/* eslint-disable no-undef-init */
let stack: any = undefined;
let inArray = Array.isArray(root);
let keys = [root];
let keys: any = [root];
let index = -1;
let edits = [];
let parent;
const path = [];
let node: any = undefined;
let key: any = undefined;
let parent: any = undefined;
const path: any = [];
const ancestors = [];
let newRoot = root;
/* eslint-enable no-undef-init */

do {
index++;
const isLeaving = index === keys.length;
let key;
let node;
const isEdited = isLeaving && edits.length !== 0;
if (isLeaving) {
key = ancestors.length === 0 ? undefined : path[path.length - 1];
Expand All @@ -209,7 +262,7 @@ export function visit(root, visitor, keyMap) {
}
let editOffset = 0;
for (let ii = 0; ii < edits.length; ii++) {
let editKey = edits[ii][0];
let editKey: any = edits[ii][0];
const editValue = edits[ii][1];
if (inArray) {
editKey -= editOffset;
Expand Down Expand Up @@ -296,8 +349,8 @@ export function visit(root, visitor, keyMap) {
return newRoot;
}

function isNode(maybeNode) {
return maybeNode && typeof maybeNode.kind === 'string';
function isNode(maybeNode): boolean %checks {
return Boolean(maybeNode && typeof maybeNode.kind === 'string');
}

/**
Expand All @@ -306,7 +359,9 @@ function isNode(maybeNode) {
*
* If a prior visitor edits a node, no following visitors will see that node.
*/
export function visitInParallel(visitors) {
export function visitInParallel(
visitors: Array<Visitor<ASTKindToNode>>,
): Visitor<ASTKindToNode> {
const skipping = new Array(visitors.length);

return {
Expand Down Expand Up @@ -351,7 +406,10 @@ export function visitInParallel(visitors) {
* Creates a new visitor instance which maintains a provided TypeInfo instance
* along with visiting visitor.
*/
export function visitWithTypeInfo(typeInfo, visitor) {
export function visitWithTypeInfo(
typeInfo: TypeInfo,
visitor: Visitor<ASTKindToNode>,
): Visitor<ASTKindToNode> {
return {
enter(node) {
typeInfo.enter(node);
Expand Down Expand Up @@ -383,7 +441,11 @@ export function visitWithTypeInfo(typeInfo, visitor) {
* Given a visitor instance, if it is leaving or not, and a node kind, return
* the function the visitor runtime should call.
*/
export function getVisitFn(visitor, kind, isLeaving) {
export function getVisitFn(
visitor: Visitor<any>,
kind: string,
isLeaving: boolean,
): ?VisitFn<any> {
const kindVisitor = visitor[kind];
if (kindVisitor) {
if (!isLeaving && typeof kindVisitor === 'function') {
Expand Down
23 changes: 21 additions & 2 deletions src/validation/__tests__/ExecutableDefinitions-test.js
Expand Up @@ -71,8 +71,27 @@ describe('Validate: Executable definitions', () => {
}
`,
[
nonExecutableDefinition('Cow', 8, 12),
nonExecutableDefinition('Dog', 12, 19),
nonExecutableDefinition('Cow', 8, 7),
nonExecutableDefinition('Dog', 12, 7),
],
);
});

it('with schema definition', () => {
expectFailsRule(
ExecutableDefinitions,
`
schema {
query: Query
}
type Query {
test: String
}
`,
[
nonExecutableDefinition('schema', 2, 7),
nonExecutableDefinition('Query', 6, 7),
],
);
});
Expand Down
14 changes: 10 additions & 4 deletions src/validation/rules/ExecutableDefinitions.js
Expand Up @@ -12,10 +12,12 @@ import { GraphQLError } from '../../error';
import {
FRAGMENT_DEFINITION,
OPERATION_DEFINITION,
SCHEMA_DEFINITION,
} from '../../language/kinds';
import type { ASTVisitor } from '../../language/visitor';

export function nonExecutableDefinitionMessage(defName: string): string {
return `The "${defName}" definition is not executable.`;
return `The ${defName} definition is not executable.`;
}

/**
Expand All @@ -24,7 +26,7 @@ export function nonExecutableDefinitionMessage(defName: string): string {
* A GraphQL document is only valid for execution if all definitions are either
* operation or fragment definitions.
*/
export function ExecutableDefinitions(context: ValidationContext): any {
export function ExecutableDefinitions(context: ValidationContext): ASTVisitor {
return {
Document(node) {
node.definitions.forEach(definition => {
Expand All @@ -34,8 +36,12 @@ export function ExecutableDefinitions(context: ValidationContext): any {
) {
context.reportError(
new GraphQLError(
nonExecutableDefinitionMessage(definition.name.value),
[definition.name],
nonExecutableDefinitionMessage(
definition.kind === SCHEMA_DEFINITION
? 'schema'
: definition.name.value,
),
[definition],
),
);
}
Expand Down
3 changes: 2 additions & 1 deletion src/validation/rules/FieldsOnCorrectType.js
Expand Up @@ -12,6 +12,7 @@ import { GraphQLError } from '../../error';
import suggestionList from '../../jsutils/suggestionList';
import quotedOrList from '../../jsutils/quotedOrList';
import type { FieldNode } from '../../language/ast';
import type { ASTVisitor } from '../../language/visitor';
import type { GraphQLSchema } from '../../type/schema';
import type { GraphQLOutputType } from '../../type/definition';
import {
Expand Down Expand Up @@ -42,7 +43,7 @@ export function undefinedFieldMessage(
* A GraphQL document is only valid if all fields selected are defined by the
* parent type, or are an allowed meta field such as __typename.
*/
export function FieldsOnCorrectType(context: ValidationContext): any {
export function FieldsOnCorrectType(context: ValidationContext): ASTVisitor {
return {
Field(node: FieldNode) {
const type = context.getParentType();
Expand Down
16 changes: 9 additions & 7 deletions src/validation/rules/FragmentsOnCompositeTypes.js
Expand Up @@ -10,6 +10,7 @@
import type { ValidationContext } from '../index';
import { GraphQLError } from '../../error';
import { print } from '../../language/printer';
import type { ASTVisitor } from '../../language/visitor';
import { isCompositeType } from '../../type/definition';
import type { GraphQLType } from '../../type/definition';
import { typeFromAST } from '../../utilities/typeFromAST';
Expand Down Expand Up @@ -37,18 +38,19 @@ export function fragmentOnNonCompositeErrorMessage(
* can only be spread into a composite type (object, interface, or union), the
* type condition must also be a composite type.
*/
export function FragmentsOnCompositeTypes(context: ValidationContext): any {
export function FragmentsOnCompositeTypes(
context: ValidationContext,
): ASTVisitor {
return {
InlineFragment(node) {
if (node.typeCondition) {
const type = typeFromAST(context.getSchema(), node.typeCondition);
const typeCondition = node.typeCondition;
if (typeCondition) {
const type = typeFromAST(context.getSchema(), typeCondition);
if (type && !isCompositeType(type)) {
context.reportError(
new GraphQLError(
inlineFragmentOnNonCompositeErrorMessage(
print(node.typeCondition),
),
[node.typeCondition],
inlineFragmentOnNonCompositeErrorMessage(print(typeCondition)),
[typeCondition],
),
);
}
Expand Down
3 changes: 2 additions & 1 deletion src/validation/rules/KnownArgumentNames.js
Expand Up @@ -9,6 +9,7 @@

import type { ValidationContext } from '../index';
import { GraphQLError } from '../../error';
import type { ASTVisitor } from '../../language/visitor';
import suggestionList from '../../jsutils/suggestionList';
import quotedOrList from '../../jsutils/quotedOrList';
import { FIELD, DIRECTIVE } from '../../language/kinds';
Expand Down Expand Up @@ -48,7 +49,7 @@ export function unknownDirectiveArgMessage(
* A GraphQL field is only valid if all supplied arguments are defined by
* that field.
*/
export function KnownArgumentNames(context: ValidationContext): any {
export function KnownArgumentNames(context: ValidationContext): ASTVisitor {
return {
Argument(node, key, parent, path, ancestors) {
const argDef = context.getArgument();
Expand Down

0 comments on commit 1f97618

Please sign in to comment.