diff --git a/README.md b/README.md index d965990..334a4e0 100644 --- a/README.md +++ b/README.md @@ -3,20 +3,35 @@ [![Build Status](https://travis-ci.org/dart-lang/code_builder.svg)](https://travis-ci.org/dart-lang/code_builder) [![Coverage Status](https://coveralls.io/repos/dart-lang/code_builder/badge.svg)](https://coveralls.io/r/dart-lang/code_builder) -Code builder is a fluent Dart API for generating valid Dart source code. +`code_builder` is a fluent Dart API for generating valid Dart source code. -Generally speaking, code generation usually is done through a series of -string concatenation which results in messy and sometimes invalid code -that is not easily readable. +Code generation was traditionally done through a series of +package-specific string concatenations which usually results in messy +and sometimes invalid Dart code that is not easily readable and is very +difficult to refactor. -Code builder uses the [analyzer](analyzer) package to create real Dart +`code_builder` uses the [analyzer](analyzer) package to create real Dart language ASTs, which, while not guaranteed to be correct, always follows the analyzer's own understood format. [analyzer]: https://pub.dartlang.org/packages/analyzer -Code builder also adds a more narrow and user-friendly API. For example -creating a class with a method is an easy affair: +## Experimental + +While `code_builder` is considered *stable*, the APIs are subject to +frequent breaking change - a number of Dart language features are not +yet implemented that make it unsuitable for all forms of code +generation. + +**Contributions are [welcome][welcome]!** + +[welcome]: CONTRIBUTING.md + +## Usage + +Code builder has a narrow and user-friendly API. + +For example creating a class with a method: ```dart new ClassBuilder('Animal', extends: 'Organism') @@ -32,4 +47,39 @@ class Animal extends Organism { } ``` -This package is in development and APIs are subject to frequent change. +Have a complicated set of dependencies for your generated code? +`code_builder` supports automatic scoping of your ASTs to automatically +use prefixes to avoid symbol conflicts: + +```dart +var lib = new LibraryBuilder.scope() + ..addDeclaration(new MethodBuilder( + name: 'doThing', + returns: new TypeBuilder( + 'Thing', + importFrom: 'package:thing/thing.dart', + ), + )) + ..addDeclaration(new MethodBuilder( + name: 'doOtherThing', + returns: new TypeBuilder( + 'Thing', + importFrom: 'package:thing/alternative.dart', + )) + ..addParameter(new ParameterBuilder( + 'thing', + type: new TypeBuilder( + 'Thing', + importFrom: 'package:thing/thing.dart', + ), + ))); +``` + +Outputs: +```dart +import 'package:thing/thing.dart' as _i1; +import 'package:thing/alternative.dart' as _i2; + +_i1.Thing doThing() {} +_i2.Thing doOtherThing(_i1.Thing thing) {} +``` diff --git a/lib/code_builder.dart b/lib/code_builder.dart index ab48ab7..2b3d189 100644 --- a/lib/code_builder.dart +++ b/lib/code_builder.dart @@ -34,6 +34,8 @@ 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'; + part 'src/builders/annotation_builder.dart'; part 'src/builders/class_builder.dart'; part 'src/builders/constructor_builder.dart'; @@ -54,11 +56,6 @@ final DartFormatter _dartfmt = new DartFormatter(); @visibleForTesting String dartfmt(String source) => _dartfmt.format(source); -// Creates a deep copy of an AST node. -AstNode/*=E*/ _cloneAst/**/(AstNode/*=E*/ astNode) { - return new AstCloner().cloneNode/**/(astNode); -} - Identifier _stringIdentifier(String s) { return new SimpleIdentifier(new StringToken(TokenType.STRING, s, 0)); } @@ -74,17 +71,3 @@ abstract class CodeBuilder { /// Uses [scope] to output an AST re-written to use appropriate prefixes. A toAst([Scope scope = const Scope.identity()]); } - -@Deprecated('Builders are all becoming lazy') -abstract class _AbstractCodeBuilder extends CodeBuilder { - final A _astNode; - - _AbstractCodeBuilder._(this._astNode); - - /// Returns a copy-safe [AstNode] representing the current builder state. - @override - A toAst([_]) => _cloneAst/**/(_astNode); - - @override - String toString() => '$runtimeType: ${_astNode.toSource()}'; -} diff --git a/lib/src/analyzer_patch.dart b/lib/src/analyzer_patch.dart new file mode 100644 index 0000000..5b27d77 --- /dev/null +++ b/lib/src/analyzer_patch.dart @@ -0,0 +1,65 @@ +// 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/src/generated/java_core.dart'; + +/// Implements both old-API [PrintWriter] and new-API [StringBuffer]. +/// +/// This makes it easier to re-use our `pretty_printer` until analyzer updates. +class PrintBuffer implements PrintWriter, StringBuffer { + final StringBuffer _impl = new StringBuffer(); + + @override + void clear() {} + + @override + bool get isEmpty => _impl.isEmpty; + + @override + bool get isNotEmpty => _impl.isNotEmpty; + + @override + int get length => _impl.length; + + @override + void newLine() { + _impl.writeln(); + } + + @override + void print(x) { + _impl.write(x); + } + + @override + void printf(String fmt, List args) => throw new UnimplementedError(); + + @override + void println(String s) { + _impl.writeln(s); + } + + @override + void write(Object obj) { + _impl.write(obj); + } + + @override + void writeAll(Iterable objects, [String separator = ""]) { + _impl.writeAll(objects); + } + + @override + void writeCharCode(int charCode) { + _impl.writeCharCode(charCode); + } + + @override + void writeln([Object obj = ""]) { + _impl.writeln(obj); + } + + @override + String toString() => _impl.toString(); +} diff --git a/lib/src/builders/class_builder.dart b/lib/src/builders/class_builder.dart index afb3cbc..df9b3ab 100644 --- a/lib/src/builders/class_builder.dart +++ b/lib/src/builders/class_builder.dart @@ -25,7 +25,7 @@ class ClassBuilder implements CodeBuilder { /// Create a new builder for a `class` named [name]. /// /// Optionally, define another class to [extend] or classes to either - /// [implement] or [mixin]. You may also define a `class` as [abstract]. + /// [implement] or [mixin]. factory ClassBuilder( String name, { TypeBuilder extend, @@ -40,6 +40,7 @@ class ClassBuilder implements CodeBuilder { new List.unmodifiable(mixin), ); + /// Create a new builder for an `abstract class` named [name]. factory ClassBuilder.asAbstract(String name, {TypeBuilder extend, Iterable implement: const [], diff --git a/lib/src/builders/constructor_builder.dart b/lib/src/builders/constructor_builder.dart index cf01588..e53e053 100644 --- a/lib/src/builders/constructor_builder.dart +++ b/lib/src/builders/constructor_builder.dart @@ -15,17 +15,21 @@ class ConstructorBuilder implements CodeBuilder { final String _name; final List _parameters = []; + /// Create a new builder for a constructor, optionally with a [name]. factory ConstructorBuilder([String name]) { return new ConstructorBuilder._(false, name); } + /// Create a new builder for a constructor, optionally with a [name]. + /// + /// The resulting constructor will be `const`. factory ConstructorBuilder.isConst([String name]) { return new ConstructorBuilder._(true, name); } ConstructorBuilder._(this._isConstant, this._name); - /// Lazily adds [parameter]. + /// Lazily adds [builder]. /// /// When the method is emitted as an AST, [ParameterBuilder.toAst] is used. void addParameter(ParameterBuilder builder) { diff --git a/lib/src/builders/expression_builder.dart b/lib/src/builders/expression_builder.dart index 823e004..1baea01 100644 --- a/lib/src/builders/expression_builder.dart +++ b/lib/src/builders/expression_builder.dart @@ -76,7 +76,7 @@ ExpressionBuilder _invokeSelfImpl( abstract class ExpressionBuilder implements CodeBuilder { /// Invoke [name] (which should be available in the local scope). /// - /// Optionally specify [positional] and [named] arguments. + /// May specify [positional] and [named] arguments. factory ExpressionBuilder.invoke( String name, { String importFrom, @@ -91,6 +91,9 @@ abstract class ExpressionBuilder implements CodeBuilder { ); } + /// Invoke the 'new' operator on [type]. + /// + /// May use a [name] of a constructor and [positional] and [named] arguments. factory ExpressionBuilder.invokeNew( TypeBuilder type, { String name, @@ -247,7 +250,7 @@ class _InvokeExpression extends ExpressionBuilder { } @override - StatementBuilder toStatement() => new StatementBuilder.fromExpression(this); + StatementBuilder toStatement() => new _ExpressionStatementBuilder(this); ArgumentList _getArgumentList(Scope scope) { return new ArgumentList( @@ -289,7 +292,7 @@ abstract class _LiteralExpression _asFunctionExpression(this, scope); @override - StatementBuilder toStatement() => new StatementBuilder.fromExpression(this); + StatementBuilder toStatement() => new _ExpressionStatementBuilder(this); } class _LiteralNull extends _LiteralExpression { diff --git a/lib/src/builders/file_builder.dart b/lib/src/builders/file_builder.dart index 8e3c026..3b65df3 100644 --- a/lib/src/builders/file_builder.dart +++ b/lib/src/builders/file_builder.dart @@ -13,14 +13,18 @@ CompilationUnit _emptyCompilationUnit() => new CompilationUnit( ); /// An `export` directive in a [LibraryBuilder]. -class ExportBuilder extends _AbstractCodeBuilder { +class ExportBuilder implements CodeBuilder { + final String _uri; + /// Create a new `export` directive exporting [uri]. - factory ExportBuilder(String uri) { - var astNode = _createExportDirective()..uri = _stringLiteral("'$uri'"); - return new ExportBuilder._(astNode); - } + factory ExportBuilder(String uri) = ExportBuilder._; + + const ExportBuilder._(this._uri); - ExportBuilder._(ExportDirective astNode) : super._(astNode); + @override + ExportDirective toAst([_]) { + return _createExportDirective()..uri = _stringLiteral("'$_uri'"); + } static ExportDirective _createExportDirective() => new ExportDirective( null, @@ -36,33 +40,41 @@ class ExportBuilder extends _AbstractCodeBuilder { /// Builds files of Dart source code. /// /// See [LibraryBuilder] and [PartBuilder] for concrete implementations. -abstract class FileBuilder extends _AbstractCodeBuilder { - FileBuilder._(CompilationUnit astNode) : super._(astNode); +abstract class FileBuilder implements CodeBuilder { + final List> _declarations = + >[]; + + FileBuilder._(); /// Adds [declaration]'s resulting AST to the source. void addDeclaration(CodeBuilder declaration) { - _astNode.declarations.add(declaration.toAst()); + _declarations.add(declaration); } + + @override + @mustCallSuper + CompilationUnit toAst([Scope scope = const Scope.identity()]) => + _emptyCompilationUnit() + ..declarations + .addAll(_declarations.map/**/((d) => d.toAst(scope))); } /// An `import` directive in a [FileBuilder]. -class ImportBuilder extends _AbstractCodeBuilder { - static Token _as = new KeywordToken(Keyword.AS, 0); +class ImportBuilder implements CodeBuilder { + final String _uri; + final String _prefix; /// Create a new `import` directive importing [uri]. /// - /// Optionally prefix [as]. - factory ImportBuilder(String uri, {String as}) { - var astNode = _createImportDirective()..uri = _stringLiteral("'$uri'"); - if (as != null) { - astNode - ..asKeyword = _as - ..prefix = _stringIdentifier(as); - } - return new ImportBuilder._(astNode); - } + /// Optionally prefix [prefix]. + const factory ImportBuilder(String uri, {String prefix}) = ImportBuilder._; + + const ImportBuilder._(this._uri, {String prefix}) : _prefix = prefix; - ImportBuilder._(ImportDirective astNode) : super._(astNode); + @override + ImportDirective toAst([_]) => _createImportDirective() + ..uri = _stringLiteral("'$_uri'") + ..prefix = _prefix != null ? _stringIdentifier(_prefix) : null; static ImportDirective _createImportDirective() => new ImportDirective( null, @@ -82,57 +94,47 @@ class ImportBuilder extends _AbstractCodeBuilder { class LibraryBuilder extends FileBuilder { static final Token _library = new KeywordToken(Keyword.LIBRARY, 0); + final String _name; final Scope _scope; + final List> _directives = >[]; + /// Create a new standalone Dart library, optionally with a [name]. factory LibraryBuilder([String name]) { - var astNode = _emptyCompilationUnit(); - if (name != null) { - astNode.directives.add(new LibraryDirective( - null, - null, - _library, - new LibraryIdentifier([_stringIdentifier(name)]), - null, - )); - } - return new LibraryBuilder._(astNode, const Scope.identity()); + return new LibraryBuilder._(name, const Scope.identity()); } /// Create a new standalone Dart library, optionally with a [name]. /// - /// As references are added in the library that implements [ScopeAware] + /// As references are added in the library that implements [CodeBuilder] /// they are re-written to avoid collisions and the imports are automatically /// included at the top with optional prefixes. factory LibraryBuilder.scope({String name, Scope scope}) { - var astNode = _emptyCompilationUnit(); - if (name != null) { - astNode.directives.add(new LibraryDirective( - null, - null, - _library, - new LibraryIdentifier([_stringIdentifier(name)]), - null, - )); - } - return new LibraryBuilder._(astNode, scope ?? new Scope()); + return new LibraryBuilder._(name, scope ?? new Scope()); } - LibraryBuilder._(CompilationUnit astNode, this._scope) : super._(astNode); - - @override - void addDeclaration(CodeBuilder declaration) { - _astNode.declarations.add(declaration.toAst(_scope)); - } + LibraryBuilder._(this._name, this._scope) : super._(); /// Adds [directive]'s resulting AST to the source. void addDirective(CodeBuilder directive) { - _astNode.directives.add(directive.toAst()); + _directives.add(directive); } @override CompilationUnit toAst([_]) { - var originalAst = super.toAst(); + var originalAst = super.toAst(_scope); + if (_name != null) { + originalAst.directives.add( + new LibraryDirective( + null, + null, + _library, + new LibraryIdentifier([_stringIdentifier(_name)]), + null, + ), + ); + } + originalAst.directives..addAll(_directives.map((d) => d.toAst())); originalAst.directives..addAll(_scope.getImports().map((i) => i.toAst())); return originalAst; } @@ -143,19 +145,24 @@ 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. - factory PartBuilder(String name) { - var astNode = _emptyCompilationUnit(); - astNode.directives.add(new PartOfDirective( + factory PartBuilder(String name) = PartBuilder._; + + PartBuilder._(this._name) : super._(); + + @override + CompilationUnit toAst([Scope scope = const Scope.identity()]) { + var originalAst = super.toAst(); + originalAst.directives.add(new PartOfDirective( null, null, _part, _of, - new LibraryIdentifier([_stringIdentifier(name)]), + new LibraryIdentifier([_stringIdentifier(_name)]), null, )); - return new PartBuilder._(astNode); + return originalAst; } - - PartBuilder._(CompilationUnit astNode) : super._(astNode); } diff --git a/lib/src/builders/statement_builder.dart b/lib/src/builders/statement_builder.dart index e7d0db9..eb02e42 100644 --- a/lib/src/builders/statement_builder.dart +++ b/lib/src/builders/statement_builder.dart @@ -6,17 +6,19 @@ part of code_builder; /// Builds a [Statement] AST. abstract class StatementBuilder implements CodeBuilder { - /// Returns a new [StatementBuilder] from the result of [ExpressionBuilder]. - factory StatementBuilder.fromExpression(ExpressionBuilder builder) { - return new _ExpressionStatementBuilder(new ExpressionStatement( - builder.toAst(), - _semicolon, - )); - } + StatementBuilder._sealed(); } -class _ExpressionStatementBuilder - extends _AbstractCodeBuilder - implements StatementBuilder { - _ExpressionStatementBuilder(ExpressionStatement astNode) : super._(astNode); +class _ExpressionStatementBuilder implements StatementBuilder { + final ExpressionBuilder _expression; + + _ExpressionStatementBuilder(this._expression); + + @override + Statement toAst([Scope scope = const Scope.identity()]) { + return new ExpressionStatement( + _expression.toAst(scope), + new Token(TokenType.SEMICOLON, 0), + ); + } } diff --git a/lib/src/pretty_printer.dart b/lib/src/pretty_printer.dart index 31bbc16..e1036e0 100644 --- a/lib/src/pretty_printer.dart +++ b/lib/src/pretty_printer.dart @@ -11,7 +11,7 @@ part of code_builder; /// This is the _recommended_ output (but not required) when comparing ASTs /// to expected golden files/text blobs. String prettyToSource(AstNode astNode) { - var buffer = new StringBuffer(); + var buffer = new PrintBuffer(); var visitor = new _PrettyToSourceVisitor(buffer); astNode.accept(visitor); return dartfmt(buffer.toString()); @@ -23,7 +23,7 @@ class _PrettyToSourceVisitor extends ToSourceVisitor { // https://github.com/dart-lang/sdk/issues/27301 final StringBuffer _buffer; - _PrettyToSourceVisitor(StringBuffer buffer) + _PrettyToSourceVisitor(PrintBuffer buffer) : _buffer = buffer, super(buffer); diff --git a/lib/src/scope.dart b/lib/src/scope.dart index 734ec7d..ab5f383 100644 --- a/lib/src/scope.dart +++ b/lib/src/scope.dart @@ -1,3 +1,7 @@ +// 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. + part of code_builder; /// Determines an [Identifier] deppending on where it appears. @@ -73,6 +77,6 @@ class _IncrementingScope implements Scope { @override Iterable getImports() { return _imports.keys.map/* new ImportBuilder(i, as: '_i${_imports[i]}')); + (i) => new ImportBuilder(i, prefix: '_i${_imports[i]}')); } } diff --git a/pubspec.yaml b/pubspec.yaml index ee334a7..2920693 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: code_builder -version: 0.1.1-dev +version: 0.1.2 description: A fluent API for generating Dart code author: Dart Team homepage: https://github.com/dart-lang/code_builder @@ -8,16 +8,10 @@ environment: sdk: '>=1.9.1 <2.0.0' dependencies: - analyzer: - dart_style: + analyzer: '>=0.28.1 <0.29.0' + dart_style: ^0.2.10 matcher: ^0.12.0+2 meta: ^1.0.2 dev_dependencies: test: ^0.12.0 - -dependency_overrides: - # As of 9-13-2016, this is the internal SHA. This means we can't publish to - # pub, but at least can develop with code that will work out of the box on - # the internal repo. - analyzer: 0.29.0-alpha.0 diff --git a/test/builders/file_builder_test.dart b/test/builders/file_builder_test.dart index a38092f..8329b97 100644 --- a/test/builders/file_builder_test.dart +++ b/test/builders/file_builder_test.dart @@ -32,7 +32,7 @@ void main() { test('should emit an import directive and a prefix', () { expect( - new ImportBuilder('package:foo/foo.dart', as: 'foo'), + new ImportBuilder('package:foo/foo.dart', prefix: 'foo'), equalsSource("import 'package:foo/foo.dart' as foo;"), ); }); diff --git a/test/integration_test.dart b/test/integration_test.dart index 1791796..f7565a8 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -90,16 +90,11 @@ void main() { ], )), ); - var lib = new LibraryBuilder.scope() - ..addDirective( - new ImportBuilder('app.dart'), - ) - ..addDeclaration(clazz); + var lib = new LibraryBuilder.scope()..addDeclaration(clazz); expect( lib, equalsSource( r''' - import 'app.dart'; import 'package:app/app.dart' as _i1; import 'dart:core' as _i2; import 'package:app/thing.dart' as _i3; diff --git a/test/scope_test.dart b/test/scope_test.dart index 1d16b9e..a75532a 100644 --- a/test/scope_test.dart +++ b/test/scope_test.dart @@ -1,3 +1,7 @@ +// 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/analyzer.dart'; import 'package:code_builder/code_builder.dart'; import 'package:test/test.dart';