diff --git a/lib/code_builder.dart b/lib/code_builder.dart index 85b644e..9cc8374 100644 --- a/lib/code_builder.dart +++ b/lib/code_builder.dart @@ -2,6 +2,7 @@ // 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. +export 'src/allocator.dart' show Allocator; export 'src/base.dart' show Spec; export 'src/emitter.dart' show DartEmitter; export 'src/matchers.dart' show equalsDart; @@ -9,7 +10,10 @@ export 'src/specs/annotation.dart' show Annotation, AnnotationBuilder; export 'src/specs/class.dart' show Class, ClassBuilder; export 'src/specs/code.dart' show Code, CodeBuilder; export 'src/specs/constructor.dart' show Constructor, ConstructorBuilder; +export 'src/specs/directive.dart' + show Directive, DirectiveType, DirectiveBuilder; export 'src/specs/field.dart' show Field, FieldBuilder, FieldModifier; +export 'src/specs/file.dart' show File, FileBuilder; export 'src/specs/method.dart' show Method, MethodBuilder, MethodType, Parameter, ParameterBuilder; export 'src/specs/reference.dart' show Reference; diff --git a/lib/src/allocator.dart b/lib/src/allocator.dart new file mode 100644 index 0000000..aaf1c69 --- /dev/null +++ b/lib/src/allocator.dart @@ -0,0 +1,95 @@ +// Copyright (c) 2017, 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 'specs/directive.dart'; +import 'specs/reference.dart'; + +/// Collects references and automatically allocates prefixes and imports. +/// +/// `Allocator` takes out the manual work of deciding whether a symbol will +/// clash with other imports in your generated code, or what imports are needed +/// to resolve all symbols in your generated code. +abstract class Allocator { + /// An allocator that does not prefix symbols nor collects imports. + static const Allocator none = const _NullAllocator(); + + /// Creates a new default allocator that applies no prefixing. + factory Allocator() = _Allocator; + + /// Creates a new allocator that applies naive prefixing to avoid conflicts. + /// + /// This implementation is not optimized for any particular code generation + /// style and instead takes a conservative approach of prefixing _every_ + /// import except references to `dart:core` (which are considered always + /// imported). + factory Allocator.simplePrefixing() = _PrefixedAllocator; + + /// Returns a reference string given a [reference] object. + /// + /// For example, a no-op implementation: + /// ```dart + /// allocate(const Reference('List', 'dart:core')); // Returns 'List'. + /// ``` + /// + /// Where-as an implementation that prefixes imports might output: + /// ```dart + /// allocate(const Reference('Foo', 'package:foo')); // Returns '_i1.Foo'. + /// ``` + String allocate(Reference reference); + + /// All imports that have so far been added implicitly via [allocate]. + Iterable get imports; +} + +class _Allocator implements Allocator { + final _imports = new Set(); + + @override + String allocate(Reference reference) { + if (reference.url != null) { + _imports.add(reference.url); + } + return reference.symbol; + } + + @override + Iterable get imports { + return _imports.map((u) => new Directive.import(u)); + } +} + +class _NullAllocator implements Allocator { + const _NullAllocator(); + + @override + String allocate(Reference reference) => reference.symbol; + + @override + Iterable get imports => const []; +} + +class _PrefixedAllocator implements Allocator { + static const _doNotPrefix = const ['dart:core']; + + final _imports = {}; + var _keys = 1; + + @override + String allocate(Reference reference) { + final symbol = reference.symbol; + if (reference.url == null || _doNotPrefix.contains(reference.url)) { + return symbol; + } + return '_${_imports.putIfAbsent(reference.url, _nextKey)}.$symbol'; + } + + int _nextKey() => _keys++; + + @override + Iterable get imports { + return _imports.keys.map( + (u) => new Directive.import(u, as: '_${_imports[u]}'), + ); + } +} diff --git a/lib/src/emitter.dart b/lib/src/emitter.dart index 9de6f7f..f9b2862 100644 --- a/lib/src/emitter.dart +++ b/lib/src/emitter.dart @@ -2,18 +2,30 @@ // 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 'allocator.dart'; +import 'base.dart'; import 'specs/annotation.dart'; import 'specs/class.dart'; import 'specs/code.dart'; import 'specs/constructor.dart'; +import 'specs/directive.dart'; import 'specs/field.dart'; +import 'specs/file.dart'; import 'specs/method.dart'; import 'specs/reference.dart'; import 'specs/type_reference.dart'; import 'visitors.dart'; -class DartEmitter extends GeneralizingSpecVisitor { - const DartEmitter(); +class DartEmitter implements SpecVisitor { + final Allocator _allocator; + + /// Creates a new instance of [DartEmitter]. + /// + /// May specify an [Allocator] to use for symbols, otherwise uses a no-op. + const DartEmitter([this._allocator = Allocator.none]); + + /// Creates a new instance of [DartEmitter] with a default [Allocator]. + factory DartEmitter.scoped() => new DartEmitter(new Allocator()); @override visitAnnotation(Annotation spec, [StringSink output]) { @@ -154,6 +166,22 @@ class DartEmitter extends GeneralizingSpecVisitor { return output..write(code); } + @override + visitDirective(Directive spec, [StringSink output]) { + output ??= new StringBuffer(); + if (spec.type == DirectiveType.import) { + output.write('import '); + } else { + output.write('export '); + } + output.write("'${spec.url}'"); + if (spec.as != null) { + output.write(' as ${spec.as}'); + } + output.write(';'); + return output; + } + @override visitField(Field spec, [StringSink output]) { output ??= new StringBuffer(); @@ -188,6 +216,25 @@ class DartEmitter extends GeneralizingSpecVisitor { return output; } + @override + visitFile(File spec, [StringSink output]) { + output ??= new StringBuffer(); + // Process the body first in order to prime the allocators. + final body = new StringBuffer(); + for (final spec in spec.body) { + body.write(visitSpec(spec)); + } + // TODO: Allow some sort of logical ordering. + for (final directive in spec.directives) { + visitDirective(directive, output); + } + for (final directive in _allocator.imports) { + visitDirective(directive, output); + } + output.write(body); + return output; + } + @override visitMethod(Method spec, [StringSink output]) { output ??= new StringBuffer(); @@ -293,9 +340,12 @@ class DartEmitter extends GeneralizingSpecVisitor { @override visitReference(Reference spec, [StringSink output]) { - return (output ??= new StringBuffer())..write(spec.symbol); + return (output ??= new StringBuffer())..write(_allocator.allocate(spec)); } + @override + visitSpec(Spec spec) => spec.accept(this); + @override visitType(TypeReference spec, [StringSink output]) { output ??= new StringBuffer(); diff --git a/lib/src/matchers.dart b/lib/src/matchers.dart index d084909..9a69864 100644 --- a/lib/src/matchers.dart +++ b/lib/src/matchers.dart @@ -22,16 +22,21 @@ String _dartfmt(String source) { } /// Encodes [spec] as Dart source code. -String _dart(Spec spec) => - _dartfmt(spec.accept(const DartEmitter()).toString()); +String _dart(Spec spec, DartEmitter emitter) => + _dartfmt(spec.accept(emitter).toString()).trim(); /// Returns a matcher for Dart source code. -Matcher equalsDart(String source) => new _EqualsDart(_dartfmt(source)); +Matcher equalsDart( + String source, [ + DartEmitter emitter = const DartEmitter(), +]) => + new _EqualsDart(_dartfmt(source).trim(), emitter); class _EqualsDart extends Matcher { + final DartEmitter _emitter; final String _source; - const _EqualsDart(this._source); + const _EqualsDart(this._source, this._emitter); @override Description describe(Description description) => description.add(_source); @@ -40,11 +45,18 @@ class _EqualsDart extends Matcher { Description describeMismatch( covariant Spec item, Description mismatchDescription, - _, - __, - ) => - mismatchDescription.add(_dart(item)); + state, + verbose, + ) { + final result = _dart(item, _emitter); + return equals(result).describeMismatch( + _source, + mismatchDescription, + state, + verbose, + ); + } @override - bool matches(covariant Spec item, _) => _dart(item) == _source; + bool matches(covariant Spec item, _) => _dart(item, _emitter) == _source; } diff --git a/lib/src/specs/directive.dart b/lib/src/specs/directive.dart new file mode 100644 index 0000000..afda656 --- /dev/null +++ b/lib/src/specs/directive.dart @@ -0,0 +1,61 @@ +// Copyright (c) 2017, 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. + +library code_builder.src.specs.directive; + +import 'package:built_value/built_value.dart'; +import 'package:meta/meta.dart'; + +import '../base.dart'; +import '../visitors.dart'; + +part 'directive.g.dart'; + +@immutable +abstract class Directive implements Built, Spec { + factory Directive([void updates(DirectiveBuilder b)]) = _$Directive; + + factory Directive.import( + String url, { + String as, + }) => + new Directive((builder) => builder + ..as = as + ..type = DirectiveType.import + ..url = url); + + factory Directive.export(String url) => new Directive((builder) => builder + ..type = DirectiveType.export + ..url = url); + + Directive._(); + + @nullable + String get as; + + String get url; + + DirectiveType get type; + + @override + R accept(SpecVisitor visitor) => visitor.visitDirective(this); +} + +abstract class DirectiveBuilder + implements Builder { + factory DirectiveBuilder() = _$DirectiveBuilder; + + DirectiveBuilder._(); + + String as; + + String url; + + DirectiveType type; +} + +enum DirectiveType { + import, + export, +} diff --git a/lib/src/specs/directive.g.dart b/lib/src/specs/directive.g.dart new file mode 100644 index 0000000..166c594 --- /dev/null +++ b/lib/src/specs/directive.g.dart @@ -0,0 +1,123 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of code_builder.src.specs.directive; + +// ************************************************************************** +// Generator: BuiltValueGenerator +// Target: abstract class Directive +// ************************************************************************** + +class _$Directive extends Directive { + @override + final String as; + @override + final String url; + @override + final DirectiveType type; + + factory _$Directive([void updates(DirectiveBuilder b)]) => + (new DirectiveBuilder()..update(updates)).build() as _$Directive; + + _$Directive._({this.as, this.url, this.type}) : super._() { + if (url == null) throw new ArgumentError.notNull('url'); + if (type == null) throw new ArgumentError.notNull('type'); + } + + @override + Directive rebuild(void updates(DirectiveBuilder b)) => + (toBuilder()..update(updates)).build(); + + @override + _$DirectiveBuilder toBuilder() => new _$DirectiveBuilder()..replace(this); + + @override + bool operator ==(dynamic other) { + if (identical(other, this)) return true; + if (other is! Directive) return false; + return as == other.as && url == other.url && type == other.type; + } + + @override + int get hashCode { + return $jf($jc($jc($jc(0, as.hashCode), url.hashCode), type.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('Directive') + ..add('as', as) + ..add('url', url) + ..add('type', type)) + .toString(); + } +} + +class _$DirectiveBuilder extends DirectiveBuilder { + _$Directive _$v; + + @override + String get as { + _$this; + return super.as; + } + + @override + set as(String as) { + _$this; + super.as = as; + } + + @override + String get url { + _$this; + return super.url; + } + + @override + set url(String url) { + _$this; + super.url = url; + } + + @override + DirectiveType get type { + _$this; + return super.type; + } + + @override + set type(DirectiveType type) { + _$this; + super.type = type; + } + + _$DirectiveBuilder() : super._(); + + DirectiveBuilder get _$this { + if (_$v != null) { + super.as = _$v.as; + super.url = _$v.url; + super.type = _$v.type; + _$v = null; + } + return this; + } + + @override + void replace(Directive other) { + if (other == null) throw new ArgumentError.notNull('other'); + _$v = other as _$Directive; + } + + @override + void update(void updates(DirectiveBuilder b)) { + if (updates != null) updates(this); + } + + @override + _$Directive build() { + final result = _$v ?? new _$Directive._(as: as, url: url, type: type); + replace(result); + return result; + } +} diff --git a/lib/src/specs/file.dart b/lib/src/specs/file.dart new file mode 100644 index 0000000..98f26e5 --- /dev/null +++ b/lib/src/specs/file.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2017, 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. + +library code_builder.src.specs.library; + +import 'package:built_collection/built_collection.dart'; +import 'package:built_value/built_value.dart'; +import 'package:meta/meta.dart'; + +import '../base.dart'; +import '../visitors.dart'; +import 'directive.dart'; + +part 'file.g.dart'; + +@immutable +abstract class File implements Built, Spec { + factory File([void updates(FileBuilder b)]) = _$File; + + File._(); + + BuiltList get directives; + BuiltList get body; + + @override + R accept(SpecVisitor visitor) => visitor.visitFile(this); +} + +abstract class FileBuilder implements Builder { + factory FileBuilder() = _$FileBuilder; + + FileBuilder._(); + + ListBuilder directives = new ListBuilder(); + ListBuilder body = new ListBuilder(); +} diff --git a/lib/src/specs/file.g.dart b/lib/src/specs/file.g.dart new file mode 100644 index 0000000..7009189 --- /dev/null +++ b/lib/src/specs/file.g.dart @@ -0,0 +1,108 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of code_builder.src.specs.library; + +// ************************************************************************** +// Generator: BuiltValueGenerator +// Target: abstract class File +// ************************************************************************** + +class _$File extends File { + @override + final BuiltList directives; + @override + final BuiltList body; + + factory _$File([void updates(FileBuilder b)]) => + (new FileBuilder()..update(updates)).build() as _$File; + + _$File._({this.directives, this.body}) : super._() { + if (directives == null) throw new ArgumentError.notNull('directives'); + if (body == null) throw new ArgumentError.notNull('body'); + } + + @override + File rebuild(void updates(FileBuilder b)) => + (toBuilder()..update(updates)).build(); + + @override + _$FileBuilder toBuilder() => new _$FileBuilder()..replace(this); + + @override + bool operator ==(dynamic other) { + if (identical(other, this)) return true; + if (other is! File) return false; + return directives == other.directives && body == other.body; + } + + @override + int get hashCode { + return $jf($jc($jc(0, directives.hashCode), body.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('File') + ..add('directives', directives) + ..add('body', body)) + .toString(); + } +} + +class _$FileBuilder extends FileBuilder { + _$File _$v; + + @override + ListBuilder get directives { + _$this; + return super.directives ??= new ListBuilder(); + } + + @override + set directives(ListBuilder directives) { + _$this; + super.directives = directives; + } + + @override + ListBuilder get body { + _$this; + return super.body ??= new ListBuilder(); + } + + @override + set body(ListBuilder body) { + _$this; + super.body = body; + } + + _$FileBuilder() : super._(); + + FileBuilder get _$this { + if (_$v != null) { + super.directives = _$v.directives?.toBuilder(); + super.body = _$v.body?.toBuilder(); + _$v = null; + } + return this; + } + + @override + void replace(File other) { + if (other == null) throw new ArgumentError.notNull('other'); + _$v = other as _$File; + } + + @override + void update(void updates(FileBuilder b)) { + if (updates != null) updates(this); + } + + @override + _$File build() { + final result = _$v ?? + new _$File._(directives: directives?.build(), body: body?.build()); + replace(result); + return result; + } +} diff --git a/lib/src/visitors.dart b/lib/src/visitors.dart index 2af0e66..dcde2ba 100644 --- a/lib/src/visitors.dart +++ b/lib/src/visitors.dart @@ -7,7 +7,9 @@ import 'specs/annotation.dart'; import 'specs/class.dart'; import 'specs/code.dart'; import 'specs/constructor.dart'; +import 'specs/directive.dart'; import 'specs/field.dart'; +import 'specs/file.dart'; import 'specs/method.dart'; import 'specs/reference.dart'; import 'specs/type_reference.dart'; @@ -23,8 +25,12 @@ abstract class SpecVisitor { T visitConstructor(Constructor spec, String clazz); + T visitDirective(Directive spec); + T visitField(Field spec); + T visitFile(File spec); + T visitMethod(Method spec); T visitReference(Reference spec); @@ -51,9 +57,15 @@ class SimpleSpecVisitor implements SpecVisitor { @override T visitCode(Code spec) => null; + @override + T visitDirective(Directive spec) => null; + @override T visitField(Field spec) => null; + @override + T visitFile(File spec) => null; + @override T visitMethod(Method spec) => null; @@ -69,11 +81,3 @@ class SimpleSpecVisitor implements SpecVisitor { @override T visitTypeParameters(Iterable specs) => null; } - -class RecursiveSpecVisitor extends SimpleSpecVisitor { - const RecursiveSpecVisitor(); -} - -class GeneralizingSpecVisitor extends RecursiveSpecVisitor { - const GeneralizingSpecVisitor(); -} diff --git a/pubspec.yaml b/pubspec.yaml index afc6135..85c8274 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,6 +15,6 @@ dependencies: dev_dependencies: build_runner: ^0.3.0 - built_value_generator: ^1.0.0 + built_value_generator: '>=1.0.0 <=1.1.4' dart_style: ^1.0.0 test: ^0.12.0 diff --git a/test/allocator_test.dart b/test/allocator_test.dart new file mode 100644 index 0000000..f800d87 --- /dev/null +++ b/test/allocator_test.dart @@ -0,0 +1,49 @@ +// Copyright (c) 2017, 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:code_builder/code_builder.dart'; +import 'package:test/test.dart'; + +void main() { + group('Allocator', () { + Allocator allocator; + + test('should return the exact (non-prefixed) symbol', () { + allocator = new Allocator(); + expect(allocator.allocate(const Reference('Foo', 'package:foo')), 'Foo'); + }); + + test('should collect import URLs', () { + allocator = new Allocator() + ..allocate(const Reference('List', 'dart:core')) + ..allocate(const Reference('LinkedHashMap', 'dart:collection')) + ..allocate(const Reference.localScope('someSymbol')); + expect(allocator.imports.map((d) => d.url), [ + 'dart:core', + 'dart:collection', + ]); + }); + + test('.none should do nothing', () { + allocator = Allocator.none; + expect(allocator.allocate(const Reference('Foo', 'package:foo')), 'Foo'); + expect(allocator.imports, isEmpty); + }); + + test('.simplePrefixing should add import prefixes', () { + allocator = new Allocator.simplePrefixing(); + expect( + allocator.allocate(const Reference('List', 'dart:core')), + 'List', + ); + expect( + allocator.allocate(const Reference('LinkedHashMap', 'dart:collection')), + '_1.LinkedHashMap', + ); + expect(allocator.imports.map((d) => '${d.url} as ${d.as}'), [ + 'dart:collection as _1', + ]); + }); + }); +} diff --git a/test/e2e/injection_test.dart b/test/e2e/injection_test.dart index 807d792..7cb40b8 100644 --- a/test/e2e/injection_test.dart +++ b/test/e2e/injection_test.dart @@ -9,8 +9,8 @@ void main() { test('should generate a complex generated file', () { // Imports from an existing Dart library. final $App = const Reference('App', 'package:app/app.dart'); - final $Module = const Reference('Module', 'package:app/app.dart'); - final $Thing = const Reference('Thing', 'package:app/app.dart'); + final $Module = const Reference('Module', 'package:app/module.dart'); + final $Thing = const Reference('Thing', 'package:app/thing.dart'); final clazz = new ClassBuilder() ..name = 'Injector' @@ -44,5 +44,17 @@ void main() { } '''), ); + + expect( + clazz.build(), + equalsDart(r''' + class Injector implements _1.App { + Injector(this._module); + final _2.Module _module; + @override + _3.Thing getThing() => new _3.Thing(_module.get1(), _module.get2()); + } + ''', new DartEmitter(new Allocator.simplePrefixing())), + ); }); } diff --git a/test/specs/file_test.dart b/test/specs/file_test.dart new file mode 100644 index 0000000..070ee18 --- /dev/null +++ b/test/specs/file_test.dart @@ -0,0 +1,64 @@ +// Copyright (c) 2017, 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:code_builder/code_builder.dart'; +import 'package:test/test.dart'; + +void main() { + group('File', () { + const $linkedHashMap = const Reference('LinkedHashMap', 'dart:collection'); + + test('should emit a source file with manual imports', () { + expect( + new File((b) => b + ..directives.add(new Directive.import('dart:collection')) + ..body.add(new Field((b) => b + ..name = 'test' + ..modifier = FieldModifier.final$ + ..assignment = new Code((b) => b + ..code = 'new {{LinkedHashMap}}()' + ..specs.addAll({'LinkedHashMap': () => $linkedHashMap}))))), + equalsDart(r''' + import 'dart:collection'; + + final test = new LinkedHashMap(); + ''', const DartEmitter()), + ); + }); + + test('should emit a source file with allocation', () { + expect( + new File((b) => b + ..body.add(new Field((b) => b + ..name = 'test' + ..modifier = FieldModifier.final$ + ..assignment = new Code((b) => b + ..code = 'new {{LinkedHashMap}}()' + ..specs.addAll({'LinkedHashMap': () => $linkedHashMap}))))), + equalsDart(r''' + import 'dart:collection'; + + final test = new LinkedHashMap(); + ''', new DartEmitter.scoped()), + ); + }); + + test('should emit a source file with allocation + prefixing', () { + expect( + new File((b) => b + ..body.add(new Field((b) => b + ..name = 'test' + ..modifier = FieldModifier.final$ + ..assignment = new Code((b) => b + ..code = 'new {{LinkedHashMap}}()' + ..specs.addAll({'LinkedHashMap': () => $linkedHashMap}))))), + equalsDart(r''' + import 'dart:collection' as _1; + + final test = new _1.LinkedHashMap(); + ''', new DartEmitter(new Allocator.simplePrefixing())), + ); + }); + }); +}