From 13ece495bb3fff39185a0ea5c671a28ad27c34f3 Mon Sep 17 00:00:00 2001 From: Naman Kumar Date: Thu, 17 Sep 2020 19:51:14 +0530 Subject: [PATCH] Adding experimental online parser (#2770) * Adding experimental online parser * Improve Typings * Update grammar structure * Move rules to js * Fix build Co-authored-by: Ivan Goncharov --- .../experimentalOnlineParser/README.md | 26 + .../experimentalOnlineParser/grammar.d.ts | 1006 +++++++++++++++++ .../experimentalOnlineParser/grammar.js | 999 ++++++++++++++++ .../experimentalOnlineParser/index.d.ts | 6 + .../experimentalOnlineParser/index.js | 6 + .../onlineParser.d.ts | 125 ++ .../experimentalOnlineParser/onlineParser.js | 722 ++++++++++++ 7 files changed, 2890 insertions(+) create mode 100644 src/language/experimentalOnlineParser/README.md create mode 100644 src/language/experimentalOnlineParser/grammar.d.ts create mode 100644 src/language/experimentalOnlineParser/grammar.js create mode 100644 src/language/experimentalOnlineParser/index.d.ts create mode 100644 src/language/experimentalOnlineParser/index.js create mode 100644 src/language/experimentalOnlineParser/onlineParser.d.ts create mode 100644 src/language/experimentalOnlineParser/onlineParser.js diff --git a/src/language/experimentalOnlineParser/README.md b/src/language/experimentalOnlineParser/README.md new file mode 100644 index 0000000000..dfcd9996d7 --- /dev/null +++ b/src/language/experimentalOnlineParser/README.md @@ -0,0 +1,26 @@ +## Experimental Online Parser + +This directory contains an experimental online parser based on JSON rules set. It is a state-full parser which parses a source string incrementally i.e. emits a token each time. + +The parser is being migrated from graphiql to graphql-js and may have frequent breaking changes. + +Example: + +```js +import { OnlineParser } from 'graphql/language/experimentalOnlineParser'; + +const source = ` + query SomeQuery { + some_field { + another_field + } + } +`; + +const parser = new OnlineParser(source); +let token; + +do { + token = parser.parseToken(); +} while (token.kind !== '' && token.kind !== 'Invalid'); +``` diff --git a/src/language/experimentalOnlineParser/grammar.d.ts b/src/language/experimentalOnlineParser/grammar.d.ts new file mode 100644 index 0000000000..6e71a66a72 --- /dev/null +++ b/src/language/experimentalOnlineParser/grammar.d.ts @@ -0,0 +1,1006 @@ +export interface GraphQLGrammarType { + [name: string]: GraphQLGrammarRule; +} + +export type GraphQLGrammarRule = + | GraphQLGrammarRuleName + | GraphQLGrammarRuleConstraint + | GraphQLGrammarConstraintsSet; + +export type GraphQLGrammarRuleName = string; + +export type GraphQLGrammarRuleConstraint = + | GraphQLGrammarTokenConstraint + | GraphQLGrammarOfTypeConstraint + | GraphQLGrammarListOfTypeConstraint + | GraphQLGrammarPeekConstraint; + +export type GraphQLGrammarConstraintsSet = Array< + GraphQLGrammarRuleName | GraphQLGrammarRuleConstraint +>; + +export interface GraphQLGrammarBaseRuleConstraint { + butNot?: GraphQLGrammarTokenConstraint | Array; + optional?: boolean; + eatNextOnFail?: boolean; +} + +export interface GraphQLGrammarTokenConstraint + extends GraphQLGrammarBaseRuleConstraint { + token: + | '!' + | '$' + | '&' + | '(' + | ')' + | '...' + | ':' + | '=' + | '@' + | '[' + | ']' + | '{' + | '}' + | '|' + | 'Name' + | 'Int' + | 'Float' + | 'String' + | 'BlockString' + | 'Comment'; + ofValue?: string; + oneOf?: Array; + tokenName?: string; + definitionName?: boolean; + typeName?: boolean; +} + +export interface GraphQLGrammarOfTypeConstraint + extends GraphQLGrammarBaseRuleConstraint { + ofType: GraphQLGrammarRule; + tokenName?: string; +} + +export interface GraphQLGrammarListOfTypeConstraint + extends GraphQLGrammarBaseRuleConstraint { + listOfType: GraphQLGrammarRuleName; +} + +export interface GraphQLGrammarPeekConstraint + extends GraphQLGrammarBaseRuleConstraint { + peek: Array; +} + +export interface GraphQLGrammarPeekConstraintCondition { + ifCondition: GraphQLGrammarTokenConstraint; + expect: GraphQLGrammarRule; + end?: boolean; +} + +const grammar: GraphQLGrammarType = { + Name: { token: 'Name' }, + String: { token: 'String' }, + BlockString: { token: 'BlockString' }, + + Document: { listOfType: 'Definition' }, + Definition: { + peek: [ + { + ifCondition: { + token: 'Name', + oneOf: ['query', 'mutation', 'subscription'], + }, + expect: 'OperationDefinition', + }, + { + ifCondition: { token: 'Name', ofValue: 'fragment' }, + expect: 'FragmentDefinition', + }, + { + ifCondition: { + token: 'Name', + oneOf: [ + 'schema', + 'scalar', + 'type', + 'interface', + 'union', + 'enum', + 'input', + 'directive', + ], + }, + expect: 'TypeSystemDefinition', + }, + { + ifCondition: { token: 'Name', ofValue: 'extend' }, + expect: 'TypeSystemExtension', + }, + { + ifCondition: { token: '{' }, + expect: 'OperationDefinition', + }, + { + ifCondition: 'String', + expect: 'TypeSystemDefinition', + }, + { + ifCondition: 'BlockString', + expect: 'TypeSystemDefinition', + }, + ], + }, + + OperationDefinition: { + peek: [ + { + ifCondition: { token: '{' }, + expect: 'SelectionSet', + }, + { + ifCondition: { + token: 'Name', + oneOf: ['query', 'mutation', 'subscription'], + }, + expect: [ + 'OperationType', + { + token: 'Name', + optional: true, + tokenName: 'OperationName', + definitionName: true, + }, + { ofType: 'VariableDefinitions', optional: true }, + { ofType: 'Directives', optional: true }, + 'SelectionSet', + ], + }, + ], + }, + OperationType: { + ofType: 'OperationTypeName', + }, + OperationTypeName: { + token: 'Name', + oneOf: ['query', 'mutation', 'subscription'], + definitionName: true, + }, + SelectionSet: [{ token: '{' }, { listOfType: 'Selection' }, { token: '}' }], + Selection: { + peek: [ + { + ifCondition: { token: '...' }, + expect: 'Fragment', + }, + { + ifCondition: { token: 'Name' }, + expect: 'Field', + }, + ], + }, + + Field: [ + { + ofType: 'Alias', + optional: true, + eatNextOnFail: true, + definitionName: true, + }, + { token: 'Name', tokenName: 'FieldName', definitionName: true }, + { ofType: 'Arguments', optional: true }, + { ofType: 'Directives', optional: true }, + { ofType: 'SelectionSet', optional: true }, + ], + + Arguments: [{ token: '(' }, { listOfType: 'Argument' }, { token: ')' }], + Argument: [ + { token: 'Name', tokenName: 'ArgumentName', definitionName: true }, + { token: ':' }, + 'Value', + ], + + Alias: [ + { token: 'Name', tokenName: 'AliasName', definitionName: true }, + { token: ':' }, + ], + + Fragment: [ + { token: '...' }, + { + peek: [ + { + ifCondition: 'FragmentName', + expect: 'FragmentSpread', + }, + { + ifCondition: { token: 'Name', ofValue: 'on' }, + expect: 'InlineFragment', + }, + { + ifCondition: { token: '@' }, + expect: 'InlineFragment', + }, + { + ifCondition: { token: '{' }, + expect: 'InlineFragment', + }, + ], + }, + ], + + FragmentSpread: ['FragmentName', { ofType: 'Directives', optional: true }], + FragmentDefinition: [ + { + token: 'Name', + ofValue: 'fragment', + tokenName: 'FragmentDefinitionKeyword', + }, + 'FragmentName', + 'TypeCondition', + { ofType: 'Directives', optional: true }, + 'SelectionSet', + ], + FragmentName: { + token: 'Name', + butNot: { token: 'Name', ofValue: 'on' }, + definitionName: true, + }, + + TypeCondition: [ + { token: 'Name', ofValue: 'on', tokenName: 'OnKeyword' }, + 'TypeName', + ], + + InlineFragment: [ + { ofType: 'TypeCondition', optional: true }, + { ofType: 'Directives', optional: true }, + 'SelectionSet', + ], + + Value: { + peek: [ + { + ifCondition: { token: '$' }, + expect: 'Variable', + }, + { + ifCondition: 'IntValue', + expect: { ofType: 'IntValue', tokenName: 'NumberValue' }, + }, + { + ifCondition: 'FloatValue', + expect: { ofType: 'FloatValue', tokenName: 'NumberValue' }, + }, + { + ifCondition: 'BooleanValue', + expect: { ofType: 'BooleanValue', tokenName: 'BooleanValue' }, + }, + { + ifCondition: 'EnumValue', + expect: { ofType: 'EnumValue', tokenName: 'EnumValue' }, + }, + { + ifCondition: 'String', + expect: { ofType: 'String', tokenName: 'StringValue' }, + }, + { + ifCondition: 'BlockString', + expect: { ofType: 'BlockString', tokenName: 'StringValue' }, + }, + { + ifCondition: 'NullValue', + expect: { ofType: 'NullValue', tokenName: 'NullValue' }, + }, + { + ifCondition: { token: '[' }, + expect: 'ListValue', + }, + { + ifCondition: { token: '{' }, + expect: 'ObjectValue', + }, + ], + }, + + ConstValue: { + peek: [ + { + ifCondition: 'IntValue', + expect: { ofType: 'IntValue' }, + }, + { + ifCondition: 'FloatValue', + expect: { ofType: 'FloatValue' }, + }, + { + ifCondition: 'BooleanValue', + expect: 'BooleanValue', + }, + { + ifCondition: 'EnumValue', + expect: 'EnumValue', + }, + { + ifCondition: 'String', + expect: { ofType: 'String', tokenName: 'StringValue' }, + }, + { + ifCondition: 'BlockString', + expect: { token: 'BlockString', tokenName: 'StringValue' }, + }, + { + ifCondition: 'NullValue', + expect: 'NullValue', + }, + { + ifCondition: { token: '[' }, + expect: 'ConstListValue', + }, + { + ifCondition: { token: '{' }, + expect: 'ObjectValue', + }, + ], + }, + + IntValue: { token: 'Int' }, + + FloatValue: { token: 'Float' }, + + StringValue: { + peek: [ + { + ifCondition: { token: 'String' }, + expect: { token: 'String', tokenName: 'StringValue' }, + }, + { + ifCondition: { token: 'BlockString' }, + expect: { token: 'BlockString', tokenName: 'StringValue' }, + }, + ], + }, + + BooleanValue: { + token: 'Name', + oneOf: ['true', 'false'], + tokenName: 'BooleanValue', + }, + + NullValue: { + token: 'Name', + ofValue: 'null', + tokenName: 'NullValue', + }, + + EnumValue: { + token: 'Name', + butNot: { token: 'Name', oneOf: ['null', 'true', 'false'] }, + tokenName: 'EnumValue', + }, + + ListValue: [ + { token: '[' }, + { listOfType: 'Value', optional: true }, + { token: ']' }, + ], + + ConstListValue: [ + { token: '[' }, + { listOfType: 'ConstValue', optional: true }, + { token: ']' }, + ], + + ObjectValue: [ + { token: '{' }, + { listOfType: 'ObjectField', optional: true }, + { token: '}' }, + ], + ObjectField: [ + { token: 'Name', tokenName: 'ObjectFieldName' }, + { token: ':' }, + { ofType: 'ConstValue' }, + ], + + Variable: [ + { token: '$', tokenName: 'VariableName' }, + { token: 'Name', tokenName: 'VariableName' }, + ], + VariableDefinitions: [ + { token: '(' }, + { listOfType: 'VariableDefinition' }, + { token: ')' }, + ], + VariableDefinition: [ + 'Variable', + { token: ':' }, + 'Type', + { ofType: 'DefaultValue', optional: true }, + ], + DefaultValue: [{ token: '=' }, 'ConstValue'], + + TypeName: { token: 'Name', tokenName: 'TypeName', typeName: true }, + + Type: { + peek: [ + { + ifCondition: { token: 'Name' }, + expect: ['TypeName', { token: '!', optional: true }], + }, + { + ifCondition: { token: '[' }, + expect: 'ListType', + }, + ], + }, + ListType: [ + { token: '[' }, + { listOfType: 'Type' }, + { token: ']' }, + { token: '!', optional: true }, + ], + + Directives: { listOfType: 'Directive' }, + Directive: [ + { token: '@', tokenName: 'DirectiveName' }, + { token: 'Name', tokenName: 'DirectiveName' }, + { ofType: 'Arguments', optional: true }, + ], + + TypeSystemDefinition: [ + { ofType: 'Description', optional: true }, + { + peek: [ + { + ifCondition: { + target: 'Name', + ofValue: 'schema', + }, + expect: 'SchemaDefinition', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'scalar', + }, + expect: 'ScalarTypeDefinition', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'type', + }, + expect: 'ObjectTypeDefinition', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'interface', + }, + expect: 'InterfaceTypeDefinition', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'union', + }, + expect: 'UnionTypeDefinition', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'enum', + }, + expect: 'EnumTypeDefinition', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'input', + }, + expect: 'InputObjectTypeDefinition', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'directive', + }, + expect: 'DirectiveDefinition', + }, + ], + }, + ], + + TypeSystemExtension: { + peek: [ + { + ifCondition: { + target: 'Name', + ofValue: 'schema', + }, + expect: 'SchemaExtension', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'scalar', + }, + expect: 'ScalarTypeExtension', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'type', + }, + expect: 'ObjectTypeExtension', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'interface', + }, + expect: 'InterfaceTypeExtension', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'union', + }, + expect: 'UnionTypeExtension', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'enum', + }, + expect: 'EnumTypeExtension', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'input', + }, + expect: 'InputObjectTypeExtension', + }, + ], + }, + + SchemaDefinition: [ + { + token: 'Name', + ofValue: 'schema', + tokenName: 'SchemaDefinitionKeyword', + }, + { ofType: 'Directives', optional: true }, + { token: '{' }, + { listOfType: 'RootOperationTypeDefinition' }, + { token: '}' }, + ], + RootOperationTypeDefinition: [ + 'OperationType', + { token: ':' }, + { token: 'Name', tokenName: 'OperationTypeDefinitionName' }, + ], + + SchemaExtension: [ + { token: 'Name', ofValue: 'extend' }, + { token: 'Name', ofValue: 'schema' }, + 'Name', + { + peek: [ + { + ifCondition: { token: '@' }, + expect: [ + 'Directives', + { + ofType: [ + { token: '{' }, + { listOfType: 'RootOperationTypeDefinition' }, + { token: '}' }, + ], + optional: true, + }, + ], + }, + { + ifCondition: { token: '{' }, + expect: [ + { token: '{' }, + { listOfType: 'RootOperationTypeDefinition' }, + { token: '}' }, + ], + }, + ], + }, + ], + + Description: 'StringValue', + + ScalarTypeDefinition: [ + { ofType: 'Description', optional: true }, + { + token: 'Name', + ofValue: 'scalar', + tokenName: 'ScalarDefinitionKeyword', + }, + 'TypeName', + { ofType: 'Directives', optional: true }, + ], + + ScalarTypeExtension: [ + { + token: 'Name', + ofValue: 'extend', + tokenName: 'ExtendDefinitionKeyword', + }, + { + token: 'Name', + ofValue: 'scalar', + tokenName: 'ScalarDefinitionKeyword', + }, + 'TypeName', + 'Directives', + ], + + ObjectTypeDefinition: [ + { ofType: 'Description', optional: true }, + { + token: 'Name', + ofValue: 'type', + tokenName: 'TypeDefinitionKeyword', + }, + 'TypeName', + { ofType: 'ImplementsInterfaces', optional: true }, + { ofType: 'Directives', optional: true }, + { ofType: 'FieldsDefinition', optional: true }, + ], + ImplementsInterfaces: [ + { + token: 'Name', + ofValue: 'implements', + tokenName: 'ImplementsKeyword', + }, + { token: '&', optional: true }, + 'TypeName', + { + listOfType: 'ImplementsAdditionalInterfaceName', + optional: true, + }, + ], + ImplementsAdditionalInterfaceName: [{ token: '&' }, 'TypeName'], + FieldsDefinition: [ + { token: '{' }, + { listOfType: 'FieldDefinition' }, + { token: '}' }, + ], + FieldDefinition: [ + { ofType: 'Description', optional: true }, + { token: 'Name', tokenName: 'AliasName', definitionName: true }, + { ofType: 'ArgumentsDefinition', optional: true }, + { token: ':' }, + 'Type', + { ofType: 'Directives', optional: true }, + ], + + ArgumentsDefinition: [ + { token: '(' }, + { listOfType: 'InputValueDefinition' }, + { token: ')' }, + ], + InputValueDefinition: [ + { ofType: 'Description', optional: true }, + { token: 'Name', tokenName: 'ArgumentName' }, + { token: ':' }, + 'Type', + { ofType: 'DefaultValue', optional: true }, + { ofType: 'Directives', optional: true }, + ], + + ObjectTypeExtension: [ + { + token: 'Name', + ofValue: 'extend', + tokenName: 'ExtendDefinitionKeyword', + }, + { + token: 'Name', + ofValue: 'type', + tokenName: 'TypeDefinitionKeyword', + }, + 'TypeName', + { + peek: [ + { + ifCondition: { token: 'Name', ofValue: 'interface' }, + expect: [ + 'ImplementsInterfaces', + { + peek: [ + { + ifCondition: { token: '@' }, + expect: [ + 'Directives', + { ofType: 'FieldsDefinition', optional: true }, + ], + }, + { + ifCondition: { token: '{' }, + expect: 'FieldsDefinition', + }, + ], + optional: true, + }, + ], + }, + { + ifCondition: { token: '@' }, + expect: [ + 'Directives', + { ofType: 'FieldsDefinition', optional: true }, + ], + }, + { + ifCondition: { token: '{' }, + expect: 'FieldsDefinition', + }, + ], + }, + ], + + InterfaceTypeDefinition: [ + { ofType: 'Description', optional: true }, + { + token: 'Name', + ofValue: 'interface', + tokenName: 'InterfaceDefinitionKeyword', + }, + 'TypeName', + { ofType: 'Directives', optional: true }, + { ofType: 'FieldsDefinition', optional: true }, + ], + + InterfaceTypeExtension: [ + { + token: 'Name', + ofValue: 'extend', + tokenName: 'ExtendDefinitionKeyword', + }, + { + token: 'Name', + ofValue: 'interface', + tokenName: 'InterfaceDefinitionKeyword', + }, + 'TypeName', + { + peek: [ + { + ifCondition: { token: '@' }, + expect: [ + 'Directives', + { ofType: 'FieldsDefinition', optional: true }, + ], + }, + { + ifCondition: { token: '{' }, + expect: 'FieldsDefinition', + }, + ], + }, + ], + + UnionTypeDefinition: [ + { ofType: 'Description', optional: true }, + { + token: 'Name', + ofValue: 'union', + tokenName: 'UnionDefinitionKeyword', + }, + 'TypeName', + { ofType: 'Directives', optional: true }, + { ofType: 'UnionMemberTypes', optional: true }, + ], + + UnionMemberTypes: [ + { token: '=' }, + { token: '|', optional: true }, + 'Name', + { + listOfType: 'UnionMemberAdditionalTypeName', + optional: true, + }, + ], + + UnionMemberAdditionalTypeName: [{ token: '|' }, 'TypeName'], + + UnionTypeExtension: [ + { + token: 'Name', + ofValue: 'extend', + tokenName: 'ExtendDefinitionKeyword', + }, + { + token: 'Name', + ofValue: 'union', + tokenName: 'UnionDefinitionKeyword', + }, + 'TypeName', + { + peek: [ + { + ifCondition: { token: '@' }, + expect: [ + 'Directives', + { ofType: 'UnionMemberTypes', optional: true }, + ], + }, + { + ifCondition: { token: '=' }, + expect: 'UnionMemberTypes', + }, + ], + }, + ], + + EnumTypeDefinition: [ + { ofType: 'Description', optional: true }, + { + token: 'Name', + ofValue: 'enum', + tokenName: 'EnumDefinitionKeyword', + }, + 'TypeName', + { ofType: 'Directives', optional: true }, + { ofType: 'EnumValuesDefinition', optional: true }, + ], + EnumValuesDefinition: [ + { token: '{' }, + { listOfType: 'EnumValueDefinition' }, + { token: '}' }, + ], + EnumValueDefinition: [ + { ofType: 'Description', optional: true }, + 'EnumValue', + { ofType: 'Directives', optional: true }, + ], + + EnumTypeExtension: [ + { + token: 'Name', + ofValue: 'extend', + tokenName: 'ExtendDefinitionKeyword', + }, + { + token: 'Name', + ofValue: 'enum', + tokenName: 'EnumDefinitionKeyword', + }, + 'TypeName', + { + peek: [ + { + ifCondition: { token: '@' }, + expect: [ + 'Directives', + { ofType: 'EnumValuesDefinition', optional: true }, + ], + }, + { + ifCondition: { token: '{' }, + expect: 'EnumValuesDefinition', + }, + ], + }, + ], + + InputObjectTypeDefinition: [ + { ofType: 'Description', optional: true }, + { + token: 'Name', + ofValue: 'input', + tokenName: 'InputDefinitionKeyword', + }, + 'TypeName', + { ofType: 'Directives', optional: true }, + { ofType: 'InputFieldsDefinition', optional: true }, + ], + InputFieldsDefinition: [ + { token: '{' }, + { listOfType: 'InputValueDefinition' }, + { token: '}' }, + ], + + InputObjectTypeExtension: [ + { + token: 'Name', + ofValue: 'extend', + tokenName: 'ExtendDefinitionKeyword', + }, + { + token: 'Name', + ofValue: 'input', + tokenName: 'InputDefinitionKeyword', + }, + 'TypeName', + { + peek: [ + { + ifCondition: { token: '@' }, + expect: [ + 'Directives', + { ofType: 'InputFieldsDefinition', optional: true }, + ], + }, + { + ifCondition: { token: '{' }, + expect: 'InputFieldsDefinition', + }, + ], + }, + ], + + DirectiveDefinition: [ + { ofType: 'Description', optional: true }, + { + token: 'Name', + ofValue: 'directive', + tokenName: 'DirectiveDefinitionKeyword', + }, + { token: '@', tokenName: 'DirectiveName' }, + { token: 'Name', tokenName: 'DirectiveName' }, + { ofType: 'ArgumentsDefinition', optional: true }, + { token: 'Name', ofValue: 'on', tokenName: 'OnKeyword' }, + 'DirectiveLocations', + ], + DirectiveLocations: [ + { token: '|', optional: true }, + 'DirectiveLocation', + { + listOfType: 'DirectiveLocationAdditionalName', + optional: true, + }, + ], + DirectiveLocationAdditionalName: [{ token: '|' }, 'DirectiveLocation'], + DirectiveLocation: { + peek: [ + { + ifCondition: 'ExecutableDirectiveLocation', + expect: 'ExecutableDirectiveLocation', + }, + { + ifCondition: 'TypeSystemDirectiveLocation', + expect: 'TypeSystemDirectiveLocation', + }, + ], + }, + ExecutableDirectiveLocation: { + token: 'Name', + oneOf: [ + 'QUERY', + 'MUTATION', + 'SUBSCRIPTION', + 'FIELD', + 'FRAGMENT_DEFINITION', + 'FRAGMENT_SPREAD', + 'INLINE_FRAGMENT', + ], + tokenName: 'EnumValue', + }, + TypeSystemDirectiveLocation: { + token: 'Name', + oneOf: [ + 'SCHEMA', + 'SCALAR', + 'OBJECT', + 'FIELD_DEFINITION', + 'ARGUMENT_DEFINITION', + 'INTERFACE', + 'UNION', + 'ENUM', + 'ENUM_VALUE', + 'INPUT_OBJECT', + 'INPUT_FIELD_DEFINITION', + ], + tokenName: 'EnumValue', + }, +}; + +export default grammar; diff --git a/src/language/experimentalOnlineParser/grammar.js b/src/language/experimentalOnlineParser/grammar.js new file mode 100644 index 0000000000..0ab7788534 --- /dev/null +++ b/src/language/experimentalOnlineParser/grammar.js @@ -0,0 +1,999 @@ +export type GraphQLGrammarType = {| + [name: string]: GraphQLGrammarRule, +|}; +export type GraphQLGrammarRuleName = string; +export type GraphQLGrammarRuleConstraint = + | GraphQLGrammarTokenConstraint + | GraphQLGrammarOfTypeConstraint + | GraphQLGrammarListOfTypeConstraint + | GraphQLGrammarPeekConstraint; +export type GraphQLGrammarConstraintsSet = Array< + GraphQLGrammarRuleName | GraphQLGrammarRuleConstraint, +>; +export type GraphQLGrammarRule = + | GraphQLGrammarRuleName + | GraphQLGrammarRuleConstraint + | GraphQLGrammarConstraintsSet; +export interface GraphQLGrammarBaseRuleConstraint { + butNot?: + | ?GraphQLGrammarTokenConstraint + | ?Array; + optional?: boolean; + eatNextOnFail?: boolean; +} +export interface GraphQLGrammarTokenConstraint + extends GraphQLGrammarBaseRuleConstraint { + token: + | '!' + | '$' + | '&' + | '(' + | ')' + | '...' + | ':' + | '=' + | '@' + | '[' + | ']' + | '{' + | '}' + | '|' + | 'Name' + | 'Int' + | 'Float' + | 'String' + | 'BlockString' + | 'Comment'; + ofValue?: ?string; + oneOf?: ?Array; + tokenName?: string; + definitionName?: boolean; + typeName?: boolean; +} +export interface GraphQLGrammarOfTypeConstraint + extends GraphQLGrammarBaseRuleConstraint { + ofType: GraphQLGrammarRule; + tokenName?: string; +} +export interface GraphQLGrammarListOfTypeConstraint + extends GraphQLGrammarBaseRuleConstraint { + listOfType: GraphQLGrammarRuleName; +} +export interface GraphQLGrammarPeekConstraint + extends GraphQLGrammarBaseRuleConstraint { + peek: Array; +} +export interface GraphQLGrammarPeekConstraintCondition { + ifCondition: GraphQLGrammarTokenConstraint; + expect: GraphQLGrammarRule; + end?: boolean; +} + +const grammar: GraphQLGrammarType = ({ + Name: { token: 'Name' }, + String: { token: 'String' }, + BlockString: { token: 'BlockString' }, + + Document: { listOfType: 'Definition' }, + Definition: { + peek: [ + { + ifCondition: { + token: 'Name', + oneOf: ['query', 'mutation', 'subscription'], + }, + expect: 'OperationDefinition', + }, + { + ifCondition: { token: 'Name', ofValue: 'fragment' }, + expect: 'FragmentDefinition', + }, + { + ifCondition: { + token: 'Name', + oneOf: [ + 'schema', + 'scalar', + 'type', + 'interface', + 'union', + 'enum', + 'input', + 'directive', + ], + }, + expect: 'TypeSystemDefinition', + }, + { + ifCondition: { token: 'Name', ofValue: 'extend' }, + expect: 'TypeSystemExtension', + }, + { + ifCondition: { token: '{' }, + expect: 'OperationDefinition', + }, + { + ifCondition: 'String', + expect: 'TypeSystemDefinition', + }, + { + ifCondition: 'BlockString', + expect: 'TypeSystemDefinition', + }, + ], + }, + + OperationDefinition: { + peek: [ + { + ifCondition: { token: '{' }, + expect: 'SelectionSet', + }, + { + ifCondition: { + token: 'Name', + oneOf: ['query', 'mutation', 'subscription'], + }, + expect: [ + 'OperationType', + { + token: 'Name', + optional: true, + tokenName: 'OperationName', + definitionName: true, + }, + { ofType: 'VariableDefinitions', optional: true }, + { ofType: 'Directives', optional: true }, + 'SelectionSet', + ], + }, + ], + }, + OperationType: { + ofType: 'OperationTypeName', + }, + OperationTypeName: { + token: 'Name', + oneOf: ['query', 'mutation', 'subscription'], + definitionName: true, + }, + SelectionSet: [{ token: '{' }, { listOfType: 'Selection' }, { token: '}' }], + Selection: { + peek: [ + { + ifCondition: { token: '...' }, + expect: 'Fragment', + }, + { + ifCondition: { token: 'Name' }, + expect: 'Field', + }, + ], + }, + + Field: [ + { + ofType: 'Alias', + optional: true, + eatNextOnFail: true, + definitionName: true, + }, + { token: 'Name', tokenName: 'FieldName', definitionName: true }, + { ofType: 'Arguments', optional: true }, + { ofType: 'Directives', optional: true }, + { ofType: 'SelectionSet', optional: true }, + ], + + Arguments: [{ token: '(' }, { listOfType: 'Argument' }, { token: ')' }], + Argument: [ + { token: 'Name', tokenName: 'ArgumentName', definitionName: true }, + { token: ':' }, + 'Value', + ], + + Alias: [ + { token: 'Name', tokenName: 'AliasName', definitionName: true }, + { token: ':' }, + ], + + Fragment: [ + { token: '...' }, + { + peek: [ + { + ifCondition: 'FragmentName', + expect: 'FragmentSpread', + }, + { + ifCondition: { token: 'Name', ofValue: 'on' }, + expect: 'InlineFragment', + }, + { + ifCondition: { token: '@' }, + expect: 'InlineFragment', + }, + { + ifCondition: { token: '{' }, + expect: 'InlineFragment', + }, + ], + }, + ], + + FragmentSpread: ['FragmentName', { ofType: 'Directives', optional: true }], + FragmentDefinition: [ + { + token: 'Name', + ofValue: 'fragment', + tokenName: 'FragmentDefinitionKeyword', + }, + 'FragmentName', + 'TypeCondition', + { ofType: 'Directives', optional: true }, + 'SelectionSet', + ], + FragmentName: { + token: 'Name', + butNot: { token: 'Name', ofValue: 'on' }, + definitionName: true, + }, + + TypeCondition: [ + { token: 'Name', ofValue: 'on', tokenName: 'OnKeyword' }, + 'TypeName', + ], + + InlineFragment: [ + { ofType: 'TypeCondition', optional: true }, + { ofType: 'Directives', optional: true }, + 'SelectionSet', + ], + + Value: { + peek: [ + { + ifCondition: { token: '$' }, + expect: 'Variable', + }, + { + ifCondition: 'IntValue', + expect: { ofType: 'IntValue', tokenName: 'NumberValue' }, + }, + { + ifCondition: 'FloatValue', + expect: { ofType: 'FloatValue', tokenName: 'NumberValue' }, + }, + { + ifCondition: 'BooleanValue', + expect: { ofType: 'BooleanValue', tokenName: 'BooleanValue' }, + }, + { + ifCondition: 'EnumValue', + expect: { ofType: 'EnumValue', tokenName: 'EnumValue' }, + }, + { + ifCondition: 'String', + expect: { ofType: 'String', tokenName: 'StringValue' }, + }, + { + ifCondition: 'BlockString', + expect: { ofType: 'BlockString', tokenName: 'StringValue' }, + }, + { + ifCondition: 'NullValue', + expect: { ofType: 'NullValue', tokenName: 'NullValue' }, + }, + { + ifCondition: { token: '[' }, + expect: 'ListValue', + }, + { + ifCondition: { token: '{' }, + expect: 'ObjectValue', + }, + ], + }, + + ConstValue: { + peek: [ + { + ifCondition: 'IntValue', + expect: { ofType: 'IntValue' }, + }, + { + ifCondition: 'FloatValue', + expect: { ofType: 'FloatValue' }, + }, + { + ifCondition: 'BooleanValue', + expect: 'BooleanValue', + }, + { + ifCondition: 'EnumValue', + expect: 'EnumValue', + }, + { + ifCondition: 'String', + expect: { ofType: 'String', tokenName: 'StringValue' }, + }, + { + ifCondition: 'BlockString', + expect: { token: 'BlockString', tokenName: 'StringValue' }, + }, + { + ifCondition: 'NullValue', + expect: 'NullValue', + }, + { + ifCondition: { token: '[' }, + expect: 'ConstListValue', + }, + { + ifCondition: { token: '{' }, + expect: 'ObjectValue', + }, + ], + }, + + IntValue: { token: 'Int' }, + + FloatValue: { token: 'Float' }, + + StringValue: { + peek: [ + { + ifCondition: { token: 'String' }, + expect: { token: 'String', tokenName: 'StringValue' }, + }, + { + ifCondition: { token: 'BlockString' }, + expect: { token: 'BlockString', tokenName: 'StringValue' }, + }, + ], + }, + + BooleanValue: { + token: 'Name', + oneOf: ['true', 'false'], + tokenName: 'BooleanValue', + }, + + NullValue: { + token: 'Name', + ofValue: 'null', + tokenName: 'NullValue', + }, + + EnumValue: { + token: 'Name', + butNot: { token: 'Name', oneOf: ['null', 'true', 'false'] }, + tokenName: 'EnumValue', + }, + + ListValue: [ + { token: '[' }, + { listOfType: 'Value', optional: true }, + { token: ']' }, + ], + + ConstListValue: [ + { token: '[' }, + { listOfType: 'ConstValue', optional: true }, + { token: ']' }, + ], + + ObjectValue: [ + { token: '{' }, + { listOfType: 'ObjectField', optional: true }, + { token: '}' }, + ], + ObjectField: [ + { token: 'Name', tokenName: 'ObjectFieldName' }, + { token: ':' }, + { ofType: 'ConstValue' }, + ], + + Variable: [ + { token: '$', tokenName: 'VariableName' }, + { token: 'Name', tokenName: 'VariableName' }, + ], + VariableDefinitions: [ + { token: '(' }, + { listOfType: 'VariableDefinition' }, + { token: ')' }, + ], + VariableDefinition: [ + 'Variable', + { token: ':' }, + 'Type', + { ofType: 'DefaultValue', optional: true }, + ], + DefaultValue: [{ token: '=' }, 'ConstValue'], + + TypeName: { token: 'Name', tokenName: 'TypeName', typeName: true }, + + Type: { + peek: [ + { + ifCondition: { token: 'Name' }, + expect: ['TypeName', { token: '!', optional: true }], + }, + { + ifCondition: { token: '[' }, + expect: 'ListType', + }, + ], + }, + ListType: [ + { token: '[' }, + { listOfType: 'Type' }, + { token: ']' }, + { token: '!', optional: true }, + ], + + Directives: { listOfType: 'Directive' }, + Directive: [ + { token: '@', tokenName: 'DirectiveName' }, + { token: 'Name', tokenName: 'DirectiveName' }, + { ofType: 'Arguments', optional: true }, + ], + + TypeSystemDefinition: [ + { ofType: 'Description', optional: true }, + { + peek: [ + { + ifCondition: { + target: 'Name', + ofValue: 'schema', + }, + expect: 'SchemaDefinition', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'scalar', + }, + expect: 'ScalarTypeDefinition', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'type', + }, + expect: 'ObjectTypeDefinition', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'interface', + }, + expect: 'InterfaceTypeDefinition', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'union', + }, + expect: 'UnionTypeDefinition', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'enum', + }, + expect: 'EnumTypeDefinition', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'input', + }, + expect: 'InputObjectTypeDefinition', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'directive', + }, + expect: 'DirectiveDefinition', + }, + ], + }, + ], + + TypeSystemExtension: { + peek: [ + { + ifCondition: { + target: 'Name', + ofValue: 'schema', + }, + expect: 'SchemaExtension', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'scalar', + }, + expect: 'ScalarTypeExtension', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'type', + }, + expect: 'ObjectTypeExtension', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'interface', + }, + expect: 'InterfaceTypeExtension', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'union', + }, + expect: 'UnionTypeExtension', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'enum', + }, + expect: 'EnumTypeExtension', + }, + { + ifCondition: { + target: 'Name', + ofValue: 'input', + }, + expect: 'InputObjectTypeExtension', + }, + ], + }, + + SchemaDefinition: [ + { + token: 'Name', + ofValue: 'schema', + tokenName: 'SchemaDefinitionKeyword', + }, + { ofType: 'Directives', optional: true }, + { token: '{' }, + { listOfType: 'RootOperationTypeDefinition' }, + { token: '}' }, + ], + RootOperationTypeDefinition: [ + 'OperationType', + { token: ':' }, + { token: 'Name', tokenName: 'OperationTypeDefinitionName' }, + ], + + SchemaExtension: [ + { token: 'Name', ofValue: 'extend' }, + { token: 'Name', ofValue: 'schema' }, + 'Name', + { + peek: [ + { + ifCondition: { token: '@' }, + expect: [ + 'Directives', + { + ofType: [ + { token: '{' }, + { listOfType: 'RootOperationTypeDefinition' }, + { token: '}' }, + ], + optional: true, + }, + ], + }, + { + ifCondition: { token: '{' }, + expect: [ + { token: '{' }, + { listOfType: 'RootOperationTypeDefinition' }, + { token: '}' }, + ], + }, + ], + }, + ], + + Description: 'StringValue', + + ScalarTypeDefinition: [ + { ofType: 'Description', optional: true }, + { + token: 'Name', + ofValue: 'scalar', + tokenName: 'ScalarDefinitionKeyword', + }, + 'TypeName', + { ofType: 'Directives', optional: true }, + ], + + ScalarTypeExtension: [ + { + token: 'Name', + ofValue: 'extend', + tokenName: 'ExtendDefinitionKeyword', + }, + { + token: 'Name', + ofValue: 'scalar', + tokenName: 'ScalarDefinitionKeyword', + }, + 'TypeName', + 'Directives', + ], + + ObjectTypeDefinition: [ + { ofType: 'Description', optional: true }, + { + token: 'Name', + ofValue: 'type', + tokenName: 'TypeDefinitionKeyword', + }, + 'TypeName', + { ofType: 'ImplementsInterfaces', optional: true }, + { ofType: 'Directives', optional: true }, + { ofType: 'FieldsDefinition', optional: true }, + ], + ImplementsInterfaces: [ + { + token: 'Name', + ofValue: 'implements', + tokenName: 'ImplementsKeyword', + }, + { token: '&', optional: true }, + 'TypeName', + { + listOfType: 'ImplementsAdditionalInterfaceName', + optional: true, + }, + ], + ImplementsAdditionalInterfaceName: [{ token: '&' }, 'TypeName'], + FieldsDefinition: [ + { token: '{' }, + { listOfType: 'FieldDefinition' }, + { token: '}' }, + ], + FieldDefinition: [ + { ofType: 'Description', optional: true }, + { token: 'Name', tokenName: 'AliasName', definitionName: true }, + { ofType: 'ArgumentsDefinition', optional: true }, + { token: ':' }, + 'Type', + { ofType: 'Directives', optional: true }, + ], + + ArgumentsDefinition: [ + { token: '(' }, + { listOfType: 'InputValueDefinition' }, + { token: ')' }, + ], + InputValueDefinition: [ + { ofType: 'Description', optional: true }, + { token: 'Name', tokenName: 'ArgumentName' }, + { token: ':' }, + 'Type', + { ofType: 'DefaultValue', optional: true }, + { ofType: 'Directives', optional: true }, + ], + + ObjectTypeExtension: [ + { + token: 'Name', + ofValue: 'extend', + tokenName: 'ExtendDefinitionKeyword', + }, + { + token: 'Name', + ofValue: 'type', + tokenName: 'TypeDefinitionKeyword', + }, + 'TypeName', + { + peek: [ + { + ifCondition: { token: 'Name', ofValue: 'interface' }, + expect: [ + 'ImplementsInterfaces', + { + peek: [ + { + ifCondition: { token: '@' }, + expect: [ + 'Directives', + { ofType: 'FieldsDefinition', optional: true }, + ], + }, + { + ifCondition: { token: '{' }, + expect: 'FieldsDefinition', + }, + ], + optional: true, + }, + ], + }, + { + ifCondition: { token: '@' }, + expect: [ + 'Directives', + { ofType: 'FieldsDefinition', optional: true }, + ], + }, + { + ifCondition: { token: '{' }, + expect: 'FieldsDefinition', + }, + ], + }, + ], + + InterfaceTypeDefinition: [ + { ofType: 'Description', optional: true }, + { + token: 'Name', + ofValue: 'interface', + tokenName: 'InterfaceDefinitionKeyword', + }, + 'TypeName', + { ofType: 'Directives', optional: true }, + { ofType: 'FieldsDefinition', optional: true }, + ], + + InterfaceTypeExtension: [ + { + token: 'Name', + ofValue: 'extend', + tokenName: 'ExtendDefinitionKeyword', + }, + { + token: 'Name', + ofValue: 'interface', + tokenName: 'InterfaceDefinitionKeyword', + }, + 'TypeName', + { + peek: [ + { + ifCondition: { token: '@' }, + expect: [ + 'Directives', + { ofType: 'FieldsDefinition', optional: true }, + ], + }, + { + ifCondition: { token: '{' }, + expect: 'FieldsDefinition', + }, + ], + }, + ], + + UnionTypeDefinition: [ + { ofType: 'Description', optional: true }, + { + token: 'Name', + ofValue: 'union', + tokenName: 'UnionDefinitionKeyword', + }, + 'TypeName', + { ofType: 'Directives', optional: true }, + { ofType: 'UnionMemberTypes', optional: true }, + ], + + UnionMemberTypes: [ + { token: '=' }, + { token: '|', optional: true }, + 'Name', + { + listOfType: 'UnionMemberAdditionalTypeName', + optional: true, + }, + ], + + UnionMemberAdditionalTypeName: [{ token: '|' }, 'TypeName'], + + UnionTypeExtension: [ + { + token: 'Name', + ofValue: 'extend', + tokenName: 'ExtendDefinitionKeyword', + }, + { + token: 'Name', + ofValue: 'union', + tokenName: 'UnionDefinitionKeyword', + }, + 'TypeName', + { + peek: [ + { + ifCondition: { token: '@' }, + expect: [ + 'Directives', + { ofType: 'UnionMemberTypes', optional: true }, + ], + }, + { + ifCondition: { token: '=' }, + expect: 'UnionMemberTypes', + }, + ], + }, + ], + + EnumTypeDefinition: [ + { ofType: 'Description', optional: true }, + { + token: 'Name', + ofValue: 'enum', + tokenName: 'EnumDefinitionKeyword', + }, + 'TypeName', + { ofType: 'Directives', optional: true }, + { ofType: 'EnumValuesDefinition', optional: true }, + ], + EnumValuesDefinition: [ + { token: '{' }, + { listOfType: 'EnumValueDefinition' }, + { token: '}' }, + ], + EnumValueDefinition: [ + { ofType: 'Description', optional: true }, + 'EnumValue', + { ofType: 'Directives', optional: true }, + ], + + EnumTypeExtension: [ + { + token: 'Name', + ofValue: 'extend', + tokenName: 'ExtendDefinitionKeyword', + }, + { + token: 'Name', + ofValue: 'enum', + tokenName: 'EnumDefinitionKeyword', + }, + 'TypeName', + { + peek: [ + { + ifCondition: { token: '@' }, + expect: [ + 'Directives', + { ofType: 'EnumValuesDefinition', optional: true }, + ], + }, + { + ifCondition: { token: '{' }, + expect: 'EnumValuesDefinition', + }, + ], + }, + ], + + InputObjectTypeDefinition: [ + { ofType: 'Description', optional: true }, + { + token: 'Name', + ofValue: 'input', + tokenName: 'InputDefinitionKeyword', + }, + 'TypeName', + { ofType: 'Directives', optional: true }, + { ofType: 'InputFieldsDefinition', optional: true }, + ], + InputFieldsDefinition: [ + { token: '{' }, + { listOfType: 'InputValueDefinition' }, + { token: '}' }, + ], + + InputObjectTypeExtension: [ + { + token: 'Name', + ofValue: 'extend', + tokenName: 'ExtendDefinitionKeyword', + }, + { + token: 'Name', + ofValue: 'input', + tokenName: 'InputDefinitionKeyword', + }, + 'TypeName', + { + peek: [ + { + ifCondition: { token: '@' }, + expect: [ + 'Directives', + { ofType: 'InputFieldsDefinition', optional: true }, + ], + }, + { + ifCondition: { token: '{' }, + expect: 'InputFieldsDefinition', + }, + ], + }, + ], + + DirectiveDefinition: [ + { ofType: 'Description', optional: true }, + { + token: 'Name', + ofValue: 'directive', + tokenName: 'DirectiveDefinitionKeyword', + }, + { token: '@', tokenName: 'DirectiveName' }, + { token: 'Name', tokenName: 'DirectiveName' }, + { ofType: 'ArgumentsDefinition', optional: true }, + { token: 'Name', ofValue: 'on', tokenName: 'OnKeyword' }, + 'DirectiveLocations', + ], + DirectiveLocations: [ + { token: '|', optional: true }, + 'DirectiveLocation', + { + listOfType: 'DirectiveLocationAdditionalName', + optional: true, + }, + ], + DirectiveLocationAdditionalName: [{ token: '|' }, 'DirectiveLocation'], + DirectiveLocation: { + peek: [ + { + ifCondition: 'ExecutableDirectiveLocation', + expect: 'ExecutableDirectiveLocation', + }, + { + ifCondition: 'TypeSystemDirectiveLocation', + expect: 'TypeSystemDirectiveLocation', + }, + ], + }, + ExecutableDirectiveLocation: { + token: 'Name', + oneOf: [ + 'QUERY', + 'MUTATION', + 'SUBSCRIPTION', + 'FIELD', + 'FRAGMENT_DEFINITION', + 'FRAGMENT_SPREAD', + 'INLINE_FRAGMENT', + ], + tokenName: 'EnumValue', + }, + TypeSystemDirectiveLocation: { + token: 'Name', + oneOf: [ + 'SCHEMA', + 'SCALAR', + 'OBJECT', + 'FIELD_DEFINITION', + 'ARGUMENT_DEFINITION', + 'INTERFACE', + 'UNION', + 'ENUM', + 'ENUM_VALUE', + 'INPUT_OBJECT', + 'INPUT_FIELD_DEFINITION', + ], + tokenName: 'EnumValue', + }, + // FIXME: enforce proper typing +}: any); + +export default grammar; diff --git a/src/language/experimentalOnlineParser/index.d.ts b/src/language/experimentalOnlineParser/index.d.ts new file mode 100644 index 0000000000..039446a16c --- /dev/null +++ b/src/language/experimentalOnlineParser/index.d.ts @@ -0,0 +1,6 @@ +export { + OnlineParser, + RuleKind, + TokenKind, + OnlineParserState, +} from './onlineParser'; diff --git a/src/language/experimentalOnlineParser/index.js b/src/language/experimentalOnlineParser/index.js new file mode 100644 index 0000000000..039446a16c --- /dev/null +++ b/src/language/experimentalOnlineParser/index.js @@ -0,0 +1,6 @@ +export { + OnlineParser, + RuleKind, + TokenKind, + OnlineParserState, +} from './onlineParser'; diff --git a/src/language/experimentalOnlineParser/onlineParser.d.ts b/src/language/experimentalOnlineParser/onlineParser.d.ts new file mode 100644 index 0000000000..9570b9e589 --- /dev/null +++ b/src/language/experimentalOnlineParser/onlineParser.d.ts @@ -0,0 +1,125 @@ +import { Lexer } from '../lexer'; + +import { + GraphQLGrammarTokenConstraint, + GraphQLGrammarOfTypeConstraint, + GraphQLGrammarListOfTypeConstraint, + GraphQLGrammarPeekConstraint, + GraphQLGrammarConstraintsSet, +} from './grammar'; + +interface BaseOnlineParserRule { + kind: string; + name?: string; + depth: number; + step: number; + expanded: boolean; + state: string; + optional?: boolean; + eatNextOnFail?: boolean; +} +interface TokenOnlineParserRule + extends BaseOnlineParserRule, + GraphQLGrammarTokenConstraint {} +interface OfTypeOnlineParserRule + extends BaseOnlineParserRule, + GraphQLGrammarOfTypeConstraint {} +interface ListOfTypeOnlineParserRule + extends BaseOnlineParserRule, + GraphQLGrammarListOfTypeConstraint {} +interface PeekOnlineParserRule + extends BaseOnlineParserRule, + GraphQLGrammarPeekConstraint { + index: number; + matched: boolean; +} +interface ConstraintsSetOnlineParserRule extends BaseOnlineParserRule { + constraintsSet: boolean; + constraints: GraphQLGrammarConstraintsSet; +} + +type OnlineParserRule = + | TokenOnlineParserRule + | OfTypeOnlineParserRule + | ListOfTypeOnlineParserRule + | PeekOnlineParserRule + | ConstraintsSetOnlineParserRule; + +export interface OnlineParserState { + rules: Array; + kind: () => string; + step: () => number; + levels: Array; + indentLevel: number | undefined; + name: string | null; + type: string | null; +} + +interface Token { + kind: string; + value?: string; + tokenName?: string | undefined; + ruleName?: string | undefined; +} + +type OnlineParserConfig = { + tabSize: number; +}; + +type OnlineParserConfigOption = { + tabSize?: number; +}; + +export class OnlineParser { + state: OnlineParserState; + _lexer: Lexer; + _config: OnlineParserConfig; + constructor( + source: string, + state?: OnlineParserState, + config?: OnlineParserConfigOption, + ); + static startState(): OnlineParserState; + static copyState(state: OnlineParserState): OnlineParserState; + sol(): boolean; + parseToken(): Token; + indentation(): number; + private readonly _parseTokenConstraint; + private readonly _parseListOfTypeConstraint; + private readonly _parseOfTypeConstraint; + private readonly _parsePeekConstraint; + private readonly _parseConstraintsSetRule; + private readonly _matchToken; + private readonly _butNot; + private readonly _transformLexerToken; + private readonly _getNextRule; + private readonly _popMatchedRule; + private readonly _rollbackRule; + private readonly _pushRule; + private readonly _getRuleKind; + private readonly _advanceToken; + private readonly _lookAhead; +} + +export const TokenKind: { + NAME: string; + INT: string; + FLOAT: string; + STRING: string; + BLOCK_STRING: string; + COMMENT: string; + PUNCTUATION: string; + EOF: string; + INVALID: string; +}; + +export const RuleKind: { + TOKEN_CONSTRAINT: string; + OF_TYPE_CONSTRAINT: string; + LIST_OF_TYPE_CONSTRAINT: string; + PEEK_CONSTRAINT: string; + CONSTRAINTS_SET: string; + CONSTRAINTS_SET_ROOT: string; + RULE_NAME: string; + INVALID: string; +}; diff --git a/src/language/experimentalOnlineParser/onlineParser.js b/src/language/experimentalOnlineParser/onlineParser.js new file mode 100644 index 0000000000..f296d1c8e2 --- /dev/null +++ b/src/language/experimentalOnlineParser/onlineParser.js @@ -0,0 +1,722 @@ +import { Lexer } from '../lexer'; +import { Source } from '../source'; + +import GraphQLGrammar from './grammar'; +import type { + GraphQLGrammarRule, + GraphQLGrammarRuleName, + GraphQLGrammarRuleConstraint, + GraphQLGrammarTokenConstraint, + GraphQLGrammarOfTypeConstraint, + GraphQLGrammarListOfTypeConstraint, + GraphQLGrammarPeekConstraint, + GraphQLGrammarConstraintsSet, +} from './grammar'; + +export const TokenKind = { + NAME: 'Name', + INT: 'Int', + FLOAT: 'Float', + STRING: 'String', + BLOCK_STRING: 'BlockString', + COMMENT: 'Comment', + PUNCTUATION: 'Punctuation', + EOF: '', + INVALID: 'Invalid', +}; + +export const RuleKind = { + TOKEN_CONSTRAINT: 'TokenConstraint', + OF_TYPE_CONSTRAINT: 'OfTypeConstraint', + LIST_OF_TYPE_CONSTRAINT: 'ListOfTypeConstraint', + PEEK_CONSTRAINT: 'PeekConstraint', + CONSTRAINTS_SET: 'ConstraintsSet', + CONSTRAINTS_SET_ROOT: 'ConstraintsSetRoot', + RULE_NAME: 'RuleName', + INVALID: 'Invalid', +}; + +interface BaseOnlineParserRule { + kind: string; + name?: string; + depth: number; + step: number; + expanded: boolean; + state: string; + optional?: boolean; + eatNextOnFail?: boolean; +} +interface TokenOnlineParserRule + extends BaseOnlineParserRule, + GraphQLGrammarTokenConstraint {} +interface OfTypeOnlineParserRule + extends BaseOnlineParserRule, + GraphQLGrammarOfTypeConstraint {} +interface ListOfTypeOnlineParserRule + extends BaseOnlineParserRule, + GraphQLGrammarListOfTypeConstraint {} +interface PeekOnlineParserRule + extends BaseOnlineParserRule, + GraphQLGrammarPeekConstraint { + index: number; + matched: boolean; +} +interface ConstraintsSetOnlineParserRule extends BaseOnlineParserRule { + constraintsSet: boolean; + constraints: GraphQLGrammarConstraintsSet; +} + +type OnlineParserRule = + | TokenOnlineParserRule + | OfTypeOnlineParserRule + | ListOfTypeOnlineParserRule + | PeekOnlineParserRule + | ConstraintsSetOnlineParserRule; + +export type OnlineParserState = {| + rules: Array, + kind: () => string, + step: () => number, + levels: Array, + indentLevel: number, + name: string | null, + type: string | null, +|}; + +type Token = {| + kind: string, + value: string, + tokenName?: ?string, + ruleName?: ?string, +|}; + +type LexerToken = {| + kind: string, + value: ?string, +|}; + +type OnlineParserConfig = {| + tabSize: number, +|}; + +type OnlineParserConfigOption = {| + tabSize: ?number, +|}; + +export class OnlineParser { + state: OnlineParserState; + _lexer: Lexer; + _config: OnlineParserConfig; + + constructor( + source: string, + state?: OnlineParserState, + config?: OnlineParserConfigOption, + ) { + this.state = state || OnlineParser.startState(); + this._config = { + tabSize: config?.tabSize ?? 2, + }; + this._lexer = new Lexer(new Source(source)); + } + + static startState(): OnlineParserState { + return { + rules: [ + // $FlowFixMe[cannot-spread-interface] + { + name: 'Document', + state: 'Document', + kind: 'ListOfTypeConstraint', + ...GraphQLGrammar.Document, + expanded: false, + depth: 1, + step: 1, + }, + ], + name: null, + type: null, + levels: [], + indentLevel: 0, + kind(): string { + return this.rules[this.rules.length - 1]?.state || ''; + }, + step(): number { + return this.rules[this.rules.length - 1]?.step || 0; + }, + }; + } + + static copyState(state: OnlineParserState): OnlineParserState { + return { + name: state.name, + type: state.type, + rules: JSON.parse(JSON.stringify(state.rules)), + levels: [...state.levels], + indentLevel: state.indentLevel, + kind(): string { + return this.rules[this.rules.length - 1]?.state || ''; + }, + step(): number { + return this.rules[this.rules.length - 1]?.step || 0; + }, + }; + } + + sol(): boolean { + return ( + this._lexer.source.locationOffset.line === 1 && + this._lexer.source.locationOffset.column === 1 + ); + } + + parseToken(): Token { + const rule = (this._getNextRule(): any); + + if (this.sol()) { + this.state.indentLevel = Math.floor( + this.indentation() / this._config.tabSize, + ); + } + + if (!rule) { + return { + kind: TokenKind.INVALID, + value: '', + }; + } + + let token; + + if (this._lookAhead().kind === '') { + return { + kind: TokenKind.EOF, + value: '', + ruleName: rule.name, + }; + } + + switch (rule.kind) { + case RuleKind.TOKEN_CONSTRAINT: + token = this._parseTokenConstraint(rule); + break; + case RuleKind.LIST_OF_TYPE_CONSTRAINT: + token = this._parseListOfTypeConstraint(rule); + break; + case RuleKind.OF_TYPE_CONSTRAINT: + token = this._parseOfTypeConstraint(rule); + break; + case RuleKind.PEEK_CONSTRAINT: + token = this._parsePeekConstraint(rule); + break; + case RuleKind.CONSTRAINTS_SET_ROOT: + token = this._parseConstraintsSetRule(rule); + break; + default: + return { + kind: TokenKind.INVALID, + value: '', + ruleName: rule.name, + }; + } + + if (token && token.kind === TokenKind.INVALID) { + if (rule.optional === true) { + this.state.rules.pop(); + } else { + this._rollbackRule(); + } + + return this.parseToken() || token; + } + + return token; + } + + indentation(): number { + const match = this._lexer.source.body.match(/\s*/); + let indent = 0; + + if (match && match.length === 0) { + const whiteSpaces = match[0]; + let pos = 0; + while (whiteSpaces.length > pos) { + if (whiteSpaces.charCodeAt(pos) === 9) { + indent += 2; + } else { + indent++; + } + pos++; + } + } + + return indent; + } + + _parseTokenConstraint(rule: TokenOnlineParserRule): Token { + rule.expanded = true; + + const token = this._lookAhead(); + + if (!this._matchToken(token, rule)) { + return { + kind: TokenKind.INVALID, + value: '', + tokenName: rule.tokenName, + ruleName: rule.name, + }; + } + + this._advanceToken(); + const parserToken = this._transformLexerToken(token, rule); + this._popMatchedRule(parserToken); + + return parserToken; + } + + _parseListOfTypeConstraint(rule: ListOfTypeOnlineParserRule): Token { + this._pushRule( + GraphQLGrammar[rule.listOfType], + rule.depth + 1, + rule.listOfType, + 1, + rule.state, + ); + + rule.expanded = true; + + const token = this.parseToken(); + + return token; + } + + _parseOfTypeConstraint(rule: OfTypeOnlineParserRule): Token { + if (rule.expanded) { + this._popMatchedRule(); + return this.parseToken(); + } + + this._pushRule(rule.ofType, rule.depth + 1, rule.tokenName, 1, rule.state); + rule.expanded = true; + + const token = this.parseToken(); + + return token; + } + + _parsePeekConstraint(rule: PeekOnlineParserRule): Token { + if (rule.expanded) { + this._popMatchedRule(); + return this.parseToken(); + } + + while (!rule.matched && rule.index < rule.peek.length - 1) { + rule.index++; + const constraint = rule.peek[rule.index]; + + let { ifCondition } = constraint; + if (typeof ifCondition === 'string') { + ifCondition = GraphQLGrammar[ifCondition]; + } + + let token = this._lookAhead(); + if (ifCondition && this._matchToken(token, ifCondition)) { + rule.matched = true; + rule.expanded = true; + this._pushRule(constraint.expect, rule.depth + 1, '', 1, rule.state); + + token = this.parseToken(); + + return token; + } + } + + return { + kind: TokenKind.INVALID, + value: '', + ruleName: rule.name, + }; + } + + _parseConstraintsSetRule(rule: ConstraintsSetOnlineParserRule): Token { + if (rule.expanded) { + this._popMatchedRule(); + return this.parseToken(); + } + + for (let index = rule.constraints.length - 1; index >= 0; index--) { + this._pushRule( + rule.constraints[index], + rule.depth + 1, + '', + index, + rule.state, + ); + } + rule.expanded = true; + + return this.parseToken(); + } + + _matchToken( + token: Token | LexerToken, + rule: GraphQLGrammarTokenConstraint, + ): boolean { + if (typeof token.value === 'string') { + if ( + (typeof rule.ofValue === 'string' && token.value !== rule.ofValue) || + (Array.isArray(rule.oneOf) && !rule.oneOf.includes(token.value)) || + (typeof rule.ofValue !== 'string' && + !Array.isArray(rule.oneOf) && + token.kind !== rule.token) + ) { + return false; + } + + return this._butNot(token, rule); + } + + if (token.kind !== rule.token) { + return false; + } + + return this._butNot(token, rule); + } + + _butNot( + token: Token | LexerToken, + rule: GraphQLGrammarRuleConstraint, + ): boolean { + if (rule.butNot) { + if (Array.isArray(rule.butNot)) { + if ( + rule.butNot.reduce( + (matched, constraint) => + matched || this._matchToken(token, constraint), + false, + ) + ) { + return false; + } + + return true; + } + + return !this._matchToken(token, rule.butNot); + } + + return true; + } + + _transformLexerToken(lexerToken: LexerToken, rule: any): Token { + let token; + const ruleName = rule.name || ''; + const tokenName = rule.tokenName || ''; + + if (lexerToken.kind === '' || lexerToken.value !== undefined) { + token = { + kind: lexerToken.kind, + value: lexerToken.value || '', + tokenName, + ruleName, + }; + + if (token.kind === TokenKind.STRING) { + token.value = `"${token.value}"`; + } else if (token.kind === TokenKind.BLOCK_STRING) { + token.value = `"""${token.value}"""`; + } + } else { + token = { + kind: TokenKind.PUNCTUATION, + value: lexerToken.kind, + tokenName, + ruleName, + }; + + if (/^[{([]/.test(token.value)) { + if (this.state.indentLevel !== undefined) { + this.state.levels = this.state.levels.concat( + this.state.indentLevel + 1, + ); + } + } else if (/^[})\]]/.test(token.value)) { + this.state.levels.pop(); + } + } + + return token; + } + + _getNextRule(): OnlineParserRule | null { + return this.state.rules[this.state.rules.length - 1] || null; + } + + _popMatchedRule(token: ?Token) { + const rule = this.state.rules.pop(); + if (!rule) { + return; + } + + if (token && rule.kind === RuleKind.TOKEN_CONSTRAINT) { + const constraint = rule; + if (typeof constraint.definitionName === 'string') { + this.state.name = token.value || null; + } else if (typeof constraint.typeName === 'string') { + this.state.type = token.value || null; + } + } + + const nextRule = this._getNextRule(); + if (!nextRule) { + return; + } + + if ( + nextRule.depth === rule.depth - 1 && + nextRule.expanded && + nextRule.kind === RuleKind.CONSTRAINTS_SET_ROOT + ) { + this.state.rules.pop(); + } + + if ( + nextRule.depth === rule.depth - 1 && + nextRule.expanded && + nextRule.kind === RuleKind.LIST_OF_TYPE_CONSTRAINT + ) { + nextRule.expanded = false; + nextRule.optional = true; + } + } + + _rollbackRule() { + if (!this.state.rules.length) { + return; + } + + const popRule = () => { + const lastPoppedRule = this.state.rules.pop(); + + if (lastPoppedRule.eatNextOnFail === true) { + this.state.rules.pop(); + } + }; + + const poppedRule = this.state.rules.pop(); + if (!poppedRule) { + return; + } + + let popped = 0; + let nextRule = this._getNextRule(); + while ( + nextRule && + (poppedRule.kind !== RuleKind.LIST_OF_TYPE_CONSTRAINT || + nextRule.expanded) && + nextRule.depth > poppedRule.depth - 1 + ) { + this.state.rules.pop(); + popped++; + nextRule = this._getNextRule(); + } + + if (nextRule && nextRule.expanded) { + if (nextRule.optional === true) { + popRule(); + } else { + if ( + nextRule.kind === RuleKind.LIST_OF_TYPE_CONSTRAINT && + popped === 1 + ) { + this.state.rules.pop(); + return; + } + this._rollbackRule(); + } + } + } + + _pushRule( + baseRule: any, + depth: number, + name?: string, + step?: number, + state?: string, + ) { + this.state.name = null; + this.state.type = null; + let rule = baseRule; + + switch (this._getRuleKind(rule)) { + case RuleKind.RULE_NAME: + rule = (rule: GraphQLGrammarRuleName); + this._pushRule( + GraphQLGrammar[rule], + depth, + (typeof name === 'string' ? name : undefined) || rule, + step, + state, + ); + break; + case RuleKind.CONSTRAINTS_SET: + rule = (rule: GraphQLGrammarConstraintsSet); + this.state.rules.push({ + name: name || '', + depth, + expanded: false, + constraints: rule, + constraintsSet: true, + kind: RuleKind.CONSTRAINTS_SET_ROOT, + state: + (typeof name === 'string' ? name : undefined) || + (typeof state === 'string' ? state : undefined) || + this._getNextRule()?.state || + '', + step: + typeof step === 'number' + ? step + : (this._getNextRule()?.step || 0) + 1, + }); + break; + case RuleKind.OF_TYPE_CONSTRAINT: + rule = (rule: GraphQLGrammarOfTypeConstraint); + this.state.rules.push({ + name: name || '', + ofType: rule.ofType, + optional: Boolean(rule.optional), + butNot: rule.butNot, + eatNextOnFail: Boolean(rule.eatNextOnFail), + depth, + expanded: false, + kind: RuleKind.OF_TYPE_CONSTRAINT, + state: + (typeof rule.tokenName === 'string' ? rule.tokenName : undefined) || + (typeof name === 'string' ? name : undefined) || + (typeof state === 'string' ? state : undefined) || + this._getNextRule()?.state || + '', + step: + typeof step === 'number' + ? step + : (this._getNextRule()?.step || 0) + 1, + }); + break; + case RuleKind.LIST_OF_TYPE_CONSTRAINT: + rule = (rule: GraphQLGrammarListOfTypeConstraint); + this.state.rules.push({ + listOfType: rule.listOfType, + optional: Boolean(rule.optional), + butNot: rule.butNot, + eatNextOnFail: Boolean(rule.eatNextOnFail), + name: name || '', + depth, + expanded: false, + kind: RuleKind.LIST_OF_TYPE_CONSTRAINT, + state: + (typeof name === 'string' ? name : undefined) || + (typeof state === 'string' ? state : undefined) || + this._getNextRule()?.state || + '', + step: + typeof step === 'number' + ? step + : (this._getNextRule()?.step || 0) + 1, + }); + break; + case RuleKind.TOKEN_CONSTRAINT: + rule = (rule: GraphQLGrammarTokenConstraint); + this.state.rules.push({ + token: rule.token, + ofValue: rule.ofValue, + oneOf: rule.oneOf, + definitionName: Boolean(rule.definitionName), + typeName: Boolean(rule.typeName), + optional: Boolean(rule.optional), + butNot: rule.butNot, + eatNextOnFail: Boolean(rule.eatNextOnFail), + name: name || '', + depth, + expanded: false, + kind: RuleKind.TOKEN_CONSTRAINT, + state: + (typeof rule.tokenName === 'string' ? rule.tokenName : undefined) || + (typeof state === 'string' ? state : undefined) || + this._getNextRule()?.state || + '', + step: + typeof step === 'number' + ? step + : (this._getNextRule()?.step || 0) + 1, + }); + break; + case RuleKind.PEEK_CONSTRAINT: + rule = (rule: GraphQLGrammarPeekConstraint); + this.state.rules.push({ + peek: rule.peek, + optional: Boolean(rule.optional), + butNot: rule.butNot, + eatNextOnFail: Boolean(rule.eatNextOnFail), + name: name || '', + depth, + index: -1, + matched: false, + expanded: false, + kind: RuleKind.PEEK_CONSTRAINT, + state: + (typeof state === 'string' ? state : undefined) || + this._getNextRule()?.state || + '', + step: + typeof step === 'number' + ? step + : (this._getNextRule()?.step || 0) + 1, + }); + break; + } + } + + _getRuleKind(rule: GraphQLGrammarRule | OnlineParserRule): string { + if (Array.isArray(rule)) { + return RuleKind.CONSTRAINTS_SET; + } + + if (rule.constraintsSet === true) { + return RuleKind.CONSTRAINTS_SET_ROOT; + } + + if (typeof rule === 'string') { + return RuleKind.RULE_NAME; + } + + if (Object.prototype.hasOwnProperty.call(rule, 'ofType')) { + return RuleKind.OF_TYPE_CONSTRAINT; + } + + if (Object.prototype.hasOwnProperty.call(rule, 'listOfType')) { + return RuleKind.LIST_OF_TYPE_CONSTRAINT; + } + + if (Object.prototype.hasOwnProperty.call(rule, 'peek')) { + return RuleKind.PEEK_CONSTRAINT; + } + + if (Object.prototype.hasOwnProperty.call(rule, 'token')) { + return RuleKind.TOKEN_CONSTRAINT; + } + + return RuleKind.INVALID; + } + + _advanceToken(): LexerToken { + return (this._lexer.advance(): any); + } + + _lookAhead(): LexerToken { + try { + return (this._lexer.lookahead(): any); + } catch (err) { + return { kind: TokenKind.INVALID, value: '' }; + } + } +}