From e2c28f2d7bc254c299a5c92d640f4d3df45421cf Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Fri, 23 Sep 2016 14:43:58 -0700 Subject: [PATCH] Add assignment expressions. (#13) Add assignment expressions. Assignment expressions can be either normal or null-aware (using ??=). This commit also does some cleanup regarding Tokens, moving all Token literals to a shared file. --- lib/code_builder.dart | 11 +-- lib/src/builders/class_builder.dart | 13 +--- lib/src/builders/constructor_builder.dart | 7 +- lib/src/builders/expression_builder.dart | 80 ++++++++++++-------- lib/src/builders/field_builder.dart | 17 ++--- lib/src/builders/file_builder.dart | 11 +-- lib/src/builders/method_builder.dart | 16 ++-- lib/src/builders/parameter_builder.dart | 8 +- lib/src/builders/statement_builder.dart | 2 +- lib/src/scope.dart | 4 +- lib/src/tokens.dart | 89 +++++++++++++++++++++++ lib/testing/equals_source.dart | 9 +-- pubspec.yaml | 2 +- test/builders/method_builder_test.dart | 33 +++++++++ 14 files changed, 205 insertions(+), 97 deletions(-) create mode 100644 lib/src/tokens.dart diff --git a/lib/code_builder.dart b/lib/code_builder.dart index 2b3d189..2df5a4d 100644 --- a/lib/code_builder.dart +++ b/lib/code_builder.dart @@ -30,11 +30,11 @@ library code_builder; import 'package:analyzer/analyzer.dart'; import 'package:analyzer/dart/ast/token.dart'; -import 'package:analyzer/src/dart/ast/token.dart'; import 'package:dart_style/dart_style.dart'; import 'package:meta/meta.dart'; import 'src/analyzer_patch.dart'; +import 'src/tokens.dart'; part 'src/builders/annotation_builder.dart'; part 'src/builders/class_builder.dart'; @@ -56,13 +56,10 @@ final DartFormatter _dartfmt = new DartFormatter(); @visibleForTesting String dartfmt(String source) => _dartfmt.format(source); -Identifier _stringIdentifier(String s) { - return new SimpleIdentifier(new StringToken(TokenType.STRING, s, 0)); -} +SimpleIdentifier _stringIdentifier(String s) => + new SimpleIdentifier(stringToken(s)); -Literal _stringLiteral(String s) { - return new SimpleStringLiteral(new StringToken(TokenType.STRING, s, 0), s); -} +Literal _stringLiteral(String s) => new SimpleStringLiteral(stringToken(s), s); /// Base class for building and emitting a Dart language [AstNode]. abstract class CodeBuilder { diff --git a/lib/src/builders/class_builder.dart b/lib/src/builders/class_builder.dart index df9b3ab..bd6a260 100644 --- a/lib/src/builders/class_builder.dart +++ b/lib/src/builders/class_builder.dart @@ -6,11 +6,6 @@ part of code_builder; /// Builds a [ClassDeclaration] AST. class ClassBuilder implements CodeBuilder { - static Token _abstract = new KeywordToken(Keyword.ABSTRACT, 0); - static Token _extends = new KeywordToken(Keyword.EXTENDS, 0); - static Token _implements = new KeywordToken(Keyword.IMPLEMENTS, 0); - static Token _with = new KeywordToken(Keyword.WITH, 0); - final String _name; final bool _isAbstract; final TypeBuilder _extend; @@ -85,18 +80,18 @@ class ClassBuilder implements CodeBuilder { ClassDeclaration toAst([Scope scope = const Scope.identity()]) { var astNode = _emptyClassDeclaration()..name = _stringIdentifier(_name); if (_isAbstract) { - astNode.abstractKeyword = _abstract; + astNode.abstractKeyword = $abstract; } if (_extend != null) { - astNode.extendsClause = new ExtendsClause(_extends, _extend.toAst(scope)); + astNode.extendsClause = new ExtendsClause($extends, _extend.toAst(scope)); } if (_implement.isNotEmpty) { - astNode.implementsClause = new ImplementsClause(_implements, + astNode.implementsClause = new ImplementsClause($implements, _implement.map/**/((i) => i.toAst(scope)).toList()); } if (_mixin.isNotEmpty) { astNode.withClause = new WithClause( - _with, _mixin.map/**/((i) => i.toAst(scope)).toList()); + $with, _mixin.map/**/((i) => i.toAst(scope)).toList()); } astNode ..metadata.addAll(_metadata.map/**/((a) => a.toAst(scope))); diff --git a/lib/src/builders/constructor_builder.dart b/lib/src/builders/constructor_builder.dart index e53e053..8758d8b 100644 --- a/lib/src/builders/constructor_builder.dart +++ b/lib/src/builders/constructor_builder.dart @@ -8,9 +8,6 @@ part of code_builder; /// /// Similar to [MethodBuilder] but with constructor-only features. class ConstructorBuilder implements CodeBuilder { - static final Token _const = new KeywordToken(Keyword.CONST, 0); - static final Token _this = new KeywordToken(Keyword.THIS, 0); - final bool _isConstant; final String _name; final List _parameters = []; @@ -43,7 +40,7 @@ class ConstructorBuilder implements CodeBuilder { null, null, null, - _isConstant ? _const : null, + _isConstant ? $const : null, null, null, _name != null ? _stringIdentifier(_name) : null, @@ -53,7 +50,7 @@ class ConstructorBuilder implements CodeBuilder { null, null, null, - new EmptyFunctionBody(MethodBuilder._semicolon), + new EmptyFunctionBody($semicolon), ); return astNode; } diff --git a/lib/src/builders/expression_builder.dart b/lib/src/builders/expression_builder.dart index 1baea01..7175c8c 100644 --- a/lib/src/builders/expression_builder.dart +++ b/lib/src/builders/expression_builder.dart @@ -14,15 +14,8 @@ const literalNull = const _LiteralNull(); /// Represents an expression value of `true`. const literalTrue = const LiteralBool(true); -// Returns wrapped as a [ExpressionFunctionBody] AST. -final Token _closeP = new Token(TokenType.CLOSE_PAREN, 0); - -// Returns wrapped as a [FunctionExpression] AST. -final Token _openP = new Token(TokenType.OPEN_PAREN, 0); - -final Token _semicolon = new Token(TokenType.SEMICOLON, 0); - // TODO(matanl): Make this part of the public API. See annotation_builder.dart. +// Returns wrapped as a [ExpressionFunctionBody] AST. ExpressionFunctionBody _asFunctionBody( CodeBuilder expression, Scope scope, @@ -31,10 +24,11 @@ ExpressionFunctionBody _asFunctionBody( null, null, expression.toAst(scope), - _semicolon, + $semicolon, ); } +// Returns wrapped as a [FunctionExpression] AST. FunctionExpression _asFunctionExpression( CodeBuilder expression, Scope scope, @@ -42,11 +36,11 @@ FunctionExpression _asFunctionExpression( return new FunctionExpression( null, new FormalParameterList( - _openP, + $openParen, const [], null, null, - _closeP, + $closeParen, ), _asFunctionBody(expression, scope), ); @@ -108,6 +102,15 @@ abstract class ExpressionBuilder implements CodeBuilder { ); } + /// Assign [left] to [right]. + /// + /// If [nullAware] is true, the assignment uses the `??=` operator. + factory ExpressionBuilder.assignment( + String left, CodeBuilder right, + {bool nullAware: false}) { + return new _AssignmentExpression(left, right, nullAware: nullAware); + } + const ExpressionBuilder._(); /// Return a new [ExpressionBuilder] invoking the result of this expression. @@ -136,10 +139,8 @@ abstract class ExpressionBuilder implements CodeBuilder { /// Creates a new literal `bool` value. class LiteralBool extends _LiteralExpression { - static final BooleanLiteral _true = - new BooleanLiteral(new KeywordToken(Keyword.TRUE, 0), true); - static final BooleanLiteral _false = - new BooleanLiteral(new KeywordToken(Keyword.FALSE, 0), false); + static final BooleanLiteral _true = new BooleanLiteral($true, true); + static final BooleanLiteral _false = new BooleanLiteral($false, false); final bool _value; @@ -159,7 +160,7 @@ class LiteralInt extends _LiteralExpression { @override IntegerLiteral toAst([_]) => new IntegerLiteral( - new StringToken(TokenType.INT, '$_value', 0), + intToken(_value), _value, ); } @@ -173,18 +174,12 @@ class LiteralString extends _LiteralExpression { @override StringLiteral toAst([_]) => new SimpleStringLiteral( - new StringToken( - TokenType.STRING, - "'$_value'", - 0, - ), + stringToken("'$_value'"), _value, ); } class _InvokeExpression extends ExpressionBuilder { - static final Token _colon = new Token(TokenType.COLON, 0); - final String _importFrom; final ExpressionBuilder _target; final String _name; @@ -231,10 +226,10 @@ class _InvokeExpression extends ExpressionBuilder { // TODO(matanl): Move to TypeBuilder.newInstance. if (_type != null) { return new InstanceCreationExpression( - new KeywordToken(Keyword.NEW, 0), + $new, new ConstructorName( _type.toAst(scope), - _name != null ? new Token(TokenType.PERIOD, 0) : null, + _name != null ? $period : null, _name != null ? _stringIdentifier(_name) : null, ), _getArgumentList(scope), @@ -242,7 +237,7 @@ class _InvokeExpression extends ExpressionBuilder { } return new MethodInvocation( _target?.toAst(scope), - _target != null ? new Token(TokenType.PERIOD, 0) : null, + _target != null ? $period : null, _stringIdentifier(_name), null, _getArgumentList(scope), @@ -254,21 +249,46 @@ class _InvokeExpression extends ExpressionBuilder { ArgumentList _getArgumentList(Scope scope) { return new ArgumentList( - new Token(TokenType.OPEN_CURLY_BRACKET, 0), + $openCurly, _positionalArguments.map/* p.toAst(scope)).toList() ..addAll(_namedArguments.keys .map/**/((name) => new NamedExpression( new Label( _stringIdentifier(name), - _colon, + $colon, ), _namedArguments[name].toAst(scope), ))), - new Token(TokenType.CLOSE_CURLY_BRACKET, 0), + $closeCurly, ); } } +class _AssignmentExpression extends ExpressionBuilder { + final String left; + final CodeBuilder right; + final bool nullAware; + + _AssignmentExpression(this.left, this.right, {this.nullAware: false}) + : super._(); + + @override + ExpressionBuilder invokeSelf(String name, + {Iterable> positional: const [], + Map> named: const {}}) { + return _invokeSelfImpl(this, name, positional: positional, named: named); + } + + @override + Expression toAst([Scope scope = const Scope.identity()]) { + return new AssignmentExpression(_stringIdentifier(left), + nullAware ? $nullAwareEquals : $equals, right.toAst(scope)); + } + + @override + StatementBuilder toStatement() => new _ExpressionStatementBuilder(this); +} + abstract class _LiteralExpression implements ExpressionBuilder, CodeBuilder { const _LiteralExpression(); @@ -296,7 +316,7 @@ abstract class _LiteralExpression } class _LiteralNull extends _LiteralExpression { - static NullLiteral _null = new NullLiteral(new KeywordToken(Keyword.NULL, 0)); + static final NullLiteral _null = new NullLiteral($null); const _LiteralNull(); diff --git a/lib/src/builders/field_builder.dart b/lib/src/builders/field_builder.dart index 5977e55..82839b1 100644 --- a/lib/src/builders/field_builder.dart +++ b/lib/src/builders/field_builder.dart @@ -10,13 +10,6 @@ part of code_builder; /// AST (for class members) via [toFieldAst] or a variable declaration (used /// both at the top-level and within methods) via [toVariablesAst]. class FieldBuilder implements CodeBuilder { - static Token _equals = new Token(TokenType.EQ, 0); - static Token _semicolon = new Token(TokenType.SEMICOLON, 0); - static Token _static = new KeywordToken(Keyword.STATIC, 0); - static Token _final = new KeywordToken(Keyword.FINAL, 0); - static Token _const = new KeywordToken(Keyword.CONST, 0); - static Token _var = new KeywordToken(Keyword.VAR, 0); - final bool _isConst; final bool _isFinal; final bool _isStatic; @@ -84,7 +77,7 @@ class FieldBuilder implements CodeBuilder { new FieldDeclaration( null, null, - _isStatic ? _static : null, + _isStatic ? $static : null, toVariablesAst(scope), null, ); @@ -100,7 +93,7 @@ class FieldBuilder implements CodeBuilder { [ new VariableDeclaration( _stringIdentifier(_name), - _initialize != null ? _equals : null, + _initialize != null ? $equals : null, _initialize?.toAst(scope), ) ], @@ -108,11 +101,11 @@ class FieldBuilder implements CodeBuilder { Token _getVariableKeyword() { if (_isFinal) { - return _final; + return $final; } if (_isConst) { - return _const; + return $const; } - return _type == null ? _var : null; + return _type == null ? $var : null; } } diff --git a/lib/src/builders/file_builder.dart b/lib/src/builders/file_builder.dart index 3b65df3..1b386b7 100644 --- a/lib/src/builders/file_builder.dart +++ b/lib/src/builders/file_builder.dart @@ -92,8 +92,6 @@ class ImportBuilder implements CodeBuilder { /// Builds a standalone Dart library [CompilationUnit] AST. class LibraryBuilder extends FileBuilder { - static final Token _library = new KeywordToken(Keyword.LIBRARY, 0); - final String _name; final Scope _scope; @@ -128,7 +126,7 @@ class LibraryBuilder extends FileBuilder { new LibraryDirective( null, null, - _library, + $library, new LibraryIdentifier([_stringIdentifier(_name)]), null, ), @@ -142,9 +140,6 @@ class LibraryBuilder extends FileBuilder { /// Builds a `part of` [CompilationUnit] AST for an existing Dart library. class PartBuilder extends FileBuilder { - static final Token _part = new KeywordToken(Keyword.PART, 0); - static final Token _of = new StringToken(TokenType.KEYWORD, 'of', 0); - final String _name; /// Create a new `part of` source file. @@ -158,8 +153,8 @@ class PartBuilder extends FileBuilder { originalAst.directives.add(new PartOfDirective( null, null, - _part, - _of, + $part, + $of, new LibraryIdentifier([_stringIdentifier(_name)]), null, )); diff --git a/lib/src/builders/method_builder.dart b/lib/src/builders/method_builder.dart index 33a6fdf..252c836 100644 --- a/lib/src/builders/method_builder.dart +++ b/lib/src/builders/method_builder.dart @@ -12,10 +12,6 @@ part of code_builder; /// /// To return nothing (`void`), use [MethodBuilder.returnVoid]. class MethodBuilder implements CodeBuilder { - static Token _abstract = new KeywordToken(Keyword.ABSTRACT, 0); - static Token _semicolon = new Token(TokenType.SEMICOLON, 0); - static Token _static = new KeywordToken(Keyword.STATIC, 0); - // Void is a "type" that is only valid as a return type on a method. static const TypeBuilder _typeVoid = const TypeBuilder('void'); @@ -126,14 +122,14 @@ class MethodBuilder implements CodeBuilder { ..returnType = _returnType?.toAst(scope); FunctionBody methodBody = _returnExpression?.toFunctionBody(scope); if (_isStatic) { - methodAst.modifierKeyword = _static; + methodAst.modifierKeyword = $static; if (methodBody == null) { methodBody = _blockBody(); } } if (methodBody == null) { methodBody = _isAbstract - ? new EmptyFunctionBody(_semicolon) + ? new EmptyFunctionBody($semicolon) : _blockBody(_statements.map/**/((s) => s.toAst(scope))); } if (_parameters.isNotEmpty) { @@ -152,9 +148,9 @@ class MethodBuilder implements CodeBuilder { null, null, new Block( - new Token(TokenType.OPEN_CURLY_BRACKET, 0), + $openCurly, statements?.toList(), - new Token(TokenType.CLOSE_CURLY_BRACKET, 0), + $closeCurly, ), ); @@ -187,10 +183,10 @@ class MethodBuilder implements CodeBuilder { ); static FormalParameterList _emptyParameters() => new FormalParameterList( - new Token(TokenType.OPEN_PAREN, 0), + $openParen, [], null, null, - new Token(TokenType.CLOSE_PAREN, 0), + $closeParen, ); } diff --git a/lib/src/builders/parameter_builder.dart b/lib/src/builders/parameter_builder.dart index 886ca24..9f38cd4 100644 --- a/lib/src/builders/parameter_builder.dart +++ b/lib/src/builders/parameter_builder.dart @@ -10,8 +10,6 @@ part of code_builder; /// class-member (field) variable (see the `field` property in the /// constructors). class ParameterBuilder implements CodeBuilder { - static final Token _this = new KeywordToken(Keyword.THIS, 0); - final String _name; final bool _isField; final bool _isOptional; @@ -129,7 +127,7 @@ class ParameterBuilder implements CodeBuilder { null, null, _type?.toAst(scope), - _this, + $this, null, _stringIdentifier(_name), null, @@ -154,9 +152,7 @@ class ParameterBuilder implements CodeBuilder { return new DefaultFormalParameter( parameter, named ? ParameterKind.NAMED : ParameterKind.POSITIONAL, - defaultTo != null - ? named ? new Token(TokenType.COLON, 0) : new Token(TokenType.EQ, 0) - : null, + defaultTo != null ? named ? $colon : $equals : null, defaultTo?.toAst(), ); } diff --git a/lib/src/builders/statement_builder.dart b/lib/src/builders/statement_builder.dart index eb02e42..683753d 100644 --- a/lib/src/builders/statement_builder.dart +++ b/lib/src/builders/statement_builder.dart @@ -18,7 +18,7 @@ class _ExpressionStatementBuilder implements StatementBuilder { Statement toAst([Scope scope = const Scope.identity()]) { return new ExpressionStatement( _expression.toAst(scope), - new Token(TokenType.SEMICOLON, 0), + $semicolon, ); } } diff --git a/lib/src/scope.dart b/lib/src/scope.dart index ab5f383..f852b0f 100644 --- a/lib/src/scope.dart +++ b/lib/src/scope.dart @@ -70,8 +70,8 @@ class _IncrementingScope implements Scope { @override Identifier getIdentifier(String symbol, String import) { var newId = _imports.putIfAbsent(import, () => ++_counter); - return new PrefixedIdentifier(_stringIdentifier('_i$newId'), - new Token(TokenType.PERIOD, 0), _stringIdentifier(symbol)); + return new PrefixedIdentifier( + _stringIdentifier('_i$newId'), $period, _stringIdentifier(symbol)); } @override diff --git a/lib/src/tokens.dart b/lib/src/tokens.dart new file mode 100644 index 0000000..2ca6c83 --- /dev/null +++ b/lib/src/tokens.dart @@ -0,0 +1,89 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/src/dart/ast/token.dart'; + +// Keywords +/// The `abstract` token. +final Token $abstract = new KeywordToken(Keyword.ABSTRACT, 0); + +/// The `extends` token. +final Token $extends = new KeywordToken(Keyword.EXTENDS, 0); + +/// The `implements` token. +final Token $implements = new KeywordToken(Keyword.IMPLEMENTS, 0); + +/// The `with` token. +final Token $with = new KeywordToken(Keyword.WITH, 0); + +/// The `static` token. +final Token $static = new KeywordToken(Keyword.STATIC, 0); + +/// The `final` token. +final Token $final = new KeywordToken(Keyword.FINAL, 0); + +/// The `const` token. +final Token $const = new KeywordToken(Keyword.CONST, 0); + +/// The `var` token. +final Token $var = new KeywordToken(Keyword.VAR, 0); + +/// The `this` token. +final Token $this = new KeywordToken(Keyword.THIS, 0); + +/// The `library` token. +final Token $library = new KeywordToken(Keyword.LIBRARY, 0); + +/// The `part` token. +final Token $part = new KeywordToken(Keyword.PART, 0); + +/// The `of` token. +final Token $of = new StringToken(TokenType.KEYWORD, 'of', 0); + +/// The `true` token. +final Token $true = new KeywordToken(Keyword.TRUE, 0); + +/// The `false` token. +final Token $false = new KeywordToken(Keyword.FALSE, 0); + +/// The `null` token. +final Token $null = new KeywordToken(Keyword.NULL, 0); + +/// The `new` token. +final Token $new = new KeywordToken(Keyword.NEW, 0); + +// Simple tokens +/// The '(' token. +final Token $openParen = new Token(TokenType.OPEN_PAREN, 0); + +/// The ')' token. +final Token $closeParen = new Token(TokenType.CLOSE_PAREN, 0); + +/// The '{' token. +final Token $openCurly = new Token(TokenType.OPEN_CURLY_BRACKET, 0); + +/// The '}' token. +final Token $closeCurly = new Token(TokenType.CLOSE_CURLY_BRACKET, 0); + +/// The ':' token. +final Token $colon = new Token(TokenType.COLON, 0); + +/// The ';' token. +final Token $semicolon = new Token(TokenType.SEMICOLON, 0); + +/// The '=' token. +final Token $equals = new Token(TokenType.EQ, 0); + +/// The '??=' token. +final Token $nullAwareEquals = new Token(TokenType.QUESTION_QUESTION_EQ, 0); + +/// The '.' token. +final Token $period = new Token(TokenType.PERIOD, 0); + +/// Returns a string token for the given string [s]. +StringToken stringToken(String s) => new StringToken(TokenType.STRING, s, 0); + +/// Returns an int token for the given int [value]. +StringToken intToken(int value) => new StringToken(TokenType.INT, '$value', 0); diff --git a/lib/testing/equals_source.dart b/lib/testing/equals_source.dart index 1231cb0..e79036c 100644 --- a/lib/testing/equals_source.dart +++ b/lib/testing/equals_source.dart @@ -3,9 +3,8 @@ // BSD-style license that can be found in the LICENSE file. import 'package:analyzer/analyzer.dart'; -import 'package:analyzer/dart/ast/token.dart'; -import 'package:analyzer/src/dart/ast/token.dart'; import 'package:code_builder/code_builder.dart'; +import 'package:code_builder/src/tokens.dart'; import 'package:matcher/matcher.dart'; /// Returns identifiers that are just the file name. @@ -38,9 +37,7 @@ Matcher equalsSource( ); } -Identifier _stringId(String s) { - return new SimpleIdentifier(new StringToken(TokenType.STRING, s, 0)); -} +Identifier _stringId(String s) => new SimpleIdentifier(stringToken(s)); class _EqualsSource extends Matcher { final Scope _scope; @@ -106,7 +103,7 @@ class _SimpleNameScope implements Scope { Uri.parse(importUri).pathSegments.last.split('.').first; return new PrefixedIdentifier( _stringId(fileWithoutExt), - new Token(TokenType.PERIOD, 0), + $period, _stringId(symbol), ); } diff --git a/pubspec.yaml b/pubspec.yaml index 2920693..483ee3a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: code_builder -version: 0.1.2 +version: 0.1.3 description: A fluent API for generating Dart code author: Dart Team homepage: https://github.com/dart-lang/code_builder diff --git a/test/builders/method_builder_test.dart b/test/builders/method_builder_test.dart index 7f6f348..3b0c70e 100644 --- a/test/builders/method_builder_test.dart +++ b/test/builders/method_builder_test.dart @@ -129,6 +129,39 @@ void main() { ); }); + test('should work with an assignment', () { + expect( + new MethodBuilder.returnVoid(name: 'main') + ..addStatement(new ExpressionBuilder.assignment( + 'foo', + const LiteralString('Hello World'), + ) + .toStatement()), + equalsSource(r''' + void main() { + foo = 'Hello World'; + } + '''), + ); + }); + + test('should work with a null-aware assignment', () { + expect( + new MethodBuilder.returnVoid(name: 'main') + ..addStatement(new ExpressionBuilder.assignment( + 'foo', + const LiteralString('Hello World'), + nullAware: true, + ) + .toStatement()), + equalsSource(r''' + void main() { + foo ??= 'Hello World'; + } + '''), + ); + }); + test('should work invoking an expression', () { expect( new MethodBuilder.returnVoid(name: 'main')