From 1f9761808e3fa8fd617599820e0dd85cca57087c Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Fri, 15 Dec 2017 18:27:23 -0800 Subject: [PATCH] Flow type visitor and validation rules. (#1155) Inspired by #1145 --- src/index.js | 6 + src/language/ast.js | 48 ++++++++ src/language/index.js | 2 + src/language/visitor.js | 92 +++++++++++--- .../__tests__/ExecutableDefinitions-test.js | 23 +++- src/validation/rules/ExecutableDefinitions.js | 14 ++- src/validation/rules/FieldsOnCorrectType.js | 3 +- .../rules/FragmentsOnCompositeTypes.js | 16 +-- src/validation/rules/KnownArgumentNames.js | 3 +- src/validation/rules/KnownDirectives.js | 113 +++++++++--------- src/validation/rules/KnownFragmentNames.js | 3 +- src/validation/rules/KnownTypeNames.js | 8 +- .../rules/LoneAnonymousOperation.js | 3 +- src/validation/rules/NoFragmentCycles.js | 3 +- src/validation/rules/NoUndefinedVariables.js | 3 +- src/validation/rules/NoUnusedFragments.js | 3 +- src/validation/rules/NoUnusedVariables.js | 3 +- .../rules/OverlappingFieldsCanBeMerged.js | 5 +- .../rules/PossibleFragmentSpreads.js | 5 +- .../rules/ProvidedNonNullArguments.js | 5 +- src/validation/rules/ScalarLeafs.js | 3 +- .../rules/SingleFieldSubscriptions.js | 5 +- src/validation/rules/UniqueArgumentNames.js | 3 +- .../rules/UniqueDirectivesPerLocation.js | 13 +- src/validation/rules/UniqueFragmentNames.js | 3 +- src/validation/rules/UniqueInputFieldNames.js | 3 +- src/validation/rules/UniqueOperationNames.js | 3 +- src/validation/rules/UniqueVariableNames.js | 3 +- src/validation/rules/ValuesOfCorrectType.js | 3 +- .../rules/VariablesAreInputTypes.js | 3 +- .../rules/VariablesDefaultValueAllowed.js | 5 +- .../rules/VariablesInAllowedPosition.js | 5 +- src/validation/validate.js | 3 +- 33 files changed, 301 insertions(+), 115 deletions(-) diff --git a/src/index.js b/src/index.js index 2e10464399..ce6d060c9b 100644 --- a/src/index.js +++ b/src/index.js @@ -190,10 +190,16 @@ export { export type { Lexer, ParseOptions, + // Visitor utilities + ASTVisitor, + Visitor, + VisitFn, + VisitorKeyMap, // AST nodes Location, Token, ASTNode, + ASTKindToNode, NameNode, DocumentNode, DefinitionNode, diff --git a/src/language/ast.js b/src/language/ast.js index f3aaac9fc3..01d17cdbe8 100644 --- a/src/language/ast.js +++ b/src/language/ast.js @@ -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 = { diff --git a/src/language/index.js b/src/language/index.js index 7097734b65..f8bd4bfbb6 100644 --- a/src/language/index.js +++ b/src/language/index.js @@ -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'; @@ -30,6 +31,7 @@ export type { Location, Token, ASTNode, + ASTKindToNode, // Each kind of AST node NameNode, DocumentNode, diff --git a/src/language/visitor.js b/src/language/visitor.js index 2b899408f2..5727896aed 100644 --- a/src/language/visitor.js +++ b/src/language/visitor.js @@ -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; +export type Visitor> = + | EnterLeave< + | VisitFn + | ShapeMap(Node) => VisitFn>, + > + | ShapeMap< + KindToNode, + (Node) => VisitFn | EnterLeave>, + >; +type EnterLeave = {| +enter?: T, +leave?: T |}; +type ShapeMap = $Shape<$ObjMap>; + +/** + * A visitor is comprised of visit functions, which are called on each node + * during the visitor's traversal. + */ +export type VisitFn = ( + // 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 | void, + // The key path to get to this node from the root node. + path: $ReadOnlyArray, + // 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>, +) => any; + +/** + * A KeyMap describes each the traversable properties of each kind of node. */ +export type VisitorKeyMap = $ObjMap< + KindToNode, + (T) => $ReadOnlyArray<$Keys>, +>; export const QueryDocumentKeys = { Name: [], @@ -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, + visitorKeys: VisitorKeyMap = 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]; @@ -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; @@ -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'); } /** @@ -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 { const skipping = new Array(visitors.length); return { @@ -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, +): Visitor { return { enter(node) { typeInfo.enter(node); @@ -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, + kind: string, + isLeaving: boolean, +): ?VisitFn { const kindVisitor = visitor[kind]; if (kindVisitor) { if (!isLeaving && typeof kindVisitor === 'function') { diff --git a/src/validation/__tests__/ExecutableDefinitions-test.js b/src/validation/__tests__/ExecutableDefinitions-test.js index c5860be0e7..bd361e73ab 100644 --- a/src/validation/__tests__/ExecutableDefinitions-test.js +++ b/src/validation/__tests__/ExecutableDefinitions-test.js @@ -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), ], ); }); diff --git a/src/validation/rules/ExecutableDefinitions.js b/src/validation/rules/ExecutableDefinitions.js index 7ca63ee43a..fc2d467646 100644 --- a/src/validation/rules/ExecutableDefinitions.js +++ b/src/validation/rules/ExecutableDefinitions.js @@ -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.`; } /** @@ -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 => { @@ -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], ), ); } diff --git a/src/validation/rules/FieldsOnCorrectType.js b/src/validation/rules/FieldsOnCorrectType.js index 644af5791d..5b8b8a252c 100644 --- a/src/validation/rules/FieldsOnCorrectType.js +++ b/src/validation/rules/FieldsOnCorrectType.js @@ -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 { @@ -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(); diff --git a/src/validation/rules/FragmentsOnCompositeTypes.js b/src/validation/rules/FragmentsOnCompositeTypes.js index 93b7c5f139..e3f367f0e0 100644 --- a/src/validation/rules/FragmentsOnCompositeTypes.js +++ b/src/validation/rules/FragmentsOnCompositeTypes.js @@ -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'; @@ -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], ), ); } diff --git a/src/validation/rules/KnownArgumentNames.js b/src/validation/rules/KnownArgumentNames.js index eb8b120597..ecdb7e9dbf 100644 --- a/src/validation/rules/KnownArgumentNames.js +++ b/src/validation/rules/KnownArgumentNames.js @@ -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'; @@ -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(); diff --git a/src/validation/rules/KnownDirectives.js b/src/validation/rules/KnownDirectives.js index 1422c138d2..a513fd1e0f 100644 --- a/src/validation/rules/KnownDirectives.js +++ b/src/validation/rules/KnownDirectives.js @@ -12,6 +12,7 @@ import { GraphQLError } from '../../error'; import find from '../../jsutils/find'; import * as Kind from '../../language/kinds'; import { DirectiveLocation } from '../../language/directiveLocation'; +import type { ASTVisitor } from '../../language/visitor'; export function unknownDirectiveMessage(directiveName: string): string { return `Unknown directive "${directiveName}".`; @@ -30,7 +31,7 @@ export function misplacedDirectiveMessage( * A GraphQL document is only valid if all `@directives` are known by the * schema and legally positioned. */ -export function KnownDirectives(context: ValidationContext): any { +export function KnownDirectives(context: ValidationContext): ASTVisitor { return { Directive(node, key, parent, path, ancestors) { const directiveDef = find( @@ -44,14 +45,10 @@ export function KnownDirectives(context: ValidationContext): any { return; } const candidateLocation = getDirectiveLocationForASTPath(ancestors); - if (!candidateLocation) { - context.reportError( - new GraphQLError( - misplacedDirectiveMessage(node.name.value, node.type), - [node], - ), - ); - } else if (directiveDef.locations.indexOf(candidateLocation) === -1) { + if ( + candidateLocation && + directiveDef.locations.indexOf(candidateLocation) === -1 + ) { context.reportError( new GraphQLError( misplacedDirectiveMessage(node.name.value, candidateLocation), @@ -65,53 +62,55 @@ export function KnownDirectives(context: ValidationContext): any { function getDirectiveLocationForASTPath(ancestors) { const appliedTo = ancestors[ancestors.length - 1]; - switch (appliedTo.kind) { - case Kind.OPERATION_DEFINITION: - switch (appliedTo.operation) { - case 'query': - return DirectiveLocation.QUERY; - case 'mutation': - return DirectiveLocation.MUTATION; - case 'subscription': - return DirectiveLocation.SUBSCRIPTION; - } - break; - case Kind.FIELD: - return DirectiveLocation.FIELD; - case Kind.FRAGMENT_SPREAD: - return DirectiveLocation.FRAGMENT_SPREAD; - case Kind.INLINE_FRAGMENT: - return DirectiveLocation.INLINE_FRAGMENT; - case Kind.FRAGMENT_DEFINITION: - return DirectiveLocation.FRAGMENT_DEFINITION; - case Kind.SCHEMA_DEFINITION: - return DirectiveLocation.SCHEMA; - case Kind.SCALAR_TYPE_DEFINITION: - case Kind.SCALAR_TYPE_EXTENSION: - return DirectiveLocation.SCALAR; - case Kind.OBJECT_TYPE_DEFINITION: - case Kind.OBJECT_TYPE_EXTENSION: - return DirectiveLocation.OBJECT; - case Kind.FIELD_DEFINITION: - return DirectiveLocation.FIELD_DEFINITION; - case Kind.INTERFACE_TYPE_DEFINITION: - case Kind.INTERFACE_TYPE_EXTENSION: - return DirectiveLocation.INTERFACE; - case Kind.UNION_TYPE_DEFINITION: - case Kind.UNION_TYPE_EXTENSION: - return DirectiveLocation.UNION; - case Kind.ENUM_TYPE_DEFINITION: - case Kind.ENUM_TYPE_EXTENSION: - return DirectiveLocation.ENUM; - case Kind.ENUM_VALUE_DEFINITION: - return DirectiveLocation.ENUM_VALUE; - case Kind.INPUT_OBJECT_TYPE_DEFINITION: - case Kind.INPUT_OBJECT_TYPE_EXTENSION: - return DirectiveLocation.INPUT_OBJECT; - case Kind.INPUT_VALUE_DEFINITION: - const parentNode = ancestors[ancestors.length - 3]; - return parentNode.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION - ? DirectiveLocation.INPUT_FIELD_DEFINITION - : DirectiveLocation.ARGUMENT_DEFINITION; + if (!Array.isArray(appliedTo)) { + switch (appliedTo.kind) { + case Kind.OPERATION_DEFINITION: + switch (appliedTo.operation) { + case 'query': + return DirectiveLocation.QUERY; + case 'mutation': + return DirectiveLocation.MUTATION; + case 'subscription': + return DirectiveLocation.SUBSCRIPTION; + } + break; + case Kind.FIELD: + return DirectiveLocation.FIELD; + case Kind.FRAGMENT_SPREAD: + return DirectiveLocation.FRAGMENT_SPREAD; + case Kind.INLINE_FRAGMENT: + return DirectiveLocation.INLINE_FRAGMENT; + case Kind.FRAGMENT_DEFINITION: + return DirectiveLocation.FRAGMENT_DEFINITION; + case Kind.SCHEMA_DEFINITION: + return DirectiveLocation.SCHEMA; + case Kind.SCALAR_TYPE_DEFINITION: + case Kind.SCALAR_TYPE_EXTENSION: + return DirectiveLocation.SCALAR; + case Kind.OBJECT_TYPE_DEFINITION: + case Kind.OBJECT_TYPE_EXTENSION: + return DirectiveLocation.OBJECT; + case Kind.FIELD_DEFINITION: + return DirectiveLocation.FIELD_DEFINITION; + case Kind.INTERFACE_TYPE_DEFINITION: + case Kind.INTERFACE_TYPE_EXTENSION: + return DirectiveLocation.INTERFACE; + case Kind.UNION_TYPE_DEFINITION: + case Kind.UNION_TYPE_EXTENSION: + return DirectiveLocation.UNION; + case Kind.ENUM_TYPE_DEFINITION: + case Kind.ENUM_TYPE_EXTENSION: + return DirectiveLocation.ENUM; + case Kind.ENUM_VALUE_DEFINITION: + return DirectiveLocation.ENUM_VALUE; + case Kind.INPUT_OBJECT_TYPE_DEFINITION: + case Kind.INPUT_OBJECT_TYPE_EXTENSION: + return DirectiveLocation.INPUT_OBJECT; + case Kind.INPUT_VALUE_DEFINITION: + const parentNode = ancestors[ancestors.length - 3]; + return parentNode.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION + ? DirectiveLocation.INPUT_FIELD_DEFINITION + : DirectiveLocation.ARGUMENT_DEFINITION; + } } } diff --git a/src/validation/rules/KnownFragmentNames.js b/src/validation/rules/KnownFragmentNames.js index 2235b563a7..dfab6ae101 100644 --- a/src/validation/rules/KnownFragmentNames.js +++ b/src/validation/rules/KnownFragmentNames.js @@ -9,6 +9,7 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; +import type { ASTVisitor } from '../../language/visitor'; export function unknownFragmentMessage(fragName: string): string { return `Unknown fragment "${fragName}".`; @@ -20,7 +21,7 @@ export function unknownFragmentMessage(fragName: string): string { * A GraphQL document is only valid if all `...Fragment` fragment spreads refer * to fragments defined in the same document. */ -export function KnownFragmentNames(context: ValidationContext): any { +export function KnownFragmentNames(context: ValidationContext): ASTVisitor { return { FragmentSpread(node) { const fragmentName = node.name.value; diff --git a/src/validation/rules/KnownTypeNames.js b/src/validation/rules/KnownTypeNames.js index e80c539d87..c057d7fa86 100644 --- a/src/validation/rules/KnownTypeNames.js +++ b/src/validation/rules/KnownTypeNames.js @@ -11,13 +11,13 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; import suggestionList from '../../jsutils/suggestionList'; import quotedOrList from '../../jsutils/quotedOrList'; -import type { GraphQLType } from '../../type/definition'; +import type { ASTVisitor } from '../../language/visitor'; export function unknownTypeMessage( - type: GraphQLType, + typeName: string, suggestedTypes: Array, ): string { - let message = `Unknown type "${String(type)}".`; + let message = `Unknown type "${typeName}".`; if (suggestedTypes.length) { message += ` Did you mean ${quotedOrList(suggestedTypes)}?`; } @@ -30,7 +30,7 @@ export function unknownTypeMessage( * A GraphQL document is only valid if referenced types (specifically * variable definitions and fragment conditions) are defined by the type schema. */ -export function KnownTypeNames(context: ValidationContext): any { +export function KnownTypeNames(context: ValidationContext): ASTVisitor { return { // TODO: when validating IDL, re-enable these. Experimental version does not // add unreferenced types, resulting in false-positive errors. Squelched diff --git a/src/validation/rules/LoneAnonymousOperation.js b/src/validation/rules/LoneAnonymousOperation.js index 065e4eb2f4..31c84bd679 100644 --- a/src/validation/rules/LoneAnonymousOperation.js +++ b/src/validation/rules/LoneAnonymousOperation.js @@ -10,6 +10,7 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; import { OPERATION_DEFINITION } from '../../language/kinds'; +import type { ASTVisitor } from '../../language/visitor'; export function anonOperationNotAloneMessage(): string { return 'This anonymous operation must be the only defined operation.'; @@ -21,7 +22,7 @@ export function anonOperationNotAloneMessage(): string { * A GraphQL document is only valid if when it contains an anonymous operation * (the query short-hand) that it contains only that one operation definition. */ -export function LoneAnonymousOperation(context: ValidationContext): any { +export function LoneAnonymousOperation(context: ValidationContext): ASTVisitor { let operationCount = 0; return { Document(node) { diff --git a/src/validation/rules/NoFragmentCycles.js b/src/validation/rules/NoFragmentCycles.js index 6b6a2fe9e5..1edefaf8b5 100644 --- a/src/validation/rules/NoFragmentCycles.js +++ b/src/validation/rules/NoFragmentCycles.js @@ -10,6 +10,7 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; import type { FragmentDefinitionNode } from '../../language/ast'; +import type { ASTVisitor } from '../../language/visitor'; export function cycleErrorMessage( fragName: string, @@ -19,7 +20,7 @@ export function cycleErrorMessage( return `Cannot spread fragment "${fragName}" within itself${via}.`; } -export function NoFragmentCycles(context: ValidationContext): any { +export function NoFragmentCycles(context: ValidationContext): ASTVisitor { // Tracks already visited fragments to maintain O(N) and to ensure that cycles // are not redundantly reported. const visitedFrags = Object.create(null); diff --git a/src/validation/rules/NoUndefinedVariables.js b/src/validation/rules/NoUndefinedVariables.js index e19127c5a1..72b0137f97 100644 --- a/src/validation/rules/NoUndefinedVariables.js +++ b/src/validation/rules/NoUndefinedVariables.js @@ -9,6 +9,7 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; +import type { ASTVisitor } from '../../language/visitor'; export function undefinedVarMessage(varName: string, opName: ?string): string { return opName @@ -22,7 +23,7 @@ export function undefinedVarMessage(varName: string, opName: ?string): string { * A GraphQL operation is only valid if all variables encountered, both directly * and via fragment spreads, are defined by that operation. */ -export function NoUndefinedVariables(context: ValidationContext): any { +export function NoUndefinedVariables(context: ValidationContext): ASTVisitor { let variableNameDefined = Object.create(null); return { diff --git a/src/validation/rules/NoUnusedFragments.js b/src/validation/rules/NoUnusedFragments.js index b5b9ba6db0..19ead757f9 100644 --- a/src/validation/rules/NoUnusedFragments.js +++ b/src/validation/rules/NoUnusedFragments.js @@ -9,6 +9,7 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; +import type { ASTVisitor } from '../../language/visitor'; export function unusedFragMessage(fragName: string): string { return `Fragment "${fragName}" is never used.`; @@ -20,7 +21,7 @@ export function unusedFragMessage(fragName: string): string { * A GraphQL document is only valid if all fragment definitions are spread * within operations, or spread within other fragments spread within operations. */ -export function NoUnusedFragments(context: ValidationContext): any { +export function NoUnusedFragments(context: ValidationContext): ASTVisitor { const operationDefs = []; const fragmentDefs = []; diff --git a/src/validation/rules/NoUnusedVariables.js b/src/validation/rules/NoUnusedVariables.js index ae8f9b0501..4788f68251 100644 --- a/src/validation/rules/NoUnusedVariables.js +++ b/src/validation/rules/NoUnusedVariables.js @@ -9,6 +9,7 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; +import type { ASTVisitor } from '../../language/visitor'; export function unusedVariableMessage( varName: string, @@ -25,7 +26,7 @@ export function unusedVariableMessage( * A GraphQL operation is only valid if all variables defined by an operation * are used, either directly or within a spread fragment. */ -export function NoUnusedVariables(context: ValidationContext): any { +export function NoUnusedVariables(context: ValidationContext): ASTVisitor { let variableDefs = []; return { diff --git a/src/validation/rules/OverlappingFieldsCanBeMerged.js b/src/validation/rules/OverlappingFieldsCanBeMerged.js index 04b1745255..8ce6298cdb 100644 --- a/src/validation/rules/OverlappingFieldsCanBeMerged.js +++ b/src/validation/rules/OverlappingFieldsCanBeMerged.js @@ -19,6 +19,7 @@ import type { } from '../../language/ast'; import * as Kind from '../../language/kinds'; import { print } from '../../language/printer'; +import type { ASTVisitor } from '../../language/visitor'; import { getNamedType, isNonNullType, @@ -67,7 +68,9 @@ function reasonMessage(reason: ConflictReasonMessage): string { * fragments) either correspond to distinct response names or can be merged * without ambiguity. */ -export function OverlappingFieldsCanBeMerged(context: ValidationContext): any { +export function OverlappingFieldsCanBeMerged( + context: ValidationContext, +): ASTVisitor { // A memoization for when two fragments are compared "between" each other for // conflicts. Two fragments may be compared many times, so memoizing this can // dramatically improve the performance of this validator. diff --git a/src/validation/rules/PossibleFragmentSpreads.js b/src/validation/rules/PossibleFragmentSpreads.js index 547bc8b069..a3342cf740 100644 --- a/src/validation/rules/PossibleFragmentSpreads.js +++ b/src/validation/rules/PossibleFragmentSpreads.js @@ -9,6 +9,7 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; +import type { ASTVisitor } from '../../language/visitor'; import { doTypesOverlap } from '../../utilities/typeComparators'; import { typeFromAST } from '../../utilities/typeFromAST'; import { isCompositeType } from '../../type/definition'; @@ -42,7 +43,9 @@ export function typeIncompatibleAnonSpreadMessage( * be true: if there is a non-empty intersection of the possible parent types, * and possible types which pass the type condition. */ -export function PossibleFragmentSpreads(context: ValidationContext): any { +export function PossibleFragmentSpreads( + context: ValidationContext, +): ASTVisitor { return { InlineFragment(node) { const fragType = context.getType(); diff --git a/src/validation/rules/ProvidedNonNullArguments.js b/src/validation/rules/ProvidedNonNullArguments.js index ee9569232c..1994917431 100644 --- a/src/validation/rules/ProvidedNonNullArguments.js +++ b/src/validation/rules/ProvidedNonNullArguments.js @@ -12,6 +12,7 @@ import { GraphQLError } from '../../error'; import keyMap from '../../jsutils/keyMap'; import { isNonNullType } from '../../type/definition'; import type { GraphQLType } from '../../type/definition'; +import type { ASTVisitor } from '../../language/visitor'; export function missingFieldArgMessage( fieldName: string, @@ -41,7 +42,9 @@ export function missingDirectiveArgMessage( * A field or directive is only valid if all required (non-null) field arguments * have been provided. */ -export function ProvidedNonNullArguments(context: ValidationContext): any { +export function ProvidedNonNullArguments( + context: ValidationContext, +): ASTVisitor { return { Field: { // Validate on leave to allow for deeper errors to appear first. diff --git a/src/validation/rules/ScalarLeafs.js b/src/validation/rules/ScalarLeafs.js index b1f4e27975..29a40ee434 100644 --- a/src/validation/rules/ScalarLeafs.js +++ b/src/validation/rules/ScalarLeafs.js @@ -12,6 +12,7 @@ import { GraphQLError } from '../../error'; import type { FieldNode } from '../../language/ast'; import { getNamedType, isLeafType } from '../../type/definition'; import type { GraphQLType } from '../../type/definition'; +import type { ASTVisitor } from '../../language/visitor'; export function noSubselectionAllowedMessage( fieldName: string, @@ -39,7 +40,7 @@ export function requiredSubselectionMessage( * A GraphQL document is valid only if all leaf fields (fields without * sub selections) are of scalar or enum types. */ -export function ScalarLeafs(context: ValidationContext): any { +export function ScalarLeafs(context: ValidationContext): ASTVisitor { return { Field(node: FieldNode) { const type = context.getType(); diff --git a/src/validation/rules/SingleFieldSubscriptions.js b/src/validation/rules/SingleFieldSubscriptions.js index b3a6b719f3..280155800e 100644 --- a/src/validation/rules/SingleFieldSubscriptions.js +++ b/src/validation/rules/SingleFieldSubscriptions.js @@ -10,6 +10,7 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; import type { OperationDefinitionNode } from '../../language/ast'; +import type { ASTVisitor } from '../../language/visitor'; export function singleFieldOnlyMessage(name: ?string): string { return ( @@ -23,7 +24,9 @@ export function singleFieldOnlyMessage(name: ?string): string { * * A GraphQL subscription is valid only if it contains a single root field. */ -export function SingleFieldSubscriptions(context: ValidationContext): any { +export function SingleFieldSubscriptions( + context: ValidationContext, +): ASTVisitor { return { OperationDefinition(node: OperationDefinitionNode) { if (node.operation === 'subscription') { diff --git a/src/validation/rules/UniqueArgumentNames.js b/src/validation/rules/UniqueArgumentNames.js index 729aa48ea6..66da7c3651 100644 --- a/src/validation/rules/UniqueArgumentNames.js +++ b/src/validation/rules/UniqueArgumentNames.js @@ -9,6 +9,7 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; +import type { ASTVisitor } from '../../language/visitor'; export function duplicateArgMessage(argName: string): string { return `There can be only one argument named "${argName}".`; @@ -20,7 +21,7 @@ export function duplicateArgMessage(argName: string): string { * A GraphQL field or directive is only valid if all supplied arguments are * uniquely named. */ -export function UniqueArgumentNames(context: ValidationContext): any { +export function UniqueArgumentNames(context: ValidationContext): ASTVisitor { let knownArgNames = Object.create(null); return { Field() { diff --git a/src/validation/rules/UniqueDirectivesPerLocation.js b/src/validation/rules/UniqueDirectivesPerLocation.js index 6e9e0fb21e..80b6b0512d 100644 --- a/src/validation/rules/UniqueDirectivesPerLocation.js +++ b/src/validation/rules/UniqueDirectivesPerLocation.js @@ -9,6 +9,8 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; +import type { DirectiveNode } from '../../language/ast'; +import type { ASTVisitor } from '../../language/visitor'; export function duplicateDirectiveMessage(directiveName: string): string { return ( @@ -23,15 +25,20 @@ export function duplicateDirectiveMessage(directiveName: string): string { * A GraphQL document is only valid if all directives at a given location * are uniquely named. */ -export function UniqueDirectivesPerLocation(context: ValidationContext): any { +export function UniqueDirectivesPerLocation( + context: ValidationContext, +): ASTVisitor { return { // Many different AST nodes may contain directives. Rather than listing // them all, just listen for entering any node, and check to see if it // defines any directives. enter(node) { - if (node.directives) { + // Flow can't refine that node.directives will only contain directives, + // so we cast so the rest of the code is well typed. + const directives: ?$ReadOnlyArray = (node: any).directives; + if (directives) { const knownDirectives = Object.create(null); - node.directives.forEach(directive => { + directives.forEach(directive => { const directiveName = directive.name.value; if (knownDirectives[directiveName]) { context.reportError( diff --git a/src/validation/rules/UniqueFragmentNames.js b/src/validation/rules/UniqueFragmentNames.js index b5129d0e81..41d274621d 100644 --- a/src/validation/rules/UniqueFragmentNames.js +++ b/src/validation/rules/UniqueFragmentNames.js @@ -9,6 +9,7 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; +import type { ASTVisitor } from '../../language/visitor'; export function duplicateFragmentNameMessage(fragName: string): string { return `There can be only one fragment named "${fragName}".`; @@ -19,7 +20,7 @@ export function duplicateFragmentNameMessage(fragName: string): string { * * A GraphQL document is only valid if all defined fragments have unique names. */ -export function UniqueFragmentNames(context: ValidationContext): any { +export function UniqueFragmentNames(context: ValidationContext): ASTVisitor { const knownFragmentNames = Object.create(null); return { OperationDefinition: () => false, diff --git a/src/validation/rules/UniqueInputFieldNames.js b/src/validation/rules/UniqueInputFieldNames.js index b1811b452f..0659a4317c 100644 --- a/src/validation/rules/UniqueInputFieldNames.js +++ b/src/validation/rules/UniqueInputFieldNames.js @@ -9,6 +9,7 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; +import type { ASTVisitor } from '../../language/visitor'; export function duplicateInputFieldMessage(fieldName: string): string { return `There can be only one input field named "${fieldName}".`; @@ -20,7 +21,7 @@ export function duplicateInputFieldMessage(fieldName: string): string { * A GraphQL input object value is only valid if all supplied fields are * uniquely named. */ -export function UniqueInputFieldNames(context: ValidationContext): any { +export function UniqueInputFieldNames(context: ValidationContext): ASTVisitor { const knownNameStack = []; let knownNames = Object.create(null); diff --git a/src/validation/rules/UniqueOperationNames.js b/src/validation/rules/UniqueOperationNames.js index 5ccc2006da..0b67bec8ff 100644 --- a/src/validation/rules/UniqueOperationNames.js +++ b/src/validation/rules/UniqueOperationNames.js @@ -9,6 +9,7 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; +import type { ASTVisitor } from '../../language/visitor'; export function duplicateOperationNameMessage(operationName: string): string { return `There can be only one operation named "${operationName}".`; @@ -19,7 +20,7 @@ export function duplicateOperationNameMessage(operationName: string): string { * * A GraphQL document is only valid if all defined operations have unique names. */ -export function UniqueOperationNames(context: ValidationContext): any { +export function UniqueOperationNames(context: ValidationContext): ASTVisitor { const knownOperationNames = Object.create(null); return { OperationDefinition(node) { diff --git a/src/validation/rules/UniqueVariableNames.js b/src/validation/rules/UniqueVariableNames.js index f7fa80e2e7..567cc1bb9a 100644 --- a/src/validation/rules/UniqueVariableNames.js +++ b/src/validation/rules/UniqueVariableNames.js @@ -10,6 +10,7 @@ import type { ValidationContext } from '../index'; import type { VariableDefinitionNode } from '../../language/ast'; import { GraphQLError } from '../../error'; +import type { ASTVisitor } from '../../language/visitor'; export function duplicateVariableMessage(variableName: string): string { return `There can be only one variable named "${variableName}".`; @@ -20,7 +21,7 @@ export function duplicateVariableMessage(variableName: string): string { * * A GraphQL operation is only valid if all its variables are uniquely named. */ -export function UniqueVariableNames(context: ValidationContext): any { +export function UniqueVariableNames(context: ValidationContext): ASTVisitor { let knownVariableNames = Object.create(null); return { OperationDefinition() { diff --git a/src/validation/rules/ValuesOfCorrectType.js b/src/validation/rules/ValuesOfCorrectType.js index d8cfe53079..1815f00e41 100644 --- a/src/validation/rules/ValuesOfCorrectType.js +++ b/src/validation/rules/ValuesOfCorrectType.js @@ -11,6 +11,7 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; import type { ValueNode } from '../../language/ast'; import { print } from '../../language/printer'; +import type { ASTVisitor } from '../../language/visitor'; import { isScalarType, isEnumType, @@ -65,7 +66,7 @@ export function unknownFieldMessage( * A GraphQL document is only valid if all value literals are of the type * expected at their position. */ -export function ValuesOfCorrectType(context: ValidationContext): any { +export function ValuesOfCorrectType(context: ValidationContext): ASTVisitor { return { NullValue(node) { const type = context.getInputType(); diff --git a/src/validation/rules/VariablesAreInputTypes.js b/src/validation/rules/VariablesAreInputTypes.js index e066542f42..b1772f1b81 100644 --- a/src/validation/rules/VariablesAreInputTypes.js +++ b/src/validation/rules/VariablesAreInputTypes.js @@ -11,6 +11,7 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; import type { VariableDefinitionNode } from '../../language/ast'; import { print } from '../../language/printer'; +import type { ASTVisitor } from '../../language/visitor'; import { isInputType } from '../../type/definition'; import { typeFromAST } from '../../utilities/typeFromAST'; @@ -27,7 +28,7 @@ export function nonInputTypeOnVarMessage( * A GraphQL operation is only valid if all the variables it defines are of * input types (scalar, enum, or input object). */ -export function VariablesAreInputTypes(context: ValidationContext): any { +export function VariablesAreInputTypes(context: ValidationContext): ASTVisitor { return { VariableDefinition(node: VariableDefinitionNode): ?GraphQLError { const type = typeFromAST(context.getSchema(), node.type); diff --git a/src/validation/rules/VariablesDefaultValueAllowed.js b/src/validation/rules/VariablesDefaultValueAllowed.js index 9f83614ad5..b03b16b44c 100644 --- a/src/validation/rules/VariablesDefaultValueAllowed.js +++ b/src/validation/rules/VariablesDefaultValueAllowed.js @@ -9,6 +9,7 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; +import type { ASTVisitor } from '../../language/visitor'; import { isNonNullType } from '../../type/definition'; import type { GraphQLType } from '../../type/definition'; @@ -30,7 +31,9 @@ export function defaultForRequiredVarMessage( * A GraphQL document is only valid if all variable default values are allowed * due to a variable not being required. */ -export function VariablesDefaultValueAllowed(context: ValidationContext): any { +export function VariablesDefaultValueAllowed( + context: ValidationContext, +): ASTVisitor { return { VariableDefinition(node) { const name = node.variable.name.value; diff --git a/src/validation/rules/VariablesInAllowedPosition.js b/src/validation/rules/VariablesInAllowedPosition.js index 857ac75672..498644f7f8 100644 --- a/src/validation/rules/VariablesInAllowedPosition.js +++ b/src/validation/rules/VariablesInAllowedPosition.js @@ -9,6 +9,7 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; +import type { ASTVisitor } from '../../language/visitor'; import { isNonNullType } from '../../type/definition'; import { GraphQLNonNull } from '../../type/wrappers'; import { isTypeSubTypeOf } from '../../utilities/typeComparators'; @@ -29,7 +30,9 @@ export function badVarPosMessage( /** * Variables passed to field arguments conform to type */ -export function VariablesInAllowedPosition(context: ValidationContext): any { +export function VariablesInAllowedPosition( + context: ValidationContext, +): ASTVisitor { let varDefMap = Object.create(null); return { diff --git a/src/validation/validate.js b/src/validation/validate.js index 33bfa51584..bcc7dbd5fb 100644 --- a/src/validation/validate.js +++ b/src/validation/validate.js @@ -20,6 +20,7 @@ import type { FragmentSpreadNode, FragmentDefinitionNode, } from '../language/ast'; +import type { ASTVisitor } from '../language/visitor'; import { GraphQLSchema } from '../type/schema'; import type { GraphQLInputType, @@ -76,7 +77,7 @@ function visitUsingRules( schema: GraphQLSchema, typeInfo: TypeInfo, documentAST: DocumentNode, - rules: $ReadOnlyArray, + rules: $ReadOnlyArray<(ValidationContext) => ASTVisitor>, ): $ReadOnlyArray { const context = new ValidationContext(schema, documentAST, typeInfo); const visitors = rules.map(rule => rule(context));