From b99eaf1965cf3d8c6e9f2dafadb0249a78df8a26 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sun, 22 Jan 2017 18:15:53 -0500 Subject: [PATCH] Whoo --- .gitignore | 1 + README.md | 4 +- example/basic.dart | 17 ++++ lib/src/language/ast/alias.dart | 16 ++++ lib/src/language/ast/argument.dart | 18 ++++ lib/src/language/ast/array_value.dart | 26 ++++++ lib/src/language/ast/ast.dart | 29 ++++++ lib/src/language/ast/boolean_value.dart | 15 +++ lib/src/language/ast/default_value.dart | 18 ++++ lib/src/language/ast/definition.dart | 5 + lib/src/language/ast/directive.dart | 37 ++++++++ lib/src/language/ast/document.dart | 20 ++++ lib/src/language/ast/field.dart | 35 +++++++ lib/src/language/ast/field_name.dart | 19 ++++ lib/src/language/ast/fragment_definition.dart | 29 ++++++ lib/src/language/ast/fragment_spread.dart | 22 +++++ lib/src/language/ast/inline_fragment.dart | 26 ++++++ lib/src/language/ast/list_type.dart | 18 ++++ lib/src/language/ast/node.dart | 13 +++ lib/src/language/ast/number_value.dart | 15 +++ .../language/ast/operation_definition.dart | 37 ++++++++ lib/src/language/ast/selection.dart | 25 +++++ lib/src/language/ast/selection_set.dart | 27 ++++++ lib/src/language/ast/string_value.dart | 15 +++ lib/src/language/ast/type.dart | 49 ++++++++++ lib/src/language/ast/type_condition.dart | 15 +++ lib/src/language/ast/type_name.dart | 15 +++ lib/src/language/ast/value.dart | 3 + lib/src/language/ast/value_or_variable.dart | 19 ++++ lib/src/language/ast/variable.dart | 16 ++++ lib/src/language/ast/variable_definition.dart | 24 +++++ .../language/ast/variable_definitions.dart | 28 ++++++ lib/src/language/language.dart | 6 ++ lib/src/language/lexer.dart | 93 +++++++++++++++++++ lib/src/language/parser.dart | 26 ++++++ lib/src/language/stream_reader.dart | 79 ++++++++++++++++ lib/src/language/syntax_error.dart | 18 ++++ lib/src/language/token.dart | 18 ++++ lib/src/language/token_type.dart | 26 ++++++ pubspec.yaml | 8 ++ 40 files changed, 928 insertions(+), 2 deletions(-) create mode 100644 example/basic.dart create mode 100644 lib/src/language/ast/alias.dart create mode 100644 lib/src/language/ast/argument.dart create mode 100644 lib/src/language/ast/array_value.dart create mode 100644 lib/src/language/ast/ast.dart create mode 100644 lib/src/language/ast/boolean_value.dart create mode 100644 lib/src/language/ast/default_value.dart create mode 100644 lib/src/language/ast/definition.dart create mode 100644 lib/src/language/ast/directive.dart create mode 100644 lib/src/language/ast/document.dart create mode 100644 lib/src/language/ast/field.dart create mode 100644 lib/src/language/ast/field_name.dart create mode 100644 lib/src/language/ast/fragment_definition.dart create mode 100644 lib/src/language/ast/fragment_spread.dart create mode 100644 lib/src/language/ast/inline_fragment.dart create mode 100644 lib/src/language/ast/list_type.dart create mode 100644 lib/src/language/ast/node.dart create mode 100644 lib/src/language/ast/number_value.dart create mode 100644 lib/src/language/ast/operation_definition.dart create mode 100644 lib/src/language/ast/selection.dart create mode 100644 lib/src/language/ast/selection_set.dart create mode 100644 lib/src/language/ast/string_value.dart create mode 100644 lib/src/language/ast/type.dart create mode 100644 lib/src/language/ast/type_condition.dart create mode 100644 lib/src/language/ast/type_name.dart create mode 100644 lib/src/language/ast/value.dart create mode 100644 lib/src/language/ast/value_or_variable.dart create mode 100644 lib/src/language/ast/variable.dart create mode 100644 lib/src/language/ast/variable_definition.dart create mode 100644 lib/src/language/ast/variable_definitions.dart create mode 100644 lib/src/language/language.dart create mode 100644 lib/src/language/lexer.dart create mode 100644 lib/src/language/parser.dart create mode 100644 lib/src/language/stream_reader.dart create mode 100644 lib/src/language/syntax_error.dart create mode 100644 lib/src/language/token.dart create mode 100644 lib/src/language/token_type.dart create mode 100644 pubspec.yaml diff --git a/.gitignore b/.gitignore index 7c280441..427e911b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ .packages .project .pub/ +.scripts-bin/ build/ **/packages/ diff --git a/README.md b/README.md index 60b32c06..ebe20bb9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# graphql -Generates services and models from GraphQL. +# graphql_parser +Parses GraphQL queries and schemas. diff --git a/example/basic.dart b/example/basic.dart new file mode 100644 index 00000000..b7bcdb15 --- /dev/null +++ b/example/basic.dart @@ -0,0 +1,17 @@ +import 'dart:async'; +import 'package:graphql_parser/src/language/language.dart'; + +Stream input() async* { + yield ''' +{ + project(name: "GraphQL") { + tagline + } +} + '''.trim(); +} + +main() async { + var lexer = new Lexer(), parser = new Parser(); + await input().transform(lexer).forEach(print); +} diff --git a/lib/src/language/ast/alias.dart b/lib/src/language/ast/alias.dart new file mode 100644 index 00000000..3d4772db --- /dev/null +++ b/lib/src/language/ast/alias.dart @@ -0,0 +1,16 @@ +import '../token.dart'; +import 'node.dart'; +import 'package:source_span/src/span.dart'; + +class AliasContext extends Node { + final Token NAME1, COLON, NAME2; + + AliasContext(this.NAME1, this.COLON, this.NAME2); + + @override + SourceSpan get span => + new SourceSpan(NAME1.span?.start, NAME2.span?.end, toSource()); + + @override + String toSource() => '${NAME1.text}:${NAME2.text}'; +} diff --git a/lib/src/language/ast/argument.dart b/lib/src/language/ast/argument.dart new file mode 100644 index 00000000..453d1892 --- /dev/null +++ b/lib/src/language/ast/argument.dart @@ -0,0 +1,18 @@ +import '../token.dart'; +import 'node.dart'; +import 'package:source_span/src/span.dart'; +import 'value_or_variable.dart'; + +class ArgumentContext extends Node { + final Token NAME, COLON; + final ValueOrVariableContext valueOrVariable; + + ArgumentContext(this.NAME, this.COLON, this.valueOrVariable); + + @override + SourceSpan get span => + new SourceSpan(NAME.span?.start, valueOrVariable.end, toSource()); + + @override + String toSource() => '${NAME.text}:${valueOrVariable.toSource()}'; +} diff --git a/lib/src/language/ast/array_value.dart b/lib/src/language/ast/array_value.dart new file mode 100644 index 00000000..265e2737 --- /dev/null +++ b/lib/src/language/ast/array_value.dart @@ -0,0 +1,26 @@ +import '../token.dart'; +import 'package:source_span/src/span.dart'; +import 'value.dart'; + +class ArrayValueContext extends ValueContext { + final Token LBRACKET, RBRACKET; + final List values = []; + + ArrayValueContext(this.LBRACKET, this.RBRACKET); + + @override + SourceSpan get span => + new SourceSpan(LBRACKET.span?.end, RBRACKET.span?.end, toSource()); + + @override + String toSource() { + var buf = new StringBuffer('['); + + for (int i = 0; i < values.length; i++) { + if (i > 0) buf.write(','); + buf.write(values[i].toSource()); + } + + return buf.toString() + ']'; + } +} diff --git a/lib/src/language/ast/ast.dart b/lib/src/language/ast/ast.dart new file mode 100644 index 00000000..8854a91d --- /dev/null +++ b/lib/src/language/ast/ast.dart @@ -0,0 +1,29 @@ +library graphql_parser.language.ast; + +export 'alias.dart'; +export 'argument.dart'; +export 'boolean_value.dart'; +export 'default_value.dart'; +export 'definition.dart'; +export 'directive.dart'; +export 'document.dart'; +export 'field.dart'; +export 'field_name.dart'; +export 'fragment_definition.dart'; +export 'fragment_spread.dart'; +export 'inline_fragment.dart'; +export 'list_type.dart'; +export 'node.dart'; +export 'number_value.dart'; +export 'operation_definition.dart'; +export 'selection.dart'; +export 'selection_set.dart'; +export 'string_value.dart'; +export 'type.dart'; +export 'type_condition.dart'; +export 'type_name.dart'; +export 'value.dart'; +export 'value_or_variable.dart'; +export 'variable.dart'; +export 'variable_definition.dart'; +export 'variable_definitions.dart'; diff --git a/lib/src/language/ast/boolean_value.dart b/lib/src/language/ast/boolean_value.dart new file mode 100644 index 00000000..61d5697a --- /dev/null +++ b/lib/src/language/ast/boolean_value.dart @@ -0,0 +1,15 @@ +import '../token.dart'; +import 'package:source_span/src/span.dart'; +import 'value.dart'; + +class BooleanValueContext extends ValueContext { + final Token BOOLEAN; + + BooleanValueContext(this.BOOLEAN); + + @override + SourceSpan get span => BOOLEAN.span; + + @override + String toSource() => BOOLEAN.text; +} diff --git a/lib/src/language/ast/default_value.dart b/lib/src/language/ast/default_value.dart new file mode 100644 index 00000000..911b920b --- /dev/null +++ b/lib/src/language/ast/default_value.dart @@ -0,0 +1,18 @@ +import '../token.dart'; +import 'node.dart'; +import 'package:source_span/src/span.dart'; +import 'value.dart'; + +class DefaultValueContext extends Node { + final Token EQUALS; + final ValueContext value; + + DefaultValueContext(this.EQUALS, this.value); + + @override + SourceSpan get span => + new SourceSpan(EQUALS.span?.start, value.end, toSource()); + + @override + String toSource() => '=${value.toSource()}'; +} diff --git a/lib/src/language/ast/definition.dart b/lib/src/language/ast/definition.dart new file mode 100644 index 00000000..042d5876 --- /dev/null +++ b/lib/src/language/ast/definition.dart @@ -0,0 +1,5 @@ +import 'node.dart'; + +abstract class DefinitionContext extends Node { + +} \ No newline at end of file diff --git a/lib/src/language/ast/directive.dart b/lib/src/language/ast/directive.dart new file mode 100644 index 00000000..f8950dc2 --- /dev/null +++ b/lib/src/language/ast/directive.dart @@ -0,0 +1,37 @@ +import '../token.dart'; +import 'argument.dart'; +import 'node.dart'; +import 'package:source_span/source_span.dart'; +import 'value_or_variable.dart'; + +class DirectiveContext extends Node { + final Token ARROBA, NAME, COLON, LPAREN, RPAREN; + final ArgumentContext argument; + final ValueOrVariableContext valueOrVariable; + + DirectiveContext(this.ARROBA, this.NAME, this.COLON, this.LPAREN, this.RPAREN, + this.argument, this.valueOrVariable) { + assert(NAME != null); + } + + @override + SourceSpan get span { + SourceLocation end = NAME.span?.end; + + if (valueOrVariable != null) + end = valueOrVariable.end; + else if (RPAREN != null) end = RPAREN.span?.end; + + return new SourceSpan(ARROBA.span?.start, end, toSource()); + } + + @override + String toSource() { + if (valueOrVariable != null) + return '@${NAME.text}:${valueOrVariable.toSource()}'; + else if (argument != null) + return '@${NAME.text}(${argument.toSource()})'; + else + return '@${NAME.text}'; + } +} diff --git a/lib/src/language/ast/document.dart b/lib/src/language/ast/document.dart new file mode 100644 index 00000000..36e47a7b --- /dev/null +++ b/lib/src/language/ast/document.dart @@ -0,0 +1,20 @@ +import 'definition.dart'; +import 'node.dart'; +import 'package:source_span/src/span.dart'; + +class DocumentContext extends Node { + final List definitions = []; + + @override + SourceSpan get span { + if (definitions.isEmpty) return null; + return new SourceSpan( + definitions.first.start, definitions.last.end, toSource()); + } + + @override + String toSource() { + if (definitions.isEmpty) return '(empty document)'; + return definitions.map((d) => d.toSource()).join(); + } +} diff --git a/lib/src/language/ast/field.dart b/lib/src/language/ast/field.dart new file mode 100644 index 00000000..ab0781bf --- /dev/null +++ b/lib/src/language/ast/field.dart @@ -0,0 +1,35 @@ +import 'argument.dart'; +import 'directive.dart'; +import 'field_name.dart'; +import 'node.dart'; +import 'package:source_span/source_span.dart'; +import 'selection_set.dart'; + +class FieldContext extends Node { + final FieldNameContext fieldName; + final List arguments = []; + final List directives = []; + final SelectionSetContext selectionSet; + + FieldContext(this.fieldName, [this.selectionSet]); + + @override + SourceSpan get span { + SourceLocation end = fieldName.end; + + if (selectionSet != null) + end = selectionSet.end; + else if (directives.isNotEmpty) + end = directives.last.end; + else if (arguments.isNotEmpty) end = arguments.last.end; + + return new SourceSpan(fieldName.start, end, toSource()); + } + + @override + String toSource() => + fieldName.toSource() + + arguments.map((a) => a.toSource()).join() + + directives.map((d) => d.toSource()).join() + + (selectionSet?.toSource() ?? ''); +} diff --git a/lib/src/language/ast/field_name.dart b/lib/src/language/ast/field_name.dart new file mode 100644 index 00000000..e7b6360d --- /dev/null +++ b/lib/src/language/ast/field_name.dart @@ -0,0 +1,19 @@ +import '../token.dart'; +import 'alias.dart'; +import 'node.dart'; +import 'package:source_span/src/span.dart'; + +class FieldNameContext extends Node { + final Token NAME; + final AliasContext alias; + + FieldNameContext(this.NAME, [this.alias]) { + assert(NAME != null || alias != null); + } + + @override + SourceSpan get span => alias?.span ?? NAME.span; + + @override + String toSource() => alias?.toSource() ?? NAME.text; +} diff --git a/lib/src/language/ast/fragment_definition.dart b/lib/src/language/ast/fragment_definition.dart new file mode 100644 index 00000000..22b6c744 --- /dev/null +++ b/lib/src/language/ast/fragment_definition.dart @@ -0,0 +1,29 @@ +import '../token.dart'; +import 'definition.dart'; +import 'directive.dart'; +import 'package:source_span/src/span.dart'; +import 'selection_set.dart'; +import 'type_condition.dart'; + +class FragmentDefinitionContext extends DefinitionContext { + final Token FRAGMENT, NAME, ON; + final TypeConditionContext typeCondition; + final List directives = []; + final SelectionSetContext selectionSet; + + String get name => NAME.text; + + FragmentDefinitionContext( + this.FRAGMENT, this.NAME, this.ON, this.typeCondition, this.selectionSet); + + @override + SourceSpan get span => + new SourceSpan(FRAGMENT.span?.start, selectionSet.end, toSource()); + + @override + String toSource() => + 'fragment ${NAME.text} on ' + + typeCondition.toSource() + + directives.map((d) => d.toSource()).join() + + selectionSet.toSource(); +} diff --git a/lib/src/language/ast/fragment_spread.dart b/lib/src/language/ast/fragment_spread.dart new file mode 100644 index 00000000..a454a538 --- /dev/null +++ b/lib/src/language/ast/fragment_spread.dart @@ -0,0 +1,22 @@ +import '../token.dart'; +import 'directive.dart'; +import 'node.dart'; +import 'package:source_span/source_span.dart'; + +class FragmentSpreadContext extends Node { + final Token ELLIPSIS, NAME; + final List directives = []; + + FragmentSpreadContext(this.ELLIPSIS, this.NAME); + + @override + SourceSpan get span { + SourceLocation end; + return new SourceSpan(ELLIPSIS.span?.start, end, toSource()); + } + + @override + String toSource() { + return '...${NAME.text}' + directives.map((d) => d.toSource()).join(); + } +} diff --git a/lib/src/language/ast/inline_fragment.dart b/lib/src/language/ast/inline_fragment.dart new file mode 100644 index 00000000..3b9b61b5 --- /dev/null +++ b/lib/src/language/ast/inline_fragment.dart @@ -0,0 +1,26 @@ +import '../token.dart'; +import 'directive.dart'; +import 'node.dart'; +import 'package:source_span/src/span.dart'; +import 'selection_set.dart'; +import 'type_condition.dart'; + +class InlineFragmentContext extends Node { + final Token ELLIPSIS, ON; + final TypeConditionContext typeCondition; + final List directives = []; + final SelectionSetContext selectionSet; + + InlineFragmentContext( + this.ELLIPSIS, this.ON, this.typeCondition, this.selectionSet); + + @override + SourceSpan get span => + new SourceSpan(ELLIPSIS.span?.start, selectionSet.end, toSource()); + + @override + String toSource() => + '...on${typeCondition.toSource()}' + + directives.map((d) => d.toSource()).join() + + selectionSet.toSource(); +} diff --git a/lib/src/language/ast/list_type.dart b/lib/src/language/ast/list_type.dart new file mode 100644 index 00000000..09c635aa --- /dev/null +++ b/lib/src/language/ast/list_type.dart @@ -0,0 +1,18 @@ +import '../token.dart'; +import 'node.dart'; +import 'package:source_span/src/span.dart'; +import 'type.dart'; + +class ListTypeContext extends Node { + final Token LBRACKET, RBRACKET; + final TypeContext type; + + ListTypeContext(this.LBRACKET, this.type, this.RBRACKET); + + @override + SourceSpan get span => + new SourceSpan(LBRACKET.span?.end, RBRACKET.span?.end, toSource()); + + @override + String toSource() => '[${type.toSource()}]'; +} diff --git a/lib/src/language/ast/node.dart b/lib/src/language/ast/node.dart new file mode 100644 index 00000000..65bd60f2 --- /dev/null +++ b/lib/src/language/ast/node.dart @@ -0,0 +1,13 @@ +import 'package:source_span/source_span.dart'; + +abstract class Node { + SourceSpan get span; + + SourceLocation get start => span.start; + SourceLocation get end => span.end; + + String toSource(); + + @override + String toString() => '${runtimeType}: ${toSource()}'; +} diff --git a/lib/src/language/ast/number_value.dart b/lib/src/language/ast/number_value.dart new file mode 100644 index 00000000..cc981c96 --- /dev/null +++ b/lib/src/language/ast/number_value.dart @@ -0,0 +1,15 @@ +import '../token.dart'; +import 'package:source_span/src/span.dart'; +import 'value.dart'; + +class NumberValueContext extends ValueContext { + final Token NUMBER; + + NumberValueContext(this.NUMBER); + + @override + SourceSpan get span => NUMBER.span; + + @override + String toSource() => NUMBER.text; +} diff --git a/lib/src/language/ast/operation_definition.dart b/lib/src/language/ast/operation_definition.dart new file mode 100644 index 00000000..c21bfcdd --- /dev/null +++ b/lib/src/language/ast/operation_definition.dart @@ -0,0 +1,37 @@ +import '../token.dart'; +import 'definition.dart'; +import 'directive.dart'; +import 'package:source_span/src/span.dart'; +import 'selection_set.dart'; +import 'variable_definitions.dart'; + +class OperationDefinitionContext extends DefinitionContext { + final Token TYPE, NAME; + final VariableDefinitionsContext variableDefinitions; + final List directives = []; + final SelectionSetContext selectionSet; + + bool get isMutation => TYPE.text == 'mutation'; + bool get isQuery => TYPE.text == 'query'; + + String get name => NAME.text; + + OperationDefinitionContext( + this.TYPE, this.NAME, this.variableDefinitions, this.selectionSet) { + assert(TYPE == null || TYPE.text == 'query' || TYPE.text == 'mutation'); + } + + @override + SourceSpan get span { + if (TYPE == null) return selectionSet.span; + return new SourceSpan(TYPE.span?.start, selectionSet.end, toSource()); + } + + @override + String toSource() { + if (TYPE == null) return selectionSet.toSource(); + return '${TYPE.text} ${NAME.text} ${variableDefinitions.toSource()} ' + + directives.map((d) => d.toSource()).join() + + ' ${selectionSet.toSource()}'; + } +} diff --git a/lib/src/language/ast/selection.dart b/lib/src/language/ast/selection.dart new file mode 100644 index 00000000..b1299828 --- /dev/null +++ b/lib/src/language/ast/selection.dart @@ -0,0 +1,25 @@ +import 'field.dart'; +import 'fragment_spread.dart'; +import 'inline_fragment.dart'; +import 'node.dart'; +import 'package:source_span/src/span.dart'; + +class SelectionContext extends Node { + final FieldContext field; + final FragmentSpreadContext fragmentSpread; + final InlineFragmentContext inlineFragment; + + SelectionContext(this.field, [this.fragmentSpread, this.inlineFragment]) { + assert(field != null || fragmentSpread != null || inlineFragment != null); + } + + @override + SourceSpan get span => + field?.span ?? fragmentSpread?.span ?? inlineFragment?.span; + + @override + String toSource() => + field?.toSource() ?? + fragmentSpread?.toSource() ?? + inlineFragment?.toSource(); +} diff --git a/lib/src/language/ast/selection_set.dart b/lib/src/language/ast/selection_set.dart new file mode 100644 index 00000000..13c86a91 --- /dev/null +++ b/lib/src/language/ast/selection_set.dart @@ -0,0 +1,27 @@ +import '../token.dart'; +import 'node.dart'; +import 'package:source_span/src/span.dart'; +import 'selection.dart'; + +class SelectionSetContext extends Node { + final Token LBRACE, RBRACE; + final List selections = []; + + SelectionSetContext(this.LBRACE, this.RBRACE); + + @override + SourceSpan get span => + new SourceSpan(LBRACE.span?.start, RBRACE.span?.end, toSource()); + + @override + String toSource() { + var buf = new StringBuffer('{'); + + for (int i = 0; i < selections.length; i++) { + if (i > 0) buf.write(','); + buf.write(selections[i].toSource()); + } + + return buf.toString() + '}'; + } +} diff --git a/lib/src/language/ast/string_value.dart b/lib/src/language/ast/string_value.dart new file mode 100644 index 00000000..b168f13e --- /dev/null +++ b/lib/src/language/ast/string_value.dart @@ -0,0 +1,15 @@ +import '../token.dart'; +import 'package:source_span/src/span.dart'; +import 'value.dart'; + +class StringValueContext extends ValueContext { + final Token STRING; + + StringValueContext(this.STRING); + + @override + SourceSpan get span => STRING.span; + + @override + String toSource() => STRING.text; +} diff --git a/lib/src/language/ast/type.dart b/lib/src/language/ast/type.dart new file mode 100644 index 00000000..fcc00c6c --- /dev/null +++ b/lib/src/language/ast/type.dart @@ -0,0 +1,49 @@ +import 'package:source_span/source_span.dart'; +import '../token.dart'; +import 'list_type.dart'; +import 'node.dart'; +import 'type_name.dart'; + +class TypeContext extends Node { + final Token EXCLAMATION; + final TypeNameContext typeName; + final ListTypeContext listType; + + bool get nonNullType => EXCLAMATION != null; + + TypeContext(this.typeName, this.listType, [this.EXCLAMATION]) { + assert(typeName != null || listType != null); + } + + @override + SourceSpan get span { + SourceLocation start, end; + + if (typeName != null) { + start = typeName.start; + end = typeName.end; + } else if (listType != null) { + start = listType.start; + end = listType.end; + } + + if (EXCLAMATION != null) end = EXCLAMATION.span?.end; + + return new SourceSpan(start, end, toSource()); + } + + @override + String toSource() { + var buf = new StringBuffer(); + + if (typeName != null) { + buf.write(typeName.toSource()); + } else if (listType != null) { + buf.write(listType.toSource()); + } + + if (nonNullType) buf.write('!'); + + return buf.toString(); + } +} diff --git a/lib/src/language/ast/type_condition.dart b/lib/src/language/ast/type_condition.dart new file mode 100644 index 00000000..e05d6e44 --- /dev/null +++ b/lib/src/language/ast/type_condition.dart @@ -0,0 +1,15 @@ +import 'node.dart'; +import 'package:source_span/src/span.dart'; +import 'type_name.dart'; + +class TypeConditionContext extends Node { + final TypeNameContext typeName; + + TypeConditionContext(this.typeName); + + @override + SourceSpan get span => typeName.span; + + @override + String toSource() => typeName.toSource(); +} \ No newline at end of file diff --git a/lib/src/language/ast/type_name.dart b/lib/src/language/ast/type_name.dart new file mode 100644 index 00000000..ddba157f --- /dev/null +++ b/lib/src/language/ast/type_name.dart @@ -0,0 +1,15 @@ +import 'node.dart'; +import 'package:source_span/src/span.dart'; +import '../token.dart'; + +class TypeNameContext extends Node { + final Token NAME; + + @override + SourceSpan get span => NAME.span; + + TypeNameContext(this.NAME); + + @override + String toSource() => NAME.text; +} diff --git a/lib/src/language/ast/value.dart b/lib/src/language/ast/value.dart new file mode 100644 index 00000000..87b34bf7 --- /dev/null +++ b/lib/src/language/ast/value.dart @@ -0,0 +1,3 @@ +import 'node.dart'; + +abstract class ValueContext extends Node {} diff --git a/lib/src/language/ast/value_or_variable.dart b/lib/src/language/ast/value_or_variable.dart new file mode 100644 index 00000000..d2ebb1be --- /dev/null +++ b/lib/src/language/ast/value_or_variable.dart @@ -0,0 +1,19 @@ +import 'node.dart'; +import 'package:source_span/src/span.dart'; +import 'value.dart'; +import 'variable.dart'; + +class ValueOrVariableContext extends Node { + final ValueContext value; + final VariableContext variable; + + ValueOrVariableContext(this.value, this.variable) { + assert(value != null || variable != null); + } + + @override + SourceSpan get span => value?.span ?? variable.span; + + @override + String toSource() => '${value?.toSource() ?? variable.toSource()}'; +} diff --git a/lib/src/language/ast/variable.dart b/lib/src/language/ast/variable.dart new file mode 100644 index 00000000..0d492d2d --- /dev/null +++ b/lib/src/language/ast/variable.dart @@ -0,0 +1,16 @@ +import '../token.dart'; +import 'node.dart'; +import 'package:source_span/src/span.dart'; + +class VariableContext extends Node { + final Token DOLLAR, NAME; + + VariableContext(this.DOLLAR, this.NAME); + + @override + SourceSpan get span => + new SourceSpan(DOLLAR?.span?.start, NAME?.span?.end, toSource()); + + @override + String toSource() => '\$${NAME.text}'; +} diff --git a/lib/src/language/ast/variable_definition.dart b/lib/src/language/ast/variable_definition.dart new file mode 100644 index 00000000..1f5396f4 --- /dev/null +++ b/lib/src/language/ast/variable_definition.dart @@ -0,0 +1,24 @@ +import '../token.dart'; +import 'node.dart'; +import 'default_value.dart'; +import 'package:source_span/src/span.dart'; +import 'type.dart'; +import 'variable.dart'; + +class VariableDefinitionContext extends Node { + final Token COLON; + final VariableContext variable; + final TypeContext type; + final DefaultValueContext defaultValue; + + VariableDefinitionContext(this.variable, this.COLON, this.type, + [this.defaultValue]); + + @override + SourceSpan get span => + new SourceSpan(variable.start, defaultValue?.end ?? type.end, toSource()); + + @override + String toSource() => + '${variable.toSource()}:${type.toSource()}${defaultValue?.toSource() ?? ""}'; +} diff --git a/lib/src/language/ast/variable_definitions.dart b/lib/src/language/ast/variable_definitions.dart new file mode 100644 index 00000000..91008531 --- /dev/null +++ b/lib/src/language/ast/variable_definitions.dart @@ -0,0 +1,28 @@ +import '../token.dart'; +import 'node.dart'; +import 'package:source_span/src/span.dart'; +import 'variable_definition.dart'; + +class VariableDefinitionsContext extends Node { + final Token LPAREN, RPAREN; + final List variableDefinitions = []; + + VariableDefinitionsContext(this.LPAREN, this.RPAREN); + + @override + SourceSpan get span => + new SourceSpan(LPAREN.span?.end, RPAREN.span?.end, toSource()); + + @override + String toSource() { + var buf = new StringBuffer('['); + + for (int i = 0; i < variableDefinitions.length; i++) { + if (i > 0) buf.write(','); + buf.write(variableDefinitions[i].toSource()); + } + + buf.write(']'); + return buf.toString(); + } +} diff --git a/lib/src/language/language.dart b/lib/src/language/language.dart new file mode 100644 index 00000000..bdf7665e --- /dev/null +++ b/lib/src/language/language.dart @@ -0,0 +1,6 @@ +library graphql_parser.language; + +export 'lexer.dart'; +export 'parser.dart'; +export 'token.dart'; +export 'token_type.dart'; \ No newline at end of file diff --git a/lib/src/language/lexer.dart b/lib/src/language/lexer.dart new file mode 100644 index 00000000..c26b4554 --- /dev/null +++ b/lib/src/language/lexer.dart @@ -0,0 +1,93 @@ +import 'dart:async'; +import 'package:string_scanner/string_scanner.dart'; +import 'package:source_span/source_span.dart'; +import 'syntax_error.dart'; +import 'token.dart'; +import 'token_type.dart'; + +final RegExp _whitespace = new RegExp('[ \t\n\r]+'); +final RegExp _boolean = new RegExp(r'true|false'); +final RegExp _number = new RegExp(r'-?[0-9]+(\.[0-9]+)?(E|e(\+|-)?[0-9]+)?'); +final RegExp _string = new RegExp( + r'"((\\(["\\/bfnrt]|(u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])))|([^"\\]))*"'); +final RegExp _name = new RegExp(r'[_A-Za-z][_0-9A-Za-z]*'); + +final Map _patterns = { + '@': TokenType.ARROBA, + ':': TokenType.COLON, + ',': TokenType.COMMA, + r'$': TokenType.DOLLAR, + '...': TokenType.ELLIPSIS, + '=': TokenType.EQUALS, + '!': TokenType.EXCLAMATION, + '{': TokenType.LBRACE, + '}': TokenType.RBRACE, + '[': TokenType.LBRACKET, + ']': TokenType.RBRACKET, + '(': TokenType.LPAREN, + ')': TokenType.RPAREN, + 'fragment': TokenType.FRAGMENT, + 'mutation': TokenType.MUTATION, + 'on': TokenType.ON, + 'query': TokenType.QUERY, + _boolean: TokenType.BOOLEAN, + _number: TokenType.NUMBER, + _string: TokenType.STRING, + _name: TokenType.NAME +}; + +class Lexer implements StreamTransformer { + @override + Stream bind(Stream stream) { + var ctrl = new StreamController(); + + stream.listen((str) { + var scanner = new StringScanner(str); + int line = 1, column = 1; + + while (!scanner.isDone) { + List potential = []; + + if (scanner.scan(_whitespace)) { + var text = scanner.lastMatch[0]; + line += '\n'.allMatches(text).length; + var lastNewLine = text.lastIndexOf('\n'); + + if (lastNewLine != -1) { + int len = text.substring(lastNewLine + 1).length; + column = 1 + len; + } + + continue; + } + + for (var pattern in _patterns.keys) { + if (scanner.matches(pattern)) { + potential.add(new Token(_patterns[pattern], scanner.lastMatch[0])); + } + } + + if (potential.isEmpty) { + var ch = new String.fromCharCode(scanner.readChar()); + ctrl.addError(new SyntaxError("Unexpected token '$ch'.", line, column)); + } else { + // Choose longest token + potential.sort((a, b) => a.text.length.compareTo(b.text.length)); + var chosen = potential.first; + var start = + new SourceLocation(scanner.position, line: line, column: column); + ctrl.add(chosen); + scanner.position += chosen.text.length; + column += chosen.text.length; + var end = + new SourceLocation(scanner.position, line: line, column: column); + chosen.span = new SourceSpan(start, end, chosen.text); + } + } + }) + ..onDone(ctrl.close) + ..onError(ctrl.addError); + + return ctrl.stream; + } +} diff --git a/lib/src/language/parser.dart b/lib/src/language/parser.dart new file mode 100644 index 00000000..ffcb9478 --- /dev/null +++ b/lib/src/language/parser.dart @@ -0,0 +1,26 @@ +import 'dart:async'; +import 'ast/ast.dart'; +import 'stream_reader.dart'; +import 'token.dart'; + +class Parser implements StreamConsumer { + bool _closed = false; + final StreamReader _reader = new StreamReader(); + + final StreamController _onNode = new StreamController(); + + Stream get onNode => _onNode.stream; + + @override + Future addStream(Stream stream) async { + if (_closed) throw new StateError('Parser is already closed.'); + stream.pipe(_reader); + } + + @override + Future close() async { + _closed = true; + + await _onNode.close(); + } +} diff --git a/lib/src/language/stream_reader.dart b/lib/src/language/stream_reader.dart new file mode 100644 index 00000000..415f295d --- /dev/null +++ b/lib/src/language/stream_reader.dart @@ -0,0 +1,79 @@ +import 'dart:async'; +import 'dart:collection'; + +class StreamReader implements StreamConsumer { + final Queue _buffer = new Queue(); + bool _closed = false; + final Queue> _nextQueue = new Queue(); + final Queue> _peekQueue = new Queue(); + + bool get isDone => _closed; + + Future peek() { + if (isDone) throw new StateError('Cannot read from closed stream.'); + if (_buffer.isNotEmpty) return new Future.value(_buffer.first); + + var c = new Completer(); + _peekQueue.addLast(c); + return c.future; + } + + Future next() { + if (isDone) throw new StateError('Cannot read from closed stream.'); + if (_buffer.isNotEmpty) return new Future.value(_buffer.removeFirst()); + + var c = new Completer(); + _nextQueue.addLast(c); + return c.future; + } + + @override + Future addStream(Stream stream) { + if (_closed) throw new StateError('StreamReader has already been used.'); + + var c = new Completer(); + + stream.listen((data) { + if (_peekQueue.isNotEmpty || _nextQueue.isNotEmpty) { + if (_peekQueue.isNotEmpty) { + _peekQueue.removeFirst().complete(data); + } + + if (_nextQueue.isNotEmpty) { + _nextQueue.removeFirst().complete(data); + } + } else { + _buffer.add(data); + } + }) + ..onDone(c.complete) + ..onError(c.completeError); + + return c.future; + } + + @override + Future close() async { + _closed = true; + } +} + +class _IteratorReader { + final Iterator _tokens; + + T _current; + + _IteratorReader(this._tokens) { + _tokens.moveNext(); + } + + T advance() { + _current = _tokens.current; + _tokens.moveNext(); + return _current; + } + + bool get eof => _tokens.current == null; + + T peek() => _tokens.current; +} diff --git a/lib/src/language/syntax_error.dart b/lib/src/language/syntax_error.dart new file mode 100644 index 00000000..0929a27b --- /dev/null +++ b/lib/src/language/syntax_error.dart @@ -0,0 +1,18 @@ +import 'package:source_span/source_span.dart'; +import 'token.dart'; + +class SyntaxError implements Exception { + final String message; + final int line, column; + final Token offendingToken; + + SyntaxError(this.message, this.line, this.column, [this.offendingToken]); + + factory SyntaxError.fromSourceLocation( + String message, SourceLocation location, + [Token offendingToken]) => + new SyntaxError(message, location.line, location.column, offendingToken); + + @override + String toString() => 'Syntax error at line $line, column $column: $message'; +} diff --git a/lib/src/language/token.dart b/lib/src/language/token.dart new file mode 100644 index 00000000..c7cee1a2 --- /dev/null +++ b/lib/src/language/token.dart @@ -0,0 +1,18 @@ +import 'package:source_span/source_span.dart'; +import 'token_type.dart'; + +class Token { + final TokenType type; + final String text; + SourceSpan span; + + Token(this.type, this.text, [this.span]); + + @override + String toString() { + if (span == null) + return "'$text' -> $type"; + else + return "(${span.start.line}:${span.start.column}) '$text' -> $type"; + } +} diff --git a/lib/src/language/token_type.dart b/lib/src/language/token_type.dart new file mode 100644 index 00000000..cffc0bf1 --- /dev/null +++ b/lib/src/language/token_type.dart @@ -0,0 +1,26 @@ +enum TokenType { + ARROBA, + COLON, + COMMA, + DOLLAR, + ELLIPSIS, + EQUALS, + EXCLAMATION, + LBRACE, + RBRACE, + LBRACKET, + RBRACKET, + LPAREN, + RPAREN, + + FRAGMENT, + MUTATION, + ON, + QUERY, + + BOOLEAN, + NUMBER, + STRING, + + NAME +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 00000000..b6356421 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,8 @@ +author: "Tobe O" +description: "Parses GraphQL queries and schemas." +homepage: "https://github.com/thosakwe/graphql_parser" +name: "graphql_parser" +version: "0.0.0" +dependencies: + source_span: "^1.3.1" + string_scanner: "^1.0.1"