Skip to content

Commit 1f97618

Browse files
authored
Flow type visitor and validation rules. (#1155)
Inspired by #1145
1 parent b283d9b commit 1f97618

33 files changed

+301
-115
lines changed

src/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,16 @@ export {
190190
export type {
191191
Lexer,
192192
ParseOptions,
193+
// Visitor utilities
194+
ASTVisitor,
195+
Visitor,
196+
VisitFn,
197+
VisitorKeyMap,
193198
// AST nodes
194199
Location,
195200
Token,
196201
ASTNode,
202+
ASTKindToNode,
197203
NameNode,
198204
DocumentNode,
199205
DefinitionNode,

src/language/ast.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,54 @@ export type ASTNode =
159159
| InputObjectTypeExtensionNode
160160
| DirectiveDefinitionNode;
161161

162+
/**
163+
* Utility type listing all nodes indexed by their kind.
164+
*/
165+
export type ASTKindToNode = {
166+
Name: NameNode,
167+
Document: DocumentNode,
168+
OperationDefinition: OperationDefinitionNode,
169+
VariableDefinition: VariableDefinitionNode,
170+
Variable: VariableNode,
171+
SelectionSet: SelectionSetNode,
172+
Field: FieldNode,
173+
Argument: ArgumentNode,
174+
FragmentSpread: FragmentSpreadNode,
175+
InlineFragment: InlineFragmentNode,
176+
FragmentDefinition: FragmentDefinitionNode,
177+
IntValue: IntValueNode,
178+
FloatValue: FloatValueNode,
179+
StringValue: StringValueNode,
180+
BooleanValue: BooleanValueNode,
181+
NullValue: NullValueNode,
182+
EnumValue: EnumValueNode,
183+
ListValue: ListValueNode,
184+
ObjectValue: ObjectValueNode,
185+
ObjectField: ObjectFieldNode,
186+
Directive: DirectiveNode,
187+
NamedType: NamedTypeNode,
188+
ListType: ListTypeNode,
189+
NonNullType: NonNullTypeNode,
190+
SchemaDefinition: SchemaDefinitionNode,
191+
OperationTypeDefinition: OperationTypeDefinitionNode,
192+
ScalarTypeDefinition: ScalarTypeDefinitionNode,
193+
ObjectTypeDefinition: ObjectTypeDefinitionNode,
194+
FieldDefinition: FieldDefinitionNode,
195+
InputValueDefinition: InputValueDefinitionNode,
196+
InterfaceTypeDefinition: InterfaceTypeDefinitionNode,
197+
UnionTypeDefinition: UnionTypeDefinitionNode,
198+
EnumTypeDefinition: EnumTypeDefinitionNode,
199+
EnumValueDefinition: EnumValueDefinitionNode,
200+
InputObjectTypeDefinition: InputObjectTypeDefinitionNode,
201+
ScalarTypeExtension: ScalarTypeExtensionNode,
202+
ObjectTypeExtension: ObjectTypeExtensionNode,
203+
InterfaceTypeExtension: InterfaceTypeExtensionNode,
204+
UnionTypeExtension: UnionTypeExtensionNode,
205+
EnumTypeExtension: EnumTypeExtensionNode,
206+
InputObjectTypeExtension: InputObjectTypeExtensionNode,
207+
DirectiveDefinition: DirectiveDefinitionNode,
208+
};
209+
162210
// Name
163211

164212
export type NameNode = {

src/language/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export {
2222
getVisitFn,
2323
BREAK,
2424
} from './visitor';
25+
export type { ASTVisitor, Visitor, VisitFn, VisitorKeyMap } from './visitor';
2526

2627
export type { Lexer } from './lexer';
2728
export type { ParseOptions } from './parser';
@@ -30,6 +31,7 @@ export type {
3031
Location,
3132
Token,
3233
ASTNode,
34+
ASTKindToNode,
3335
// Each kind of AST node
3436
NameNode,
3537
DocumentNode,

src/language/visitor.js

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,56 @@
33
*
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type { ASTNode, ASTKindToNode } from './ast';
11+
import type { TypeInfo } from '../utilities/TypeInfo';
12+
13+
/**
14+
* A visitor is provided to visit, it contains the collection of
15+
* relevant functions to be called during the visitor's traversal.
16+
*/
17+
export type ASTVisitor = Visitor<ASTKindToNode>;
18+
export type Visitor<KindToNode, Nodes = $Values<KindToNode>> =
19+
| EnterLeave<
20+
| VisitFn<Nodes>
21+
| ShapeMap<KindToNode, <Node>(Node) => VisitFn<Nodes, Node>>,
22+
>
23+
| ShapeMap<
24+
KindToNode,
25+
<Node>(Node) => VisitFn<Nodes, Node> | EnterLeave<VisitFn<Nodes, Node>>,
26+
>;
27+
type EnterLeave<T> = {| +enter?: T, +leave?: T |};
28+
type ShapeMap<O, F> = $Shape<$ObjMap<O, F>>;
29+
30+
/**
31+
* A visitor is comprised of visit functions, which are called on each node
32+
* during the visitor's traversal.
33+
*/
34+
export type VisitFn<TAnyNode, TVisitedNode: TAnyNode = TAnyNode> = (
35+
// The current node being visiting.
36+
node: TVisitedNode,
37+
// The index or key to this node from the parent node or Array.
38+
key: string | number | void,
39+
// The parent immediately above this node, which may be an Array.
40+
parent: TAnyNode | $ReadOnlyArray<TAnyNode> | void,
41+
// The key path to get to this node from the root node.
42+
path: $ReadOnlyArray<string | number>,
43+
// All nodes and Arrays visited before reaching this node.
44+
// These correspond to array indices in `path`.
45+
// Note: ancestors includes arrays which contain the visited node.
46+
ancestors: $ReadOnlyArray<TAnyNode | $ReadOnlyArray<TAnyNode>>,
47+
) => any;
48+
49+
/**
50+
* A KeyMap describes each the traversable properties of each kind of node.
651
*/
52+
export type VisitorKeyMap<KindToNode> = $ObjMap<
53+
KindToNode,
54+
<T>(T) => $ReadOnlyArray<$Keys<T>>,
55+
>;
756

857
export const QueryDocumentKeys = {
958
Name: [],
@@ -172,24 +221,28 @@ export const BREAK = {};
172221
* }
173222
* })
174223
*/
175-
export function visit(root, visitor, keyMap) {
176-
const visitorKeys = keyMap || QueryDocumentKeys;
177-
178-
let stack;
224+
export function visit(
225+
root: ASTNode,
226+
visitor: Visitor<ASTKindToNode>,
227+
visitorKeys: VisitorKeyMap<ASTKindToNode> = QueryDocumentKeys,
228+
): mixed {
229+
/* eslint-disable no-undef-init */
230+
let stack: any = undefined;
179231
let inArray = Array.isArray(root);
180-
let keys = [root];
232+
let keys: any = [root];
181233
let index = -1;
182234
let edits = [];
183-
let parent;
184-
const path = [];
235+
let node: any = undefined;
236+
let key: any = undefined;
237+
let parent: any = undefined;
238+
const path: any = [];
185239
const ancestors = [];
186240
let newRoot = root;
241+
/* eslint-enable no-undef-init */
187242

188243
do {
189244
index++;
190245
const isLeaving = index === keys.length;
191-
let key;
192-
let node;
193246
const isEdited = isLeaving && edits.length !== 0;
194247
if (isLeaving) {
195248
key = ancestors.length === 0 ? undefined : path[path.length - 1];
@@ -209,7 +262,7 @@ export function visit(root, visitor, keyMap) {
209262
}
210263
let editOffset = 0;
211264
for (let ii = 0; ii < edits.length; ii++) {
212-
let editKey = edits[ii][0];
265+
let editKey: any = edits[ii][0];
213266
const editValue = edits[ii][1];
214267
if (inArray) {
215268
editKey -= editOffset;
@@ -296,8 +349,8 @@ export function visit(root, visitor, keyMap) {
296349
return newRoot;
297350
}
298351

299-
function isNode(maybeNode) {
300-
return maybeNode && typeof maybeNode.kind === 'string';
352+
function isNode(maybeNode): boolean %checks {
353+
return Boolean(maybeNode && typeof maybeNode.kind === 'string');
301354
}
302355

303356
/**
@@ -306,7 +359,9 @@ function isNode(maybeNode) {
306359
*
307360
* If a prior visitor edits a node, no following visitors will see that node.
308361
*/
309-
export function visitInParallel(visitors) {
362+
export function visitInParallel(
363+
visitors: Array<Visitor<ASTKindToNode>>,
364+
): Visitor<ASTKindToNode> {
310365
const skipping = new Array(visitors.length);
311366

312367
return {
@@ -351,7 +406,10 @@ export function visitInParallel(visitors) {
351406
* Creates a new visitor instance which maintains a provided TypeInfo instance
352407
* along with visiting visitor.
353408
*/
354-
export function visitWithTypeInfo(typeInfo, visitor) {
409+
export function visitWithTypeInfo(
410+
typeInfo: TypeInfo,
411+
visitor: Visitor<ASTKindToNode>,
412+
): Visitor<ASTKindToNode> {
355413
return {
356414
enter(node) {
357415
typeInfo.enter(node);
@@ -383,7 +441,11 @@ export function visitWithTypeInfo(typeInfo, visitor) {
383441
* Given a visitor instance, if it is leaving or not, and a node kind, return
384442
* the function the visitor runtime should call.
385443
*/
386-
export function getVisitFn(visitor, kind, isLeaving) {
444+
export function getVisitFn(
445+
visitor: Visitor<any>,
446+
kind: string,
447+
isLeaving: boolean,
448+
): ?VisitFn<any> {
387449
const kindVisitor = visitor[kind];
388450
if (kindVisitor) {
389451
if (!isLeaving && typeof kindVisitor === 'function') {

src/validation/__tests__/ExecutableDefinitions-test.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,27 @@ describe('Validate: Executable definitions', () => {
7171
}
7272
`,
7373
[
74-
nonExecutableDefinition('Cow', 8, 12),
75-
nonExecutableDefinition('Dog', 12, 19),
74+
nonExecutableDefinition('Cow', 8, 7),
75+
nonExecutableDefinition('Dog', 12, 7),
76+
],
77+
);
78+
});
79+
80+
it('with schema definition', () => {
81+
expectFailsRule(
82+
ExecutableDefinitions,
83+
`
84+
schema {
85+
query: Query
86+
}
87+
88+
type Query {
89+
test: String
90+
}
91+
`,
92+
[
93+
nonExecutableDefinition('schema', 2, 7),
94+
nonExecutableDefinition('Query', 6, 7),
7695
],
7796
);
7897
});

src/validation/rules/ExecutableDefinitions.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import { GraphQLError } from '../../error';
1212
import {
1313
FRAGMENT_DEFINITION,
1414
OPERATION_DEFINITION,
15+
SCHEMA_DEFINITION,
1516
} from '../../language/kinds';
17+
import type { ASTVisitor } from '../../language/visitor';
1618

1719
export function nonExecutableDefinitionMessage(defName: string): string {
18-
return `The "${defName}" definition is not executable.`;
20+
return `The ${defName} definition is not executable.`;
1921
}
2022

2123
/**
@@ -24,7 +26,7 @@ export function nonExecutableDefinitionMessage(defName: string): string {
2426
* A GraphQL document is only valid for execution if all definitions are either
2527
* operation or fragment definitions.
2628
*/
27-
export function ExecutableDefinitions(context: ValidationContext): any {
29+
export function ExecutableDefinitions(context: ValidationContext): ASTVisitor {
2830
return {
2931
Document(node) {
3032
node.definitions.forEach(definition => {
@@ -34,8 +36,12 @@ export function ExecutableDefinitions(context: ValidationContext): any {
3436
) {
3537
context.reportError(
3638
new GraphQLError(
37-
nonExecutableDefinitionMessage(definition.name.value),
38-
[definition.name],
39+
nonExecutableDefinitionMessage(
40+
definition.kind === SCHEMA_DEFINITION
41+
? 'schema'
42+
: definition.name.value,
43+
),
44+
[definition],
3945
),
4046
);
4147
}

src/validation/rules/FieldsOnCorrectType.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { GraphQLError } from '../../error';
1212
import suggestionList from '../../jsutils/suggestionList';
1313
import quotedOrList from '../../jsutils/quotedOrList';
1414
import type { FieldNode } from '../../language/ast';
15+
import type { ASTVisitor } from '../../language/visitor';
1516
import type { GraphQLSchema } from '../../type/schema';
1617
import type { GraphQLOutputType } from '../../type/definition';
1718
import {
@@ -42,7 +43,7 @@ export function undefinedFieldMessage(
4243
* A GraphQL document is only valid if all fields selected are defined by the
4344
* parent type, or are an allowed meta field such as __typename.
4445
*/
45-
export function FieldsOnCorrectType(context: ValidationContext): any {
46+
export function FieldsOnCorrectType(context: ValidationContext): ASTVisitor {
4647
return {
4748
Field(node: FieldNode) {
4849
const type = context.getParentType();

src/validation/rules/FragmentsOnCompositeTypes.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import type { ValidationContext } from '../index';
1111
import { GraphQLError } from '../../error';
1212
import { print } from '../../language/printer';
13+
import type { ASTVisitor } from '../../language/visitor';
1314
import { isCompositeType } from '../../type/definition';
1415
import type { GraphQLType } from '../../type/definition';
1516
import { typeFromAST } from '../../utilities/typeFromAST';
@@ -37,18 +38,19 @@ export function fragmentOnNonCompositeErrorMessage(
3738
* can only be spread into a composite type (object, interface, or union), the
3839
* type condition must also be a composite type.
3940
*/
40-
export function FragmentsOnCompositeTypes(context: ValidationContext): any {
41+
export function FragmentsOnCompositeTypes(
42+
context: ValidationContext,
43+
): ASTVisitor {
4144
return {
4245
InlineFragment(node) {
43-
if (node.typeCondition) {
44-
const type = typeFromAST(context.getSchema(), node.typeCondition);
46+
const typeCondition = node.typeCondition;
47+
if (typeCondition) {
48+
const type = typeFromAST(context.getSchema(), typeCondition);
4549
if (type && !isCompositeType(type)) {
4650
context.reportError(
4751
new GraphQLError(
48-
inlineFragmentOnNonCompositeErrorMessage(
49-
print(node.typeCondition),
50-
),
51-
[node.typeCondition],
52+
inlineFragmentOnNonCompositeErrorMessage(print(typeCondition)),
53+
[typeCondition],
5254
),
5355
);
5456
}

src/validation/rules/KnownArgumentNames.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import type { ValidationContext } from '../index';
1111
import { GraphQLError } from '../../error';
12+
import type { ASTVisitor } from '../../language/visitor';
1213
import suggestionList from '../../jsutils/suggestionList';
1314
import quotedOrList from '../../jsutils/quotedOrList';
1415
import { FIELD, DIRECTIVE } from '../../language/kinds';
@@ -48,7 +49,7 @@ export function unknownDirectiveArgMessage(
4849
* A GraphQL field is only valid if all supplied arguments are defined by
4950
* that field.
5051
*/
51-
export function KnownArgumentNames(context: ValidationContext): any {
52+
export function KnownArgumentNames(context: ValidationContext): ASTVisitor {
5253
return {
5354
Argument(node, key, parent, path, ancestors) {
5455
const argDef = context.getArgument();

0 commit comments

Comments
 (0)