From 5530a9dc595885f17efb06dbf26602a0ab827aa1 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Fri, 7 Mar 2014 15:15:28 -0800 Subject: [PATCH 1/4] WIP --- angular_static.dart | 0 example/web/animation.dart | 15 +- example/web/bouncing_balls.dart | 125 +- example/web/hello_world.dart | 5 +- example/web/todo.dart | 3 +- lib/angular.dart | 40 - lib/angular_dynamic.dart | 56 + lib/angular_static.dart | 22 + lib/bootstrap.dart | 77 +- lib/change_detection/change_detection.dart | 6 + .../dirty_checking_change_detector.dart | 68 +- ...irty_checking_change_detector_dynamic.dart | 19 + ...dirty_checking_change_detector_static.dart | 15 + lib/change_detection/watch_group.dart | 52 +- lib/change_detection/watch_group_dynamic.dart | 4 + lib/change_detection/watch_group_static.dart | 4 + lib/core/module.dart | 2 - lib/core/parser/static_parser.dart | 61 - lib/core/registry.dart | 10 +- lib/core/registry_dynamic.dart | 78 ++ lib/core/registry_static.dart | 17 + lib/core/scope.dart | 10 +- lib/core/zone.dart | 2 +- lib/core_dom/directive_map.dart | 62 +- lib/core_dom/module.dart | 2 - lib/metadata.dart | 0 lib/mock/module.dart | 1 + lib/mock/test_injection.dart | 4 +- lib/routing/routing.dart | 2 +- perf/invoke_perf.dart | 143 +- perf/mirror_perf.dart | 65 - perf/watch_group_perf.dart | 43 +- test/bootstrap_spec.dart | 7 +- .../dirty_checking_change_detector_spec.dart | 976 +++++++------- test/change_detection/watch_group_spec.dart | 1168 +++++++++-------- test/core/core_directive_spec.dart | 8 +- test/core/registry_spec.dart | 10 +- test/core/scope_spec.dart | 1 - test/core_dom/block_spec.dart | 6 +- test/introspection_spec.dart | 3 +- test/routing/routing_spec.dart | 9 +- 41 files changed, 1701 insertions(+), 1500 deletions(-) create mode 100644 angular_static.dart create mode 100644 lib/angular_dynamic.dart create mode 100644 lib/angular_static.dart create mode 100644 lib/change_detection/dirty_checking_change_detector_dynamic.dart create mode 100644 lib/change_detection/dirty_checking_change_detector_static.dart create mode 100644 lib/change_detection/watch_group_dynamic.dart create mode 100644 lib/change_detection/watch_group_static.dart delete mode 100644 lib/core/parser/static_parser.dart create mode 100644 lib/core/registry_dynamic.dart create mode 100644 lib/core/registry_static.dart create mode 100644 lib/metadata.dart delete mode 100644 perf/mirror_perf.dart diff --git a/angular_static.dart b/angular_static.dart new file mode 100644 index 000000000..e69de29bb diff --git a/example/web/animation.dart b/example/web/animation.dart index 7ee1a7eea..210933dca 100644 --- a/example/web/animation.dart +++ b/example/web/animation.dart @@ -1,16 +1,9 @@ library animation; import 'package:angular/angular.dart'; +import 'package:angular/angular_dynamic.dart'; import 'package:angular/animate/module.dart'; -// This annotation allows Dart to shake away any classes -// not used from Dart code nor listed in another @MirrorsUsed. -// -// If you create classes that are referenced from the Angular -// expressions, you must include a library target in @MirrorsUsed. -@MirrorsUsed(override: '*') -import 'dart:mirrors'; - part 'animation/repeat_demo.dart'; part 'animation/visibility_demo.dart'; part 'animation/stress_demo.dart'; @@ -25,11 +18,13 @@ class AnimationDemoController { } main() { - ngBootstrap(module: new Module() + new NgDynamicApp() + .addModule(new Module() ..install(new NgAnimateModule()) ..type(RepeatDemoComponent) ..type(VisibilityDemoComponent) ..type(StressDemoComponent) ..type(CssDemoComponent) - ..type(AnimationDemoController)); + ..type(AnimationDemoController)) + .run(); } diff --git a/example/web/bouncing_balls.dart b/example/web/bouncing_balls.dart index 783769740..b5718d072 100644 --- a/example/web/bouncing_balls.dart +++ b/example/web/bouncing_balls.dart @@ -1,4 +1,7 @@ +import 'package:perf_api/perf_api.dart'; import 'package:angular/angular.dart'; +import 'package:angular/angular_static.dart'; +import 'package:angular/change_detection/change_detection.dart'; import 'dart:html'; import 'dart:math'; import 'dart:core'; @@ -30,18 +33,17 @@ class BallModel { publishAs: 'bounce') class BounceController { var lastTime = window.performance.now(); - var run = true; + var run = false; var fps = 0; var digestTime = 0; var currentDigestTime = 0; var balls = []; - final NgZone zone; final Scope scope; var ballClassName = 'ball'; - BounceController(this.zone, this.scope) { + BounceController(this.scope) { changeCount(100); - tick(); + if (run) tick(); } void toggleCSS() { @@ -54,7 +56,7 @@ class BounceController { } void requestAnimationFrame(fn) { - window.requestAnimationFrame((_) => zone.run(fn)); + window.requestAnimationFrame((_) => fn()); } void changeCount(count) { @@ -118,20 +120,111 @@ class MyModule extends Module { MyModule() { type(BounceController); type(BallPositionDirective); - value(GetterCache, new GetterCache({ - 'x': (o) => o.x, - 'y': (o) => o.y, - 'bounce': (o) => o.bounce, - 'fps': (o) => o.fps, - 'balls': (o) => o.balls, - 'length': (o) => o.length, - 'digestTime': (o) => o.digestTime, - 'ballClassName': (o) => o.ballClassName - })); value(ScopeStats, new ScopeStats(report: true)); } } main() { - ngBootstrap(module: new MyModule()); + var getters = { + 'x': (o) => o.x, + 'y': (o) => o.y, + 'bounce': (o) => o.bounce, + 'fps': (o) => o.fps, + 'balls': (o) => o.balls, + 'length': (o) => o.length, + 'digestTime': (o) => o.digestTime, + 'ballClassName': (o) => o.ballClassName, + 'position': (o) => o.position, + 'onClick': (o) => o.onClick, + 'ball': (o) => o.ball, + 'color': (o) => o.color, + 'changeCount': (o) => o.changeCount, + 'playPause': (o) => o.playPause, + 'toggleCSS': (o) => o.toggleCSS, + 'timeDigest': (o) => o.timeDigest, + 'expression': (o) => o.expression, + 'fps': (o) => o.fps, + 'length': (o) => o.length, + 'digestTime': (o) => o.digestTime, + 'ballClassName': (o) => o.ballClassName, + }; + var setters = { + 'position': (o, v) => o.position = v, + 'onClick': (o, v) => o.onClick = v, + 'ball': (o, v) => o.ball = v, + 'x': (o, v) => o.x = v, + 'y': (o, v) => o.y = v, + 'balls': (o, v) => o.balls = v, + 'bounce': (o, v) => o.bounce = v, + 'expression': (o, v) => o.expression = v, + 'fps': (o, v) => o.fps = v, + 'length': (o, v) => o.length = v, + 'digestTime': (o, v) => o.digestTime = v, + 'ballClassName': (o, v) => o.ballClassName = v, + }; + var metadata = { + BounceController: [new NgController(selector: '[bounce-controller]', publishAs: 'bounce')], + BallPositionDirective: [new NgDirective(selector: '[ball-position]', map: const { "ball-position": '=>position'})], + NgEventDirective: [new NgDirective(selector: '[ng-click]', map: const {'ng-click': '&onClick'})], + NgADirective: [new NgDirective(selector: 'a[href]')], + NgRepeatDirective: [new NgDirective(children: NgAnnotation.TRANSCLUDE_CHILDREN, selector: '[ng-repeat]', map: const {'.': '@expression'})], + NgTextMustacheDirective: [new NgDirective(selector: r':contains(/{{.*}}/)')], + NgAttrMustacheDirective: [new NgDirective(selector: r'[*=/{{.*}}/]')], + }; + var types = { + Profiler: (t) => new Profiler(), + DirectiveSelectorFactory: (t) => new DirectiveSelectorFactory(), + DirectiveMap: (t) => new DirectiveMap(t(Injector), t(MetadataExtractor), t(DirectiveSelectorFactory)), + Lexer: (t) => new Lexer(), + ClosureMap: (t) => new StaticClosureMap(getters, setters), // TODO: types don't match + DynamicParserBackend: (t) => new DynamicParserBackend(t(ClosureMap)), + DynamicParser: (t) => new DynamicParser(t(Lexer), t(ParserBackend)), + Compiler: (t) => new Compiler(t(Profiler), t(Parser), t(Expando)), + AstParser: (t) => new AstParser(t(Parser)), + FilterMap: (t) => new FilterMap(t(Injector), t(MetadataExtractor)), + ExceptionHandler: (t) => new ExceptionHandler(), + FieldGetterFactory: (t) => new StaticFieldGetterFactory(getters), + ScopeDigestTTL: (t) => new ScopeDigestTTL(), + ScopeStats: (t) => new ScopeStats(), + RootScope: (t) => new RootScope(t(Object), t(AstParser), t(Parser), t(FieldGetterFactory), t(FilterMap), t(ExceptionHandler), t(ScopeDigestTTL), t(NgZone), t(ScopeStats)), + NgAnimate: (t) => new NgAnimate(), + Interpolate: (t) => new Interpolate(t(Parser)), + + NgEventDirective: (t) => new NgEventDirective(t(Element), t(Scope)), + NgADirective: (t) => new NgADirective(t(Element)), + NgRepeatDirective: (t) => new NgRepeatDirective(t(BlockHole), t(BoundBlockFactory), t(Scope), t(Parser), t(AstParser), t(FilterMap)), + + BounceController: (t) => new BounceController(t(Scope)), + BallPositionDirective: (t) => new BallPositionDirective(t(Element), t(Scope)), + }; + new NgStaticApp(types, metadata, getters) + .addModule(new MyModule()) + .run(); +} + +class StaticClosureMap extends ClosureMap { + final Map getters; + final Map setters; + + StaticClosureMap(this.getters, this.setters); + + Getter lookupGetter(String name) { + Getter getter = getters[name]; + if (getter == null) throw "No getter for '$name'."; + return getter; + } + + Setter lookupSetter(String name) { + Setter setter = setters[name]; + if (setter == null) throw "No setter for '$name'."; + return setter; + } + + Function lookupFunction(String name, int arity) { + var fn = lookupGetter(name); + return (o, [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13]) { + var args = [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13]; + return Function.apply(fn(o), args.getRange(0, arity).toList()); + }; + } } diff --git a/example/web/hello_world.dart b/example/web/hello_world.dart index 338e33bb6..6b1f3957c 100644 --- a/example/web/hello_world.dart +++ b/example/web/hello_world.dart @@ -1,4 +1,5 @@ import 'package:angular/angular.dart'; +import 'package:angular/angular_dynamic.dart'; // This annotation allows Dart to shake away any classes // not used from Dart code nor listed in another @MirrorsUsed. @@ -16,5 +17,7 @@ class HelloWorldController { } main() { - ngBootstrap(module: new Module()..type(HelloWorldController)); + new NgDynamicApp() + .addModule(new Module()..type(HelloWorldController)) + .run(); } diff --git a/example/web/todo.dart b/example/web/todo.dart index c480c9636..c40cd12cd 100644 --- a/example/web/todo.dart +++ b/example/web/todo.dart @@ -1,6 +1,7 @@ library todo; import 'package:angular/angular.dart'; +import 'package:angular/angular_dynamic.dart'; import 'package:angular/playback/playback_http.dart'; import 'todo.dart'; @@ -128,5 +129,5 @@ main() { module.type(HttpBackend, implementedBy: PlaybackHttpBackend); } - ngBootstrap(module: module); + new NgDynamicApp().addModule(module).run(); } diff --git a/lib/angular.dart b/lib/angular.dart index 136a01815..c190d6a7e 100644 --- a/lib/angular.dart +++ b/lib/angular.dart @@ -13,46 +13,8 @@ library angular; import 'dart:html' as dom; import 'dart:js' as js; import 'package:di/di.dart'; -import 'package:di/dynamic_injector.dart'; import 'package:intl/date_symbol_data_local.dart'; -/** - * If you are writing code accessed from Angular expressions, you must include - * your own @MirrorsUsed annotation or ensure that everything is tagged with - * the Ng annotations. - * - * All programs should also include a @MirrorsUsed(override: '*') which - * tells the compiler that only the explicitly listed libraries will - * be reflected over. - * - * This is a short-term fix until we implement a transformer-based solution - * which does not rely on mirrors. - */ -@MirrorsUsed(targets: const [ - 'angular', - 'angular.animate', - 'angular.core', - 'angular.core.dom', - 'angular.filter', - 'angular.perf', - 'angular.directive', - 'angular.routing', - 'angular.core.parser.Parser', - 'angular.core.parser.dynamic_parser', - 'angular.core.parser.lexer', - 'perf_api', - List, - dom.NodeTreeSanitizer, -], -metaTargets: const [ - NgInjectableService, - NgDirective, - NgController, - NgComponent, - NgFilter -]) -import 'dart:mirrors' show MirrorsUsed; - import 'package:angular/core/module.dart'; import 'package:angular/core_dom/module.dart'; import 'package:angular/directive/module.dart'; @@ -68,8 +30,6 @@ export 'package:angular/core/parser/lexer.dart'; export 'package:angular/directive/module.dart'; export 'package:angular/filter/module.dart'; export 'package:angular/routing/module.dart'; -export 'package:angular/change_detection/dirty_checking_change_detector.dart' - show FieldGetter, GetterCache; part 'bootstrap.dart'; part 'introspection.dart'; diff --git a/lib/angular_dynamic.dart b/lib/angular_dynamic.dart new file mode 100644 index 000000000..41264e445 --- /dev/null +++ b/lib/angular_dynamic.dart @@ -0,0 +1,56 @@ +library angular.dynamic; + +import 'package:di/dynamic_injector.dart'; +import "package:angular/angular.dart"; +import 'package:angular/change_detection/change_detection.dart'; +import 'package:angular/change_detection/dirty_checking_change_detector_dynamic.dart'; +import 'package:angular/core/registry_dynamic.dart'; +import 'dart:html' as dom; + +/** + * If you are writing code accessed from Angular expressions, you must include + * your own @MirrorsUsed annotation or ensure that everything is tagged with + * the Ng annotations. + * + * All programs should also include a @MirrorsUsed(override: '*') which + * tells the compiler that only the explicitly listed libraries will + * be reflected over. + * + * This is a short-term fix until we implement a transformer-based solution + * which does not rely on mirrors. + */ +@MirrorsUsed(targets: const [ + 'angular', + 'angular.animate', + 'angular.core', + 'angular.core.dom', + 'angular.filter', + 'angular.perf', + 'angular.directive', + 'angular.routing', + 'angular.core.parser.Parser', + 'angular.core.parser.dynamic_parser', + 'angular.core.parser.lexer', + 'perf_api', + List, + dom.NodeTreeSanitizer, +], +metaTargets: const [ + NgInjectableService, + NgDirective, + NgController, + NgComponent, + NgFilter +]) +import 'dart:mirrors' show MirrorsUsed; + +class NgDynamicApp extends NgApp { + NgDynamicApp() { + ngModule + ..type(MetadataExtractor, implementedBy: DynamicMetadataExtractor) + ..type(FieldGetterFactory, implementedBy: DynamicFieldGetterFactory); + } + + Injector createInjector() + => new DynamicInjector(modules: modules); +} diff --git a/lib/angular_static.dart b/lib/angular_static.dart new file mode 100644 index 000000000..717aee384 --- /dev/null +++ b/lib/angular_static.dart @@ -0,0 +1,22 @@ +library angular.static; + +import 'package:di/static_injector.dart'; +import 'package:angular/angular.dart'; +import 'package:angular/core/registry_static.dart'; +import 'package:angular/change_detection/change_detection.dart'; +import 'package:angular/change_detection/dirty_checking_change_detector_static.dart'; + +class NgStaticApp extends NgApp { + final Map typeFactories; + + NgStaticApp(Map this.typeFactories, + Map metadata, + Map fieldGetters) { + ngModule + ..value(MetadataExtractor, new StaticMetadataExtractor(metadata)) + ..value(FieldGetterFactory, new StaticFieldGetterFactory(fieldGetters)); + } + + Injector createInjector() + => new StaticInjector(modules: modules, typeFactories: typeFactories); +} diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 6c0172f88..d082691fc 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -23,13 +23,9 @@ class AngularModule extends Module { type(MetadataExtractor); value(Expando, _elementExpando); - value(NgApp, new NgApp(dom.window.document.documentElement)); } } -Injector _defaultInjectorFactory(List modules) => - new DynamicInjector(modules: modules); - /** * This method is the main entry point to an angular application. * @@ -62,46 +58,49 @@ Injector _defaultInjectorFactory(List modules) => * .... * Injector injector = ngBootstrap(module: myAppModule); */ -Injector ngBootstrap({ - Module module: null, - List modules: null, - dom.Element element: null, - String selector: '[ng-app]', - Injector injectorFactory(List modules): _defaultInjectorFactory}) { - _publishToJavaScript(); - var ngModules = [new AngularModule()]; - if (module != null) ngModules.add(module); - if (modules != null) ngModules.addAll(modules); - if (element == null) { - element = dom.querySelector(selector); - if (element == null) { - element = dom.window.document.childNodes - .firstWhere((e) => e is dom.Element); - } +abstract class NgApp { + static _find(String selector, [dom.Element defaultElement]) { + var element = dom.window.document.querySelector(selector); + if (element == null) element = defaultElement; + if (element == null)throw "Could not find application element '$selector'."; + return element; } - // The injector must be created inside the zone, so we create the - // zone manually and give it back to the injector as a value. - NgZone zone = new NgZone(); - ngModules.add(new Module() + final NgZone zone = new NgZone(); + final AngularModule ngModule = new AngularModule(); + final List modules = []; + dom.Element element; + + dom.Element selector(String selector) => element = _find(selector); + + NgApp(): element = _find('[ng-app]', dom.window.document.documentElement) { + modules.add(ngModule); + ngModule ..value(NgZone, zone) - ..value(NgApp, new NgApp(element))); + ..value(NgApp, this); + } + + Injector injector; - return zone.run(() { - var rootElements = [element]; - Injector injector = injectorFactory(ngModules); - initializeDateFormatting(null, null).then((_) { - var compiler = injector.get(Compiler); - var blockFactory = compiler(rootElements, injector.get(DirectiveMap)); - blockFactory(injector, rootElements); + NgApp addModule(Module module) { + modules.add(module); + return this; + } + + Injector run() { + _publishToJavaScript(); + return zone.run(() { + var rootElements = [element]; + Injector injector = createInjector(); + initializeDateFormatting(null, null).then((_) { + var compiler = injector.get(Compiler); + var blockFactory = compiler(rootElements, injector.get(DirectiveMap)); + blockFactory(injector, rootElements); + }); + return injector; }); - return injector; - }); -} + } -/// Holds a reference to the root of the application used by ngBootstrap. -class NgApp { - final dom.Element root; - NgApp(this.root); + Injector createInjector(); } diff --git a/lib/change_detection/change_detection.dart b/lib/change_detection/change_detection.dart index c3394708a..de368f258 100644 --- a/lib/change_detection/change_detection.dart +++ b/lib/change_detection/change_detection.dart @@ -230,6 +230,12 @@ abstract class RemovedItem extends CollectionChangeItem { RemovedItem get nextRemovedItem; } +typedef FieldGetter(object); + +abstract class FieldGetterFactory { + FieldGetter call(Object object, String name); +} + class AvgStopwatch extends Stopwatch { int _count = 0; diff --git a/lib/change_detection/dirty_checking_change_detector.dart b/lib/change_detection/dirty_checking_change_detector.dart index 420ece3db..9852d2298 100644 --- a/lib/change_detection/dirty_checking_change_detector.dart +++ b/lib/change_detection/dirty_checking_change_detector.dart @@ -1,19 +1,8 @@ library dirty_checking_change_detector; -import 'dart:mirrors'; import 'dart:collection'; import 'package:angular/change_detection/change_detection.dart'; -typedef FieldGetter(object); - -class GetterCache { - final Map _map; - - GetterCache(this._map); - - FieldGetter call(String name) => _map[name]; -} - /** * [DirtyCheckingChangeDetector] determines which object properties have changed * by comparing them to the their previous value. @@ -47,7 +36,7 @@ class DirtyCheckingChangeDetectorGroup implements ChangeDetectorGroup { */ final DirtyCheckingRecord _marker = new DirtyCheckingRecord.marker(); - final GetterCache _getterCache; + final FieldGetterFactory _fieldGetterFactory; /** * All records for group are kept together and are denoted by head/tail. @@ -85,7 +74,7 @@ class DirtyCheckingChangeDetectorGroup implements ChangeDetectorGroup { */ DirtyCheckingChangeDetectorGroup _parent, _childHead, _childTail, _prev, _next; - DirtyCheckingChangeDetectorGroup(this._parent, this._getterCache) { + DirtyCheckingChangeDetectorGroup(this._parent, this._fieldGetterFactory) { // we need to insert the marker record at the beginning. if (_parent == null) { _recordHead = _marker; @@ -116,9 +105,8 @@ class DirtyCheckingChangeDetectorGroup implements ChangeDetectorGroup { WatchRecord watch(Object object, String field, H handler) { assert(_root != null); // prove that we are not deleted connected; - var getter = field == null ? null : _getterCache(field); - return _recordAdd(new DirtyCheckingRecord(this, object, field, getter, - handler)); + return _recordAdd(new DirtyCheckingRecord(this, _fieldGetterFactory, + handler, field, object)); } /** @@ -126,7 +114,7 @@ class DirtyCheckingChangeDetectorGroup implements ChangeDetectorGroup { */ DirtyCheckingChangeDetectorGroup newGroup() { assert(_root._assertRecordsOk()); - var child = new DirtyCheckingChangeDetectorGroup(this, _getterCache); + var child = new DirtyCheckingChangeDetectorGroup(this, _fieldGetterFactory); if (_childHead == null) { _childHead = _childTail = child; } else { @@ -250,7 +238,8 @@ class DirtyCheckingChangeDetector extends DirtyCheckingChangeDetectorGroup final DirtyCheckingRecord _fakeHead = new DirtyCheckingRecord.marker(); - DirtyCheckingChangeDetector(GetterCache getterCache): super(null, getterCache); + DirtyCheckingChangeDetector(FieldGetterFactory fieldGetterFactory) + : super(null, fieldGetterFactory); DirtyCheckingChangeDetector get _root => this; @@ -349,19 +338,17 @@ class _ChangeIterator implements Iterator>{ */ class DirtyCheckingRecord implements Record, WatchRecord { static const List _MODE_NAMES = - const ['MARKER', 'IDENT', 'REFLECT', 'GETTER', 'MAP[]', 'ITERABLE', 'MAP']; + const ['MARKER', 'IDENT', 'GETTER', 'MAP[]', 'ITERABLE', 'MAP']; static const int _MODE_MARKER_ = 0; static const int _MODE_IDENTITY_ = 1; - static const int _MODE_REFLECT_ = 2; - static const int _MODE_GETTER_ = 3; - static const int _MODE_MAP_FIELD_ = 4; - static const int _MODE_ITERABLE_ = 5; - static const int _MODE_MAP_ = 6; + static const int _MODE_GETTER_ = 2; + static const int _MODE_MAP_FIELD_ = 3; + static const int _MODE_ITERABLE_ = 4; + static const int _MODE_MAP_ = 5; final DirtyCheckingChangeDetectorGroup _group; + final FieldGetterFactory _fieldGetterFactory; final String field; - final Symbol _symbol; - final FieldGetter _getter; final H handler; int _mode; @@ -372,20 +359,18 @@ class DirtyCheckingRecord implements Record, WatchRecord { DirtyCheckingRecord _prevRecord; Record _nextChange; var _object; - InstanceMirror _instanceMirror; + FieldGetter _getter; - DirtyCheckingRecord(this._group, object, fieldName, this._getter, this.handler) - : field = fieldName, - _symbol = fieldName == null ? null : new Symbol(fieldName) - { + DirtyCheckingRecord(this._group, this._fieldGetterFactory, this.handler, + this.field, object) { this.object = object; } DirtyCheckingRecord.marker() - : handler = null, + : _group = null, + _fieldGetterFactory = null, + handler = null, field = null, - _group = null, - _symbol = null, _getter = null, _mode = _MODE_MARKER_; @@ -400,11 +385,12 @@ class DirtyCheckingRecord implements Record, WatchRecord { _object = obj; if (obj == null) { _mode = _MODE_IDENTITY_; + _getter = null; return; } if (field == null) { - _instanceMirror = null; + _getter = null; if (obj is Map) { if (_mode != _MODE_MAP_) { // Last one was collection as well, don't reset state. @@ -426,13 +412,10 @@ class DirtyCheckingRecord implements Record, WatchRecord { if (obj is Map) { _mode = _MODE_MAP_FIELD_; - _instanceMirror = null; - } else if (_getter != null) { - _mode = _MODE_GETTER_; - _instanceMirror = null; + _getter = null; } else { - _mode = _MODE_REFLECT_; - _instanceMirror = reflect(obj); + _mode = _MODE_GETTER_; + _getter = _fieldGetterFactory.call(obj, field); } } @@ -442,9 +425,6 @@ class DirtyCheckingRecord implements Record, WatchRecord { switch (_mode) { case _MODE_MARKER_: return false; - case _MODE_REFLECT_: - current = _instanceMirror.getField(_symbol).reflectee; - break; case _MODE_GETTER_: current = _getter(object); break; diff --git a/lib/change_detection/dirty_checking_change_detector_dynamic.dart b/lib/change_detection/dirty_checking_change_detector_dynamic.dart new file mode 100644 index 000000000..a34ac6cd2 --- /dev/null +++ b/lib/change_detection/dirty_checking_change_detector_dynamic.dart @@ -0,0 +1,19 @@ +library dirty_checking_change_detector_dynamic; + +import 'package:angular/change_detection/change_detection.dart'; + +/** + * We are using mirrors, but there is no need to import anything. + */ +@MirrorsUsed(targets: const [], metaTargets: const []) +import 'dart:mirrors'; + +class DynamicFieldGetterFactory implements FieldGetterFactory { + FieldGetter call (Object object, String name) { + Symbol symbol = new Symbol(name); + InstanceMirror instanceMirror = reflect(object); + return (Object object) { + return instanceMirror.getField(symbol).reflectee; + }; + } +} diff --git a/lib/change_detection/dirty_checking_change_detector_static.dart b/lib/change_detection/dirty_checking_change_detector_static.dart new file mode 100644 index 000000000..8267f71ea --- /dev/null +++ b/lib/change_detection/dirty_checking_change_detector_static.dart @@ -0,0 +1,15 @@ +library dirty_checking_change_detector_static; + +import 'package:angular/change_detection/change_detection.dart'; + +class StaticFieldGetterFactory implements FieldGetterFactory { + Map getters; + + StaticFieldGetterFactory(this.getters); + + FieldGetter call(Object object, String name) { + var getter = getters[name]; + if (getter == null) throw "Missing getter: (o) => o.$name"; + return getter; + } +} diff --git a/lib/change_detection/watch_group.dart b/lib/change_detection/watch_group.dart index b3085562a..1e4b49547 100644 --- a/lib/change_detection/watch_group.dart +++ b/lib/change_detection/watch_group.dart @@ -1,6 +1,5 @@ library angular.watch_group; -import 'dart:mirrors'; import 'package:angular/change_detection/change_detection.dart'; part 'linked_list.dart'; @@ -206,7 +205,8 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { _EvalWatchRecord _addEvalWatch(AST lhsAST, /* dartbug.com/16401 Function */ fn, String name, List argsAST, String expression) { _InvokeHandler invokeHandler = new _InvokeHandler(this, expression); - var evalWatchRecord = new _EvalWatchRecord(this, invokeHandler, fn, name, + var evalWatchRecord = new _EvalWatchRecord( + _rootGroup._fieldGetterFactory, this, invokeHandler, fn, name, argsAST.length); invokeHandler.watchRecord = evalWatchRecord; @@ -339,6 +339,7 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { * [RootWatchGroup] */ class RootWatchGroup extends WatchGroup { + final FieldGetterFactory _fieldGetterFactory; Watch _dirtyWatchHead, _dirtyWatchTail; /** @@ -351,7 +352,9 @@ class RootWatchGroup extends WatchGroup { int _removeCount = 0; - RootWatchGroup(ChangeDetector changeDetector, Object context): + RootWatchGroup(FieldGetterFactory this._fieldGetterFactory, + ChangeDetector changeDetector, + Object context): super._root(changeDetector, context); RootWatchGroup get _rootGroup => this; @@ -676,20 +679,19 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, Record<_Handler> { WatchGroup watchGrp; final _Handler handler; final List args; - final Symbol symbol; final String name; int mode; /* dartbug.com/16401 Function*/ var fn; - InstanceMirror _instanceMirror; + FieldGetterFactory _fieldGetterFactory; bool dirtyArgs = true; dynamic currentValue, previousValue, _object; _EvalWatchRecord _prevEvalWatch, _nextEvalWatch; - _EvalWatchRecord(this.watchGrp, this.handler, this.fn, name, int arity) - : args = new List(arity), - name = name, - symbol = name == null ? null : new Symbol(name) { + _EvalWatchRecord(this._fieldGetterFactory, this.watchGrp, this.handler, + this.fn, this.name, int arity) + : args = new List(arity) + { if (fn is FunctionApply) { mode = _MODE_FUNCTION_APPLY_; } else if (fn is Function) { @@ -701,21 +703,21 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, Record<_Handler> { _EvalWatchRecord.marker() : mode = _MODE_MARKER_, + _fieldGetterFactory = null, watchGrp = null, handler = null, args = null, fn = null, - symbol = null, name = null; _EvalWatchRecord.constant(_Handler handler, dynamic constantValue) : mode = _MODE_MARKER_, + _fieldGetterFactory = null, handler = handler, currentValue = constantValue, watchGrp = null, args = null, fn = null, - symbol = null, name = null; get field => '()'; @@ -727,7 +729,6 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, Record<_Handler> { assert(mode != _MODE_MARKER_); assert(mode != _MODE_FUNCTION_); assert(mode != _MODE_FUNCTION_APPLY_); - assert(symbol != null); _object = value; if (value == null) { @@ -736,10 +737,21 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, Record<_Handler> { if (value is Map) { mode = _MODE_MAP_CLOSURE_; } else { - _instanceMirror = reflect(value); - mode = _hasMethod(_instanceMirror, symbol) - ? _MODE_METHOD_ - : _MODE_FIELD_CLOSURE_; + var getter = _fieldGetterFactory.call(value, name); + // We need to know if we are referring to method or field which is a + // function We can find out by calling it twice and seeing if we get + // the same value. + var val1 = getter(_object); + var val2 = getter(_object); + if (identical(val1, val2)) { + // It is a field since calling it twice returns same value + fn = getter; + mode = _MODE_FIELD_CLOSURE_; + } else { + // It is a method since method closurizes into different instances + mode = _MODE_METHOD_; + fn = val1; + } } } } @@ -761,7 +773,7 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, Record<_Handler> { dirtyArgs = false; break; case _MODE_FIELD_CLOSURE_: - var closure = _instanceMirror.getField(symbol).reflectee; + var closure = fn(_object); value = closure == null ? null : Function.apply(closure, args); break; case _MODE_MAP_CLOSURE_: @@ -769,7 +781,7 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, Record<_Handler> { value = closure == null ? null : Function.apply(closure, args); break; case _MODE_METHOD_: - value = _instanceMirror.invoke(symbol, args).reflectee; + value = Function.apply(fn, args); break; default: assert(false); @@ -803,8 +815,4 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, Record<_Handler> { if (mode == _MODE_MARKER_) return 'MARKER[$currentValue]'; return '${watchGrp.id}:${handler.expression}'; } - - static bool _hasMethod(InstanceMirror mirror, Symbol symbol) { - return mirror.type.instanceMembers[symbol] is MethodMirror; - } } diff --git a/lib/change_detection/watch_group_dynamic.dart b/lib/change_detection/watch_group_dynamic.dart new file mode 100644 index 000000000..78b8f4c52 --- /dev/null +++ b/lib/change_detection/watch_group_dynamic.dart @@ -0,0 +1,4 @@ +library watch_group_dynamic; + +import 'package:angular/change_detection/watch_group.dart'; + diff --git a/lib/change_detection/watch_group_static.dart b/lib/change_detection/watch_group_static.dart new file mode 100644 index 000000000..817d6a984 --- /dev/null +++ b/lib/change_detection/watch_group_static.dart @@ -0,0 +1,4 @@ +library watch_group_static; + +import 'package:angular/change_detection/watch_group.dart'; + diff --git a/lib/core/module.dart b/lib/core/module.dart index 4558a3d5f..846eec44e 100644 --- a/lib/core/module.dart +++ b/lib/core/module.dart @@ -2,7 +2,6 @@ library angular.core; import 'dart:async' as async; import 'dart:collection'; -import 'dart:mirrors'; import 'package:intl/intl.dart'; import 'package:di/di.dart'; @@ -43,7 +42,6 @@ class NgCoreModule extends Module { type(RootScope); factory(Scope, (injector) => injector.get(RootScope)); value(ScopeStats, new ScopeStats()); - value(GetterCache, new GetterCache({})); value(Object, {}); // RootScope context type(AstParser); type(NgZone); diff --git a/lib/core/parser/static_parser.dart b/lib/core/parser/static_parser.dart deleted file mode 100644 index b9c59ee5d..000000000 --- a/lib/core/parser/static_parser.dart +++ /dev/null @@ -1,61 +0,0 @@ -library angular.core.parser.static_parser; - -import 'package:angular/core/module.dart' show FilterMap, NgInjectableService; -import 'package:angular/core/parser/parser.dart'; -import 'package:angular/core/parser/utils.dart' show EvalError; -import 'package:angular/core/parser/syntax.dart'; - -class StaticParserFunctions { - final Map eval; - final Map assign; - StaticParserFunctions(this.eval, this.assign); -} - -@NgInjectableService() -class StaticParser implements Parser { - final StaticParserFunctions _functions; - final DynamicParser _fallbackParser; - final Map _cache = {}; - StaticParser(this._functions, this._fallbackParser); - - Expression call(String input) { - if (input == null) input = ''; - return _cache.putIfAbsent(input, () => _construct(input)); - } - - Expression _construct(String input) { - var eval = _functions.eval[input]; - if (eval == null) return _fallbackParser(input); - if (eval is !Function) throw eval; - Function assign = _functions.assign[input]; - return new StaticExpression(input, eval, assign); - } -} - -class StaticExpression extends Expression { - final String _input; - final Function _eval; - final Function _assign; - StaticExpression(this._input, this._eval, [this._assign]); - - bool get isAssignable => _assign != null; - accept(Visitor visitor) => throw "Cannot visit static expression $this"; - toString() => _input; - - eval(scope, [FilterMap filters = defaultFilterMap]) { - try { - return _eval(scope, filters); - } on EvalError catch (e, s) { - throw e.unwrap("$this", s); - } - } - - assign(scope, value) { - try { - if (_assign == null) throw new EvalError("Cannot assign to $this"); - return _assign(scope, value); - } on EvalError catch (e, s) { - throw e.unwrap("$this", s); - } - } -} \ No newline at end of file diff --git a/lib/core/registry.dart b/lib/core/registry.dart index 11ed68651..8bfad16bf 100644 --- a/lib/core/registry.dart +++ b/lib/core/registry.dart @@ -70,12 +70,6 @@ abstract class AnnotationsMap { @NgInjectableService() -class MetadataExtractor { - Iterable call(Type type) { - if (reflectType(type) is TypedefMirror) return []; - var metadata = reflectClass(type).metadata; - return metadata == null - ? [] - : metadata.map((InstanceMirror im) => im.reflectee); - } +abstract class MetadataExtractor { + Iterable call(Type type); } diff --git a/lib/core/registry_dynamic.dart b/lib/core/registry_dynamic.dart new file mode 100644 index 000000000..37de9cd00 --- /dev/null +++ b/lib/core/registry_dynamic.dart @@ -0,0 +1,78 @@ +library angular.core_dynamic; + +import 'dart:mirrors'; +import 'package:angular/core/module.dart'; + +@NgInjectableService() +class DynamicMetadataExtractor implements MetadataExtractor { + final _fieldAnnotations = [ + reflectType(NgAttr), + reflectType(NgOneWay), + reflectType(NgOneWayOneTime), + reflectType(NgTwoWay), + reflectType(NgCallback) + ]; + + Iterable call(Type type) { + if (reflectType(type) is TypedefMirror) return []; + var metadata = reflectClass(type).metadata; + if (metadata == null) { + metadata = []; + } else { + metadata = metadata.map((InstanceMirror im) => map(type, im.reflectee)); + } + return metadata; + } + + map(Type type, obj) { + if (obj is NgAnnotation) { + return mapDirectiveAnnotation(type, obj); + } else { + return obj; + } + } + + NgAnnotation mapDirectiveAnnotation(Type type, NgAnnotation annotation) { + var match; + var fieldMetadata = fieldMetadataExtractor(type); + if (fieldMetadata.isNotEmpty) { + var newMap = annotation.map == null ? {} : new Map.from(annotation.map); + fieldMetadata.forEach((String fieldName, AttrFieldAnnotation ann) { + var attrName = ann.attrName; + if (newMap.containsKey(attrName)) { + throw 'Mapping for attribute $attrName is already defined (while ' + 'processing annottation for field $fieldName of $type)'; + } + newMap[attrName] = '${ann.mappingSpec}$fieldName'; + }); + annotation = annotation.cloneWithNewMap(newMap); + } + return annotation; + } + + + Map fieldMetadataExtractor(Type type) { + ClassMirror cm = reflectType(type); + final fields = {}; + cm.declarations.forEach((Symbol name, DeclarationMirror decl) { + if (decl is VariableMirror || + decl is MethodMirror && (decl.isGetter || decl.isSetter)) { + var fieldName = MirrorSystem.getName(name); + if (decl is MethodMirror && decl.isSetter) { + // Remove "=" from the end of the setter. + fieldName = fieldName.substring(0, fieldName.length - 1); + } + decl.metadata.forEach((InstanceMirror meta) { + if (_fieldAnnotations.contains(meta.type)) { + if (fields.containsKey(fieldName)) { + throw 'Attribute annotation for $fieldName is defined more ' + 'than once in $type'; + } + fields[fieldName] = meta.reflectee as AttrFieldAnnotation; + } + }); + } + }); + return fields; + } +} diff --git a/lib/core/registry_static.dart b/lib/core/registry_static.dart new file mode 100644 index 000000000..78ad4e9fa --- /dev/null +++ b/lib/core/registry_static.dart @@ -0,0 +1,17 @@ +library angular.core_static; + +import 'package:angular/angular.dart'; +import 'package:angular/core/module.dart'; + +@NgInjectableService() +class StaticMetadataExtractor extends MetadataExtractor { + Map metadataMap; + final List empty = const []; + + StaticMetadataExtractor(this.metadataMap); + + Iterable call(Type type) { + Iterable i = metadataMap[type]; + return i == null ? empty : i; + } +} diff --git a/lib/core/scope.dart b/lib/core/scope.dart index 707d59568..0246e6bca 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -389,7 +389,7 @@ class ScopeStats { } toString() => - 'digest #$_digestLoopNo:' + '${_digestLoopNo == 1 ? 'digest' : ' '} #$_digestLoopNo:' 'Field: ${_stat(digestFieldStopwatch)} ' 'Eval: ${_stat(digestEvalStopwatch)} ' 'Process: ${_stat(digestProcessStopwatch)}'; @@ -417,12 +417,14 @@ class RootScope extends Scope { String _state; RootScope(Object context, this._astParser, this._parser, - GetterCache cacheGetter, FilterMap filterMap, + FieldGetterFactory fieldGetterFactory, FilterMap filterMap, this._exceptionHandler, this._ttl, this._zone, this._scopeStats) : super(context, null, null, - new RootWatchGroup(new DirtyCheckingChangeDetector(cacheGetter), context), - new RootWatchGroup(new DirtyCheckingChangeDetector(cacheGetter), context)) + new RootWatchGroup(fieldGetterFactory, + new DirtyCheckingChangeDetector(fieldGetterFactory), context), + new RootWatchGroup(fieldGetterFactory, + new DirtyCheckingChangeDetector(fieldGetterFactory), context)) { _zone.onTurnDone = apply; _zone.onError = (e, s, ls) => _exceptionHandler(e, s); diff --git a/lib/core/zone.dart b/lib/core/zone.dart index 6d5eb3e39..c1c04ac48 100644 --- a/lib/core/zone.dart +++ b/lib/core/zone.dart @@ -108,7 +108,7 @@ class NgZone { /** * A function called with any errors from the zone. */ - var onError = (e, s, ls) => null; + var onError = (e, s, ls) => print('$e\n$s\n$ls'); /** * A function that is called at the end of each VM turn in which the diff --git a/lib/core_dom/directive_map.dart b/lib/core_dom/directive_map.dart index affd2fab2..5a740af3a 100644 --- a/lib/core_dom/directive_map.dart +++ b/lib/core_dom/directive_map.dart @@ -9,62 +9,8 @@ class DirectiveMap extends AnnotationsMap { return _selector = _directiveSelectorFactory.selector(this); } - DirectiveMap(Injector injector, MetadataExtractor metadataExtractor, - FieldMetadataExtractor fieldMetadataExtractor, - this._directiveSelectorFactory) - : super(injector, metadataExtractor) { - final directives = >{}; - forEach((NgAnnotation annotation, Type type) { - var match; - var fieldMetadata = fieldMetadataExtractor(type); - if (fieldMetadata.isNotEmpty) { - var newMap = annotation.map == null ? {} : new Map.from(annotation.map); - fieldMetadata.forEach((String fieldName, AttrFieldAnnotation ann) { - var attrName = ann.attrName; - if (newMap.containsKey(attrName)) { - throw 'Mapping for attribute $attrName is already defined (while ' - 'processing annottation for field $fieldName of $type)'; - } - newMap[attrName] = '${ann.mappingSpec}$fieldName'; - }); - annotation = annotation.cloneWithNewMap(newMap); - } - directives.putIfAbsent(annotation, () => []).add(type); - }); - map - ..clear() - ..addAll(directives); - } -} - -@NgInjectableService() -class FieldMetadataExtractor implements Function { - final _fieldAnnotations = [reflectType(NgAttr), reflectType(NgOneWay), - reflectType(NgOneWayOneTime), reflectType(NgTwoWay), - reflectType(NgCallback)]; - - Map call(Type type) { - ClassMirror cm = reflectType(type); - final fields = {}; - cm.declarations.forEach((Symbol name, DeclarationMirror decl) { - if (decl is VariableMirror || - decl is MethodMirror && (decl.isGetter || decl.isSetter)) { - var fieldName = MirrorSystem.getName(name); - if (decl is MethodMirror && decl.isSetter) { - // Remove "=" from the end of the setter. - fieldName = fieldName.substring(0, fieldName.length - 1); - } - decl.metadata.forEach((InstanceMirror meta) { - if (_fieldAnnotations.contains(meta.type)) { - if (fields.containsKey(fieldName)) { - throw 'Attribute annotation for $fieldName is defined more ' - 'than once in $type'; - } - fields[fieldName] = meta.reflectee as AttrFieldAnnotation; - } - }); - } - }); - return fields; - } + DirectiveMap(Injector injector, + MetadataExtractor metadataExtractor, + this._directiveSelectorFactory) + : super(injector, metadataExtractor); } diff --git a/lib/core_dom/module.dart b/lib/core_dom/module.dart index 3fc9e2e89..2316df0f6 100644 --- a/lib/core_dom/module.dart +++ b/lib/core_dom/module.dart @@ -3,7 +3,6 @@ library angular.core.dom; import 'dart:async' as async; import 'dart:convert' show JSON; import 'dart:html' as dom; -import 'dart:mirrors'; import 'package:di/di.dart'; import 'package:perf_api/perf_api.dart'; @@ -50,7 +49,6 @@ class NgCoreDomModule extends Module { type(BrowserCookies); type(Cookies); type(LocationWrapper); - type(FieldMetadataExtractor); type(DirectiveMap); type(DirectiveSelectorFactory); } diff --git a/lib/metadata.dart b/lib/metadata.dart new file mode 100644 index 000000000..e69de29bb diff --git a/lib/mock/module.dart b/lib/mock/module.dart index 92b7386a6..f62cf96e6 100644 --- a/lib/mock/module.dart +++ b/lib/mock/module.dart @@ -6,6 +6,7 @@ import 'dart:convert' show JSON; import 'dart:html'; import 'dart:js' as js; import 'package:angular/angular.dart'; +import 'package:angular/angular_dynamic.dart'; import 'package:angular/utils.dart' as utils; import 'package:di/di.dart'; import 'package:di/dynamic_injector.dart'; diff --git a/lib/mock/test_injection.dart b/lib/mock/test_injection.dart index 2ca2b6c2c..471eaa424 100644 --- a/lib/mock/test_injection.dart +++ b/lib/mock/test_injection.dart @@ -127,7 +127,9 @@ module(fnOrModule) { void setUpInjector() { _currentSpecInjector = new _SpecInjector(); _currentSpecInjector.module((Module m) { - m..install(new AngularModule())..install(new AngularMockModule()); + m + ..install(new NgDynamicApp().ngModule) + ..install(new AngularMockModule()); }); } diff --git a/lib/routing/routing.dart b/lib/routing/routing.dart index 4a26bf6f8..948c3f26a 100644 --- a/lib/routing/routing.dart +++ b/lib/routing/routing.dart @@ -137,7 +137,7 @@ class NgRoutingHelper { }); }); - router.listen(appRoot: _ngApp.root); + router.listen(appRoot: _ngApp.element); } _reloadViews({Route startingFrom}) { diff --git a/perf/invoke_perf.dart b/perf/invoke_perf.dart index 09c45fd46..524b2a56f 100644 --- a/perf/invoke_perf.dart +++ b/perf/invoke_perf.dart @@ -1,31 +1,132 @@ library angular.perf.invoke; -import '_perf.dart'; -import 'dart:async'; +import 'package:benchmark_harness/benchmark_harness.dart'; main() { - var handleDirect = (a, b, c) => a; - var wrap = new Wrap(); - var handleDirectNamed = ({a, b, c}) => a; - var handleIndirect = (e) => e.a; - var streamC = new StreamController(sync:true); - var stream = streamC.stream..listen(handleIndirect); - - time('direct', () => handleDirect(1, 2, 3) ); - time('.call', () => wrap(1, 2, 3) ); - time('directNamed', () => handleDirectNamed(a:1, b:2, c:3) ); - time('indirect', () => handleIndirect(new Container(1, 2, 3)) ); - time('stream', () => streamC.add(new Container(1, 2, 3))); + new MonomorphicClosure0ListInvoke().report(); + new PolymorphicMethod0ListInvoke().report(); + new PolymorphicClosure0ListInvoke().report(); + new PolymorphicMethod1ListInvoke().report(); + new PolymorphicClosure1ListInvoke().report(); } -class Container { - var a; - var b; - var c; +var closure0Factory = [ + () => () => 0, + () => () => 1, + () => () => 2, + () => () => 3, + () => () => 4, + () => () => 5, + () => () => 6, + () => () => 7, + () => () => 8, + () => () => 9, +]; - Container(this.a, this.b, this.c); +var closure1Factory = [ + () => (i) => 0, + () => (i) => 1, + () => (i) => 2, + () => (i) => 3, + () => (i) => 4, + () => (i) => 5, + () => (i) => 6, + () => (i) => 7, + () => (i) => 8, + () => (i) => 9, +]; + +var instanceFactory = [ + () => new Obj0(), + () => new Obj1(), + () => new Obj2(), + () => new Obj3(), + () => new Obj4(), + () => new Obj5(), + () => new Obj6(), + () => new Obj7(), + () => new Obj8(), + () => new Obj9(), +]; + +class Obj0 { method0() => 0; method1(i) => 0; } +class Obj1 { method0() => 1; method1(i) => 1; } +class Obj2 { method0() => 2; method1(i) => 2; } +class Obj3 { method0() => 3; method1(i) => 3; } +class Obj4 { method0() => 4; method1(i) => 4; } +class Obj5 { method0() => 5; method1(i) => 5; } +class Obj6 { method0() => 6; method1(i) => 6; } +class Obj7 { method0() => 7; method1(i) => 7; } +class Obj8 { method0() => 8; method1(i) => 8; } +class Obj9 { method0() => 9; method1(i) => 9; } + +class PolymorphicClosure0ListInvoke extends BenchmarkBase { + PolymorphicClosure0ListInvoke() : super('PolymorphicClosure0ListInvoke'); + + var list = new List.generate(10000, (i) => closure0Factory[i%10]()); + + run() { + int sum = 0; + for(var i=0; i < list.length; i++) { + sum += list[i](); + } + return sum; + } +} + +class MonomorphicClosure0ListInvoke extends BenchmarkBase { + MonomorphicClosure0ListInvoke() : super('MonomorphicClosure0ListInvoke'); + + var list = new List.generate(10000, (i) => closure0Factory[0]()); + + run() { + int sum = 0; + for(var i=0; i < list.length; i++) { + sum += list[i](); + } + return sum; + } +} + +class PolymorphicClosure1ListInvoke extends BenchmarkBase { + PolymorphicClosure1ListInvoke() : super('PolymorphicClosure1ListInvoke'); + + var list = new List.generate(10000, (i) => closure1Factory[i%10]()); + + run() { + int sum = 0; + for(var i=0; i < list.length; i++) { + sum += list[i](i); + } + return sum; + } +} + +class PolymorphicMethod0ListInvoke extends BenchmarkBase { + PolymorphicMethod0ListInvoke() : super('PolymorphicMethod0ListInvoke'); + + var list = new List.generate(10000, (i) => instanceFactory[i%10]()); + + run() { + int sum = 0; + for(var i=0; i < list.length; i++) { + sum += list[i].method0(); + } + return sum; + } } -class Wrap { - call(a, b, c) => a + b + c; +class PolymorphicMethod1ListInvoke extends BenchmarkBase { + PolymorphicMethod1ListInvoke() : super('PolymorphicMethod1ListInvoke'); + + var list = new List.generate(10000, (i) => instanceFactory[i%10]()); + + run() { + int sum = 0; + for(var i=0; i < list.length; i++) { + sum += list[i].method1(i); + } + return sum; + } } + diff --git a/perf/mirror_perf.dart b/perf/mirror_perf.dart deleted file mode 100644 index 478906040..000000000 --- a/perf/mirror_perf.dart +++ /dev/null @@ -1,65 +0,0 @@ -library angular.perf.mirror; - -import '_perf.dart'; -import 'dart:mirrors'; -import 'package:angular/change_detection/dirty_checking_change_detector.dart'; - -main() { - var c = new _Obj(1); - InstanceMirror im = reflect(c); - Symbol symbol = const Symbol('a'); - _Watch head = new _Watch(); - _Watch current = head; - GetterCache getterCache = new GetterCache({}); - var detector = new DirtyCheckingChangeDetector(getterCache); - for(var i=1; i < 10000; i++) { - _Watch next = new _Watch(); - current = (current.next = new _Watch()); - detector.watch(c, 'a', ''); - } - - var dirtyCheck = () { - _Watch current = head; - while(current != null) { - if (!identical(current.lastValue, current.im.getField(current.symbol).reflectee)) { - throw "We should not get here"; - } - current = current.next; - } - }; - - var dirtyCheckFn = () { - _Watch current = head; - while(current != null) { - if (!identical(current.lastValue, current.getter(current.object))) { - throw "We should not get here"; - } - current = current.next; - } - }; - - xtime('fieldRead', () => im.getField(symbol).reflectee ); - xtime('Object.observe', dirtyCheck); - xtime('Object.observe fn()', dirtyCheckFn); - time('ChangeDetection', detector.collectChanges); -} - -class _Watch { - dynamic lastValue = 1; - _Watch next; - String location; - dynamic object = new _Obj(1); - InstanceMirror im; - Symbol symbol = const Symbol('a'); - Function getter = (s) => s.a; - - _Watch() { - im = reflect(object); - } -} - -class _Obj { - var a; - - _Obj(this.a); -} diff --git a/perf/watch_group_perf.dart b/perf/watch_group_perf.dart index bfc2ab6a5..6f2fcec24 100644 --- a/perf/watch_group_perf.dart +++ b/perf/watch_group_perf.dart @@ -2,6 +2,8 @@ library angular.perf.watch_group; import '_perf.dart'; import 'package:angular/change_detection/dirty_checking_change_detector.dart'; +import 'package:angular/change_detection/dirty_checking_change_detector_dynamic.dart'; +import 'package:angular/change_detection/dirty_checking_change_detector_static.dart'; import 'package:angular/change_detection/watch_group.dart'; import 'package:benchmark_harness/benchmark_harness.dart'; @@ -14,7 +16,14 @@ import 'package:benchmark_harness/benchmark_harness.dart'; import 'dart:mirrors' show MirrorsUsed; var _reactionFn = (_, __) => null; -var _getterCache = new GetterCache({}); +var _staticFieldGetterFactory = new StaticFieldGetterFactory({ + "a": (o) => o.a, "b": (o) => o.b, "c": (o) => o.c, "d": (o) => o.d, "e": (o) => o.e, + "f": (o) => o.f, "g": (o) => o.g, "h": (o) => o.h, "i": (o) => o.i, "j": (o) => o.j, + "k": (o) => o.k, "l": (o) => o.l, "m": (o) => o.m, "n": (o) => o.n, "o": (o) => o.o, + "p": (o) => o.p, "q": (o) => o.q, "r": (o) => o.r, "s": (o) => o.s, "t": (o) => o.t, +}); +var _dynamicFieldGetterFactory = new DynamicFieldGetterFactory(); + main() { _fieldRead(); _fieldReadGetter(); @@ -27,7 +36,7 @@ main() { class _CollectionCheck extends BenchmarkBase { List list = new List.generate(1000, (i) => i); - var detector = new DirtyCheckingChangeDetector(_getterCache); + var detector = new DirtyCheckingChangeDetector(_dynamicFieldGetterFactory); _CollectionCheck(): super('change-detect List[1000]') { detector @@ -41,8 +50,8 @@ class _CollectionCheck extends BenchmarkBase { } _fieldRead() { - var watchGrp = new RootWatchGroup( - new DirtyCheckingChangeDetector(_getterCache), new _Obj()) + var watchGrp = new RootWatchGroup(_dynamicFieldGetterFactory, + new DirtyCheckingChangeDetector(_dynamicFieldGetterFactory), new _Obj()) ..watch(_parse('a'), _reactionFn) ..watch(_parse('b'), _reactionFn) ..watch(_parse('c'), _reactionFn) @@ -70,14 +79,8 @@ _fieldRead() { } _fieldReadGetter() { - var getterCache = new GetterCache({ - "a": (o) => o.a, "b": (o) => o.b, "c": (o) => o.c, "d": (o) => o.d, "e": (o) => o.e, - "f": (o) => o.f, "g": (o) => o.g, "h": (o) => o.h, "i": (o) => o.i, "j": (o) => o.j, - "k": (o) => o.k, "l": (o) => o.l, "m": (o) => o.m, "n": (o) => o.n, "o": (o) => o.o, - "p": (o) => o.p, "q": (o) => o.q, "r": (o) => o.r, "s": (o) => o.s, "t": (o) => o.t, - }); - var watchGrp= new RootWatchGroup( - new DirtyCheckingChangeDetector(getterCache), new _Obj()) + var watchGrp= new RootWatchGroup(_staticFieldGetterFactory, + new DirtyCheckingChangeDetector(_staticFieldGetterFactory), new _Obj()) ..watch(_parse('a'), _reactionFn) ..watch(_parse('b'), _reactionFn) ..watch(_parse('c'), _reactionFn) @@ -110,8 +113,8 @@ _mapRead() { 'f': 0, 'g': 1, 'h': 2, 'i': 3, 'j': 4, 'k': 0, 'l': 1, 'm': 2, 'n': 3, 'o': 4, 'p': 0, 'q': 1, 'r': 2, 's': 3, 't': 4}; - var watchGrp = new RootWatchGroup( - new DirtyCheckingChangeDetector(_getterCache), map) + var watchGrp = new RootWatchGroup(_dynamicFieldGetterFactory, + new DirtyCheckingChangeDetector(_dynamicFieldGetterFactory), map) ..watch(_parse('a'), _reactionFn) ..watch(_parse('b'), _reactionFn) ..watch(_parse('c'), _reactionFn) @@ -140,8 +143,8 @@ _mapRead() { _methodInvoke0() { var context = new _Obj(); context.a = new _Obj(); - var watchGrp = new RootWatchGroup( - new DirtyCheckingChangeDetector(_getterCache), context) + var watchGrp = new RootWatchGroup(_dynamicFieldGetterFactory, + new DirtyCheckingChangeDetector(_dynamicFieldGetterFactory), context) ..watch(_method('a', 'methodA'), _reactionFn) ..watch(_method('a', 'methodB'), _reactionFn) ..watch(_method('a', 'methodC'), _reactionFn) @@ -170,8 +173,8 @@ _methodInvoke0() { _methodInvoke1() { var context = new _Obj(); context.a = new _Obj(); - var watchGrp = new RootWatchGroup( - new DirtyCheckingChangeDetector(_getterCache), context) + var watchGrp = new RootWatchGroup(_dynamicFieldGetterFactory, + new DirtyCheckingChangeDetector(_dynamicFieldGetterFactory), context) ..watch(_method('a', 'methodA1', [_parse('a')]), _reactionFn) ..watch(_method('a', 'methodB1', [_parse('a')]), _reactionFn) ..watch(_method('a', 'methodC1', [_parse('a')]), _reactionFn) @@ -199,8 +202,8 @@ _methodInvoke1() { _function2() { var context = new _Obj(); - var watchGrp = new RootWatchGroup( - new DirtyCheckingChangeDetector(_getterCache), context) + var watchGrp = new RootWatchGroup(_dynamicFieldGetterFactory, + new DirtyCheckingChangeDetector(_dynamicFieldGetterFactory), context) ..watch(_add(0, _parse('a'), _parse('a')), _reactionFn) ..watch(_add(1, _parse('a'), _parse('a')), _reactionFn) ..watch(_add(2, _parse('a'), _parse('a')), _reactionFn) diff --git a/test/bootstrap_spec.dart b/test/bootstrap_spec.dart index b74e7b263..f0c6e5dee 100644 --- a/test/bootstrap_spec.dart +++ b/test/bootstrap_spec.dart @@ -1,6 +1,7 @@ library bootstrap_spec; import '_specs.dart'; +import 'package:angular/angular_dynamic.dart'; void main() { describe('bootstrap', () { @@ -8,7 +9,7 @@ void main() { it('should default to whole page', () { body.innerHtml = '
{{"works"}}
'; - ngBootstrap(); + new NgDynamicApp().run(); expect(body.innerHtml).toEqual('
works
'); }); @@ -16,7 +17,7 @@ void main() { body.setInnerHtml( '
{{ignor me}}
', treeSanitizer: new NullTreeSanitizer()); - ngBootstrap(); + new NgDynamicApp().run(); expect(body.text).toEqual('{{ignor me}}works'); }); @@ -24,7 +25,7 @@ void main() { body.setInnerHtml( '
{{ignor me}}
', treeSanitizer: new NullTreeSanitizer()); - ngBootstrap(element:body.querySelector('div[ng-bind]')); + new NgDynamicApp()..selector('div[ng-bind]')..run(); expect(body.text).toEqual('{{ignor me}}works'); }); }); diff --git a/test/change_detection/dirty_checking_change_detector_spec.dart b/test/change_detection/dirty_checking_change_detector_spec.dart index 8f54dca37..f975d32cf 100644 --- a/test/change_detection/dirty_checking_change_detector_spec.dart +++ b/test/change_detection/dirty_checking_change_detector_spec.dart @@ -1,515 +1,523 @@ -library dirty_chekcing_change_detector_spec; +library dirty_checking_change_detector_spec; import '../_specs.dart'; import 'package:angular/change_detection/change_detection.dart'; import 'package:angular/change_detection/dirty_checking_change_detector.dart'; +import 'package:angular/change_detection/dirty_checking_change_detector_dynamic.dart'; +import 'package:angular/change_detection/dirty_checking_change_detector_static.dart'; import 'dart:collection'; void main() { - describe('DirtyCheckingChangeDetector', () { - DirtyCheckingChangeDetector detector; + var staticFieldGetterFactory = new StaticFieldGetterFactory({ + 'first': (o) => o.first, + 'last': (o) => o.last, + 'age': (o) => o.age, + }); - beforeEach(() { - GetterCache getterCache = new GetterCache({ - "first": (o) => o.first, - "age": (o) => o.age - }); - detector = new DirtyCheckingChangeDetector(getterCache); - }); + they(FieldGetterFactory fieldGetterFactory, String modeName) { + describe('DirtyCheckingChangeDetector-$modeName', () { + DirtyCheckingChangeDetector detector; - describe('object field', () { - it('should detect nothing', () { - var changes = detector.collectChanges(); - expect(changes.moveNext()).toEqual(false); + beforeEach(() { + detector = new DirtyCheckingChangeDetector(fieldGetterFactory); }); - it('should detect field changes', () { - var user = new _User('', ''); - Iterator changeIterator; + describe('object field', () { + it('should detect nothing', () { + var changes = detector.collectChanges(); + expect(changes.moveNext()).toEqual(false); + }); + + it('should detect field changes', () { + var user = new _User('', ''); + Iterator changeIterator; - detector + detector ..watch(user, 'first', null) ..watch(user, 'last', null) ..collectChanges(); // throw away first set - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(false); - user..first = 'misko' + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(false); + user..first = 'misko' ..last = 'hevery'; - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.currentValue).toEqual('misko'); - expect(changeIterator.current.previousValue).toEqual(''); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.currentValue).toEqual('hevery'); - expect(changeIterator.current.previousValue).toEqual(''); - expect(changeIterator.moveNext()).toEqual(false); - - // force different instance - user.first = 'mis'; - user.first += 'ko'; - - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(false); - - user.last = 'Hevery'; - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.currentValue).toEqual('Hevery'); - expect(changeIterator.current.previousValue).toEqual('hevery'); - expect(changeIterator.moveNext()).toEqual(false); - }); - - it('should ignore NaN != NaN', () { - var user = new _User(); - user.age = double.NAN; - detector..watch(user, 'age', null)..collectChanges(); // throw away first set - - var changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(false); - - user.age = 123; - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.currentValue).toEqual(123); - expect(changeIterator.current.previousValue.isNaN).toEqual(true); - expect(changeIterator.moveNext()).toEqual(false); - }); - - it('should treat map field dereference as []', () { - var obj = {'name':'misko'}; - detector.watch(obj, 'name', null); - detector.collectChanges(); // throw away first set - - obj['name'] = 'Misko'; - var changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.currentValue).toEqual('Misko'); - expect(changeIterator.current.previousValue).toEqual('misko'); - }); - }); - - describe('insertions / removals', () { - it('should insert at the end of list', () { - var obj = {}; - var a = detector.watch(obj, 'a', 'a'); - var b = detector.watch(obj, 'b', 'b'); - - obj['a'] = obj['b'] = 1; - var changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.handler).toEqual('a'); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.handler).toEqual('b'); - expect(changeIterator.moveNext()).toEqual(false); - - obj['a'] = obj['b'] = 2; - a.remove(); - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.handler).toEqual('b'); - expect(changeIterator.moveNext()).toEqual(false); - - obj['a'] = obj['b'] = 3; - b.remove(); - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(false); - }); - - it('should remove all watches in group and group\'s children', () { - var obj = {}; - detector.watch(obj, 'a', '0a'); - var child1a = detector.newGroup(); - var child1b = detector.newGroup(); - var child2 = child1a.newGroup(); - child1a.watch(obj,'a', '1a'); - child1b.watch(obj,'a', '1b'); - detector.watch(obj, 'a', '0A'); - child1a.watch(obj,'a', '1A'); - child2.watch(obj,'a', '2A'); - - var iterator; - obj['a'] = 1; - expect(detector.collectChanges(), - toEqualChanges(['0a', '0A', '1a', '1A', '2A', '1b'])); - - obj['a'] = 2; - child1a.remove(); // should also remove child2 - expect(detector.collectChanges(), toEqualChanges(['0a', '0A', '1b'])); - }); - - it('should add watches within its own group', () { - var obj = {}; - var ra = detector.watch(obj, 'a', 'a'); - var child = detector.newGroup(); - var cb = child.watch(obj,'b', 'b'); - var iterotar; - - obj['a'] = obj['b'] = 1; - expect(detector.collectChanges(), toEqualChanges(['a', 'b'])); - - obj['a'] = obj['b'] = 2; - ra.remove(); - expect(detector.collectChanges(), toEqualChanges(['b'])); - - obj['a'] = obj['b'] = 3; - cb.remove(); - expect(detector.collectChanges(), toEqualChanges([])); - - // TODO: add them back in wrong order, assert events in right order - cb = child.watch(obj,'b', 'b'); - ra = detector.watch(obj, 'a', 'a'); - obj['a'] = obj['b'] = 4; - expect(detector.collectChanges(), toEqualChanges(['a', 'b'])); - }); - - it('should properly add children', () { - var a = detector.newGroup(); - var aChild = a.newGroup(); - var b = detector.newGroup(); - expect(detector.collectChanges).not.toThrow(); - }); - }); - - describe('list watching', () { - it('should detect changes in list', () { - var list = []; - var record = detector.watch(list, null, 'handler'); - expect(detector.collectChanges().moveNext()).toEqual(false); - var iterator; - - list.add('a'); - iterator = detector.collectChanges(); - expect(iterator.moveNext()).toEqual(true); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['a[null -> 0]'], - additions: ['a[null -> 0]'], - moves: [], - removals: [])); - - list.add('b'); - iterator = detector.collectChanges(); - expect(iterator.moveNext()).toEqual(true); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['a', 'b[null -> 1]'], - additions: ['b[null -> 1]'], - moves: [], - removals: [])); - - list.add('c'); - list.add('d'); - iterator = detector.collectChanges(); - expect(iterator.moveNext()).toEqual(true); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['a', 'b', 'c[null -> 2]', 'd[null -> 3]'], - additions: ['c[null -> 2]', 'd[null -> 3]'], - moves: [], - removals: [])); - - list.remove('c'); - expect(list).toEqual(['a', 'b', 'd']); - iterator = detector.collectChanges(); - expect(iterator.moveNext()).toEqual(true); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['a', 'b', 'd[3 -> 2]'], - additions: [], - moves: ['d[3 -> 2]'], - removals: ['c[2 -> null]'])); - - list.clear(); - list.addAll(['d', 'c', 'b', 'a']); - iterator = detector.collectChanges(); - expect(iterator.moveNext()).toEqual(true); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['d[2 -> 0]', 'c[null -> 1]', 'b[1 -> 2]', 'a[0 -> 3]'], - additions: ['c[null -> 1]'], - moves: ['d[2 -> 0]', 'b[1 -> 2]', 'a[0 -> 3]'], - removals: [])); - }); - - it('should detect changes in list', () { - var list = []; - var record = detector.watch(list.map((i) => i), null, 'handler'); - expect(detector.collectChanges().moveNext()).toEqual(false); - var iterator; - - list.add('a'); - iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['a[null -> 0]'], - additions: ['a[null -> 0]'], - moves: [], - removals: [])); - - list.add('b'); - iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['a', 'b[null -> 1]'], - additions: ['b[null -> 1]'], - moves: [], - removals: [])); - - list.add('c'); - list.add('d'); - iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['a', 'b', 'c[null -> 2]', 'd[null -> 3]'], - additions: ['c[null -> 2]', 'd[null -> 3]'], - moves: [], - removals: [])); - - list.remove('c'); - expect(list).toEqual(['a', 'b', 'd']); - iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['a', 'b', 'd[3 -> 2]'], - additions: [], - moves: ['d[3 -> 2]'], - removals: ['c[2 -> null]'])); - - list.clear(); - list.addAll(['d', 'c', 'b', 'a']); - iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['d[2 -> 0]', 'c[null -> 1]', 'b[1 -> 2]', 'a[0 -> 3]'], - additions: ['c[null -> 1]'], - moves: ['d[2 -> 0]', 'b[1 -> 2]', 'a[0 -> 3]'], - removals: [])); - }); - - it('should test string by value rather than by reference', () { - var list = ['a', 'boo']; - detector..watch(list, null, null)..collectChanges(); - - list[1] = 'b' + 'oo'; - - expect(detector.collectChanges().moveNext()).toEqual(false); - }); - - it('should ignore [NaN] != [NaN]', () { - var list = [double.NAN]; - var record = detector..watch(list, null, null)..collectChanges(); - - expect(detector.collectChanges().moveNext()).toEqual(false); - }); - - it('should remove and add same item', () { - var list = ['a', 'b', 'c']; - var record = detector.watch(list, null, 'handler'); - var iterator; - detector.collectChanges(); - - list.remove('b'); - iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['a', 'c[2 -> 1]'], - additions: [], - moves: ['c[2 -> 1]'], - removals: ['b[1 -> null]'])); - - list.insert(1, 'b'); - expect(list).toEqual(['a', 'b', 'c']); - iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['a', 'b[null -> 1]', 'c[1 -> 2]'], - additions: ['b[null -> 1]'], - moves: ['c[1 -> 2]'], - removals: [])); - }); - - it('should support duplicates', () { - var list = ['a', 'a', 'a', 'b', 'b']; - var record = detector.watch(list, null, 'handler'); - detector.collectChanges(); - - list.removeAt(0); - var iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['a', 'a', 'b[3 -> 2]', 'b[4 -> 3]'], - additions: [], - moves: ['b[3 -> 2]', 'b[4 -> 3]'], - removals: ['a[2 -> null]'])); - }); - - - it('should support insertions/moves', () { - var list = ['a', 'a', 'b', 'b']; - var record = detector.watch(list, null, 'handler'); - var iterator; - detector.collectChanges(); - list.insert(0, 'b'); - expect(list).toEqual(['b', 'a', 'a', 'b', 'b']); - iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['b[2 -> 0]', 'a[0 -> 1]', 'a[1 -> 2]', 'b', 'b[null -> 4]'], - additions: ['b[null -> 4]'], - moves: ['b[2 -> 0]', 'a[0 -> 1]', 'a[1 -> 2]'], - removals: [])); - }); - - it('should support UnmodifiableListView', () { - var hiddenList = [1]; - var list = new UnmodifiableListView(hiddenList); - var record = detector.watch(list, null, 'handler'); - var iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['1[null -> 0]'], - additions: ['1[null -> 0]'], - moves: [], - removals: [])); - - // assert no changes detected - expect(detector.collectChanges().moveNext()).toEqual(false); - - // change the hiddenList normally this should trigger change detection - // but because we are wrapped in UnmodifiableListView we see nothing. - hiddenList[0] = 2; - expect(detector.collectChanges().moveNext()).toEqual(false); - }); - - it('should bug', () { - var list = [1, 2, 3, 4]; - var record = detector.watch(list, null, 'handler'); - var iterator; - - iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['1[null -> 0]', '2[null -> 1]', '3[null -> 2]', '4[null -> 3]'], - additions: ['1[null -> 0]', '2[null -> 1]', '3[null -> 2]', '4[null -> 3]'], - moves: [], - removals: [])); - detector.collectChanges(); - - list.removeRange(0, 1); - iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['2[1 -> 0]', '3[2 -> 1]', '4[3 -> 2]'], - additions: [], - moves: ['2[1 -> 0]', '3[2 -> 1]', '4[3 -> 2]'], - removals: ['1[0 -> null]'])); - - list.insert(0, 1); - iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['1[null -> 0]', '2[0 -> 1]', '3[1 -> 2]', '4[2 -> 3]'], - additions: ['1[null -> 0]'], - moves: ['2[0 -> 1]', '3[1 -> 2]', '4[2 -> 3]'], - removals: [])); - }); - }); - - describe('map watching', () { - it('should do basic map watching', () { - var map = {}; - var record = detector.watch(map, null, 'handler'); - expect(detector.collectChanges().moveNext()).toEqual(false); - - var changeIterator; - map['a'] = 'A'; - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.currentValue, toEqualMapRecord( - map: ['a[null -> A]'], - additions: ['a[null -> A]'], - changes: [], - removals: [])); - - map['b'] = 'B'; - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.currentValue, toEqualMapRecord( - map: ['a', 'b[null -> B]'], - additions: ['b[null -> B]'], - changes: [], - removals: [])); - - map['b'] = 'BB'; - map['d'] = 'D'; - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.currentValue, toEqualMapRecord( - map: ['a', 'b[B -> BB]', 'd[null -> D]'], - additions: ['d[null -> D]'], - changes: ['b[B -> BB]'], - removals: [])); - - map.remove('b'); - expect(map).toEqual({'a': 'A', 'd':'D'}); - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.currentValue, toEqualMapRecord( - map: ['a', 'd'], - additions: [], - changes: [], - removals: ['b[BB -> null]'])); - - map.clear(); - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.currentValue, toEqualMapRecord( - map: [], - additions: [], - changes: [], - removals: ['a[A -> null]', 'd[D -> null]'])); - }); - - it('should test string keys by value rather than by reference', () { - var map = {'foo': 0}; - detector..watch(map, null, null)..collectChanges(); - - map['f' + 'oo'] = 0; - - expect(detector.collectChanges().moveNext()).toEqual(false); + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.currentValue).toEqual('misko'); + expect(changeIterator.current.previousValue).toEqual(''); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.currentValue).toEqual('hevery'); + expect(changeIterator.current.previousValue).toEqual(''); + expect(changeIterator.moveNext()).toEqual(false); + + // force different instance + user.first = 'mis'; + user.first += 'ko'; + + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(false); + + user.last = 'Hevery'; + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.currentValue).toEqual('Hevery'); + expect(changeIterator.current.previousValue).toEqual('hevery'); + expect(changeIterator.moveNext()).toEqual(false); + }); + + it('should ignore NaN != NaN', () { + var user = new _User(); + user.age = double.NAN; + detector..watch(user, 'age', null)..collectChanges(); // throw away first set + + var changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(false); + + user.age = 123; + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.currentValue).toEqual(123); + expect(changeIterator.current.previousValue.isNaN).toEqual(true); + expect(changeIterator.moveNext()).toEqual(false); + }); + + it('should treat map field dereference as []', () { + var obj = {'name':'misko'}; + detector.watch(obj, 'name', null); + detector.collectChanges(); // throw away first set + + obj['name'] = 'Misko'; + var changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.currentValue).toEqual('Misko'); + expect(changeIterator.current.previousValue).toEqual('misko'); + }); }); - it('should test string values by value rather than by reference', () { - var map = {'foo': 'bar'}; - detector..watch(map, null, null)..collectChanges(); - - map['foo'] = 'b' + 'ar'; - - expect(detector.collectChanges().moveNext()).toEqual(false); + describe('insertions / removals', () { + it('should insert at the end of list', () { + var obj = {}; + var a = detector.watch(obj, 'a', 'a'); + var b = detector.watch(obj, 'b', 'b'); + + obj['a'] = obj['b'] = 1; + var changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.handler).toEqual('a'); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.handler).toEqual('b'); + expect(changeIterator.moveNext()).toEqual(false); + + obj['a'] = obj['b'] = 2; + a.remove(); + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.handler).toEqual('b'); + expect(changeIterator.moveNext()).toEqual(false); + + obj['a'] = obj['b'] = 3; + b.remove(); + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(false); + }); + + it('should remove all watches in group and group\'s children', () { + var obj = {}; + detector.watch(obj, 'a', '0a'); + var child1a = detector.newGroup(); + var child1b = detector.newGroup(); + var child2 = child1a.newGroup(); + child1a.watch(obj,'a', '1a'); + child1b.watch(obj,'a', '1b'); + detector.watch(obj, 'a', '0A'); + child1a.watch(obj,'a', '1A'); + child2.watch(obj,'a', '2A'); + + var iterator; + obj['a'] = 1; + expect(detector.collectChanges(), + toEqualChanges(['0a', '0A', '1a', '1A', '2A', '1b'])); + + obj['a'] = 2; + child1a.remove(); // should also remove child2 + expect(detector.collectChanges(), toEqualChanges(['0a', '0A', '1b'])); + }); + + it('should add watches within its own group', () { + var obj = {}; + var ra = detector.watch(obj, 'a', 'a'); + var child = detector.newGroup(); + var cb = child.watch(obj,'b', 'b'); + var iterotar; + + obj['a'] = obj['b'] = 1; + expect(detector.collectChanges(), toEqualChanges(['a', 'b'])); + + obj['a'] = obj['b'] = 2; + ra.remove(); + expect(detector.collectChanges(), toEqualChanges(['b'])); + + obj['a'] = obj['b'] = 3; + cb.remove(); + expect(detector.collectChanges(), toEqualChanges([])); + + // TODO: add them back in wrong order, assert events in right order + cb = child.watch(obj,'b', 'b'); + ra = detector.watch(obj, 'a', 'a'); + obj['a'] = obj['b'] = 4; + expect(detector.collectChanges(), toEqualChanges(['a', 'b'])); + }); + + it('should properly add children', () { + var a = detector.newGroup(); + var aChild = a.newGroup(); + var b = detector.newGroup(); + expect(detector.collectChanges).not.toThrow(); + }); }); - it('should not see a NaN value as a change', () { - var map = {'foo': double.NAN}; - var record = detector..watch(map, null, null)..collectChanges(); - - expect(detector.collectChanges().moveNext()).toEqual(false); + describe('list watching', () { + it('should detect changes in list', () { + var list = []; + var record = detector.watch(list, null, 'handler'); + expect(detector.collectChanges().moveNext()).toEqual(false); + var iterator; + + list.add('a'); + iterator = detector.collectChanges(); + expect(iterator.moveNext()).toEqual(true); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['a[null -> 0]'], + additions: ['a[null -> 0]'], + moves: [], + removals: [])); + + list.add('b'); + iterator = detector.collectChanges(); + expect(iterator.moveNext()).toEqual(true); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['a', 'b[null -> 1]'], + additions: ['b[null -> 1]'], + moves: [], + removals: [])); + + list.add('c'); + list.add('d'); + iterator = detector.collectChanges(); + expect(iterator.moveNext()).toEqual(true); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['a', 'b', 'c[null -> 2]', 'd[null -> 3]'], + additions: ['c[null -> 2]', 'd[null -> 3]'], + moves: [], + removals: [])); + + list.remove('c'); + expect(list).toEqual(['a', 'b', 'd']); + iterator = detector.collectChanges(); + expect(iterator.moveNext()).toEqual(true); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['a', 'b', 'd[3 -> 2]'], + additions: [], + moves: ['d[3 -> 2]'], + removals: ['c[2 -> null]'])); + + list.clear(); + list.addAll(['d', 'c', 'b', 'a']); + iterator = detector.collectChanges(); + expect(iterator.moveNext()).toEqual(true); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['d[2 -> 0]', 'c[null -> 1]', 'b[1 -> 2]', 'a[0 -> 3]'], + additions: ['c[null -> 1]'], + moves: ['d[2 -> 0]', 'b[1 -> 2]', 'a[0 -> 3]'], + removals: [])); + }); + + it('should detect changes in list', () { + var list = []; + var record = detector.watch(list.map((i) => i), null, 'handler'); + expect(detector.collectChanges().moveNext()).toEqual(false); + var iterator; + + list.add('a'); + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['a[null -> 0]'], + additions: ['a[null -> 0]'], + moves: [], + removals: [])); + + list.add('b'); + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['a', 'b[null -> 1]'], + additions: ['b[null -> 1]'], + moves: [], + removals: [])); + + list.add('c'); + list.add('d'); + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['a', 'b', 'c[null -> 2]', 'd[null -> 3]'], + additions: ['c[null -> 2]', 'd[null -> 3]'], + moves: [], + removals: [])); + + list.remove('c'); + expect(list).toEqual(['a', 'b', 'd']); + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['a', 'b', 'd[3 -> 2]'], + additions: [], + moves: ['d[3 -> 2]'], + removals: ['c[2 -> null]'])); + + list.clear(); + list.addAll(['d', 'c', 'b', 'a']); + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['d[2 -> 0]', 'c[null -> 1]', 'b[1 -> 2]', 'a[0 -> 3]'], + additions: ['c[null -> 1]'], + moves: ['d[2 -> 0]', 'b[1 -> 2]', 'a[0 -> 3]'], + removals: [])); + }); + + it('should test string by value rather than by reference', () { + var list = ['a', 'boo']; + detector..watch(list, null, null)..collectChanges(); + + list[1] = 'b' + 'oo'; + + expect(detector.collectChanges().moveNext()).toEqual(false); + }); + + it('should ignore [NaN] != [NaN]', () { + var list = [double.NAN]; + var record = detector..watch(list, null, null)..collectChanges(); + + expect(detector.collectChanges().moveNext()).toEqual(false); + }); + + it('should remove and add same item', () { + var list = ['a', 'b', 'c']; + var record = detector.watch(list, null, 'handler'); + var iterator; + detector.collectChanges(); + + list.remove('b'); + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['a', 'c[2 -> 1]'], + additions: [], + moves: ['c[2 -> 1]'], + removals: ['b[1 -> null]'])); + + list.insert(1, 'b'); + expect(list).toEqual(['a', 'b', 'c']); + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['a', 'b[null -> 1]', 'c[1 -> 2]'], + additions: ['b[null -> 1]'], + moves: ['c[1 -> 2]'], + removals: [])); + }); + + it('should support duplicates', () { + var list = ['a', 'a', 'a', 'b', 'b']; + var record = detector.watch(list, null, 'handler'); + detector.collectChanges(); + + list.removeAt(0); + var iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['a', 'a', 'b[3 -> 2]', 'b[4 -> 3]'], + additions: [], + moves: ['b[3 -> 2]', 'b[4 -> 3]'], + removals: ['a[2 -> null]'])); + }); + + + it('should support insertions/moves', () { + var list = ['a', 'a', 'b', 'b']; + var record = detector.watch(list, null, 'handler'); + var iterator; + detector.collectChanges(); + list.insert(0, 'b'); + expect(list).toEqual(['b', 'a', 'a', 'b', 'b']); + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['b[2 -> 0]', 'a[0 -> 1]', 'a[1 -> 2]', 'b', 'b[null -> 4]'], + additions: ['b[null -> 4]'], + moves: ['b[2 -> 0]', 'a[0 -> 1]', 'a[1 -> 2]'], + removals: [])); + }); + + it('should support UnmodifiableListView', () { + var hiddenList = [1]; + var list = new UnmodifiableListView(hiddenList); + var record = detector.watch(list, null, 'handler'); + var iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['1[null -> 0]'], + additions: ['1[null -> 0]'], + moves: [], + removals: [])); + + // assert no changes detected + expect(detector.collectChanges().moveNext()).toEqual(false); + + // change the hiddenList normally this should trigger change detection + // but because we are wrapped in UnmodifiableListView we see nothing. + hiddenList[0] = 2; + expect(detector.collectChanges().moveNext()).toEqual(false); + }); + + it('should bug', () { + var list = [1, 2, 3, 4]; + var record = detector.watch(list, null, 'handler'); + var iterator; + + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['1[null -> 0]', '2[null -> 1]', '3[null -> 2]', '4[null -> 3]'], + additions: ['1[null -> 0]', '2[null -> 1]', '3[null -> 2]', '4[null -> 3]'], + moves: [], + removals: [])); + detector.collectChanges(); + + list.removeRange(0, 1); + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['2[1 -> 0]', '3[2 -> 1]', '4[3 -> 2]'], + additions: [], + moves: ['2[1 -> 0]', '3[2 -> 1]', '4[3 -> 2]'], + removals: ['1[0 -> null]'])); + + list.insert(0, 1); + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['1[null -> 0]', '2[0 -> 1]', '3[1 -> 2]', '4[2 -> 3]'], + additions: ['1[null -> 0]'], + moves: ['2[0 -> 1]', '3[1 -> 2]', '4[2 -> 3]'], + removals: [])); + }); }); - }); - describe('DuplicateMap', () { - DuplicateMap map; - beforeEach(() => map = new DuplicateMap()); - - it('should do basic operations', () { - var k1 = 'a'; - var r1 = new ItemRecord(k1)..currentIndex = 1; - map.put(r1); - expect(map.get(k1, 2)).toEqual(null); - expect(map.get(k1, 1)).toEqual(null); - expect(map.get(k1, 0)).toEqual(r1); - expect(map.remove(r1)).toEqual(r1); - expect(map.get(k1, -1)).toEqual(null); + describe('map watching', () { + it('should do basic map watching', () { + var map = {}; + var record = detector.watch(map, null, 'handler'); + expect(detector.collectChanges().moveNext()).toEqual(false); + + var changeIterator; + map['a'] = 'A'; + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.currentValue, toEqualMapRecord( + map: ['a[null -> A]'], + additions: ['a[null -> A]'], + changes: [], + removals: [])); + + map['b'] = 'B'; + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.currentValue, toEqualMapRecord( + map: ['a', 'b[null -> B]'], + additions: ['b[null -> B]'], + changes: [], + removals: [])); + + map['b'] = 'BB'; + map['d'] = 'D'; + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.currentValue, toEqualMapRecord( + map: ['a', 'b[B -> BB]', 'd[null -> D]'], + additions: ['d[null -> D]'], + changes: ['b[B -> BB]'], + removals: [])); + + map.remove('b'); + expect(map).toEqual({'a': 'A', 'd':'D'}); + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.currentValue, toEqualMapRecord( + map: ['a', 'd'], + additions: [], + changes: [], + removals: ['b[BB -> null]'])); + + map.clear(); + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.currentValue, toEqualMapRecord( + map: [], + additions: [], + changes: [], + removals: ['a[A -> null]', 'd[D -> null]'])); + }); + + it('should test string keys by value rather than by reference', () { + var map = {'foo': 0}; + detector..watch(map, null, null)..collectChanges(); + + map['f' + 'oo'] = 0; + + expect(detector.collectChanges().moveNext()).toEqual(false); + }); + + it('should test string values by value rather than by reference', () { + var map = {'foo': 'bar'}; + detector..watch(map, null, null)..collectChanges(); + + map['foo'] = 'b' + 'ar'; + + expect(detector.collectChanges().moveNext()).toEqual(false); + }); + + it('should not see a NaN value as a change', () { + var map = {'foo': double.NAN}; + var record = detector..watch(map, null, null)..collectChanges(); + + expect(detector.collectChanges().moveNext()).toEqual(false); + }); }); - it('should do basic operations on duplicate keys', () { - var k1 = 'a'; - var r1 = new ItemRecord(k1)..currentIndex = 1; - var r2 = new ItemRecord(k1)..currentIndex = 2; - map..put(r1)..put(r2); - expect(map.get(k1, 0)).toEqual(r1); - expect(map.get(k1, 1)).toEqual(r2); - expect(map.get(k1, 2)).toEqual(null); - expect(map.remove(r2)).toEqual(r2); - expect(map.get(k1, 0)).toEqual(r1); - expect(map.remove(r1)).toEqual(r1); - expect(map.get(k1, 0)).toEqual(null); + describe('DuplicateMap', () { + DuplicateMap map; + beforeEach(() => map = new DuplicateMap()); + + it('should do basic operations', () { + var k1 = 'a'; + var r1 = new ItemRecord(k1)..currentIndex = 1; + map.put(r1); + expect(map.get(k1, 2)).toEqual(null); + expect(map.get(k1, 1)).toEqual(null); + expect(map.get(k1, 0)).toEqual(r1); + expect(map.remove(r1)).toEqual(r1); + expect(map.get(k1, -1)).toEqual(null); + }); + + it('should do basic operations on duplicate keys', () { + var k1 = 'a'; + var r1 = new ItemRecord(k1)..currentIndex = 1; + var r2 = new ItemRecord(k1)..currentIndex = 2; + map..put(r1)..put(r2); + expect(map.get(k1, 0)).toEqual(r1); + expect(map.get(k1, 1)).toEqual(r2); + expect(map.get(k1, 2)).toEqual(null); + expect(map.remove(r2)).toEqual(r2); + expect(map.get(k1, 0)).toEqual(r1); + expect(map.remove(r1)).toEqual(r1); + expect(map.get(k1, 0)).toEqual(null); + }); }); }); - }); + } + they(new DynamicFieldGetterFactory(), 'dynamic'); + they(staticFieldGetterFactory, 'static'); } class _User { diff --git a/test/change_detection/watch_group_spec.dart b/test/change_detection/watch_group_spec.dart index b18aaa2d7..cb905537f 100644 --- a/test/change_detection/watch_group_spec.dart +++ b/test/change_detection/watch_group_spec.dart @@ -1,694 +1,706 @@ library watch_group_spec; import '../_specs.dart'; -import 'package:angular/change_detection/watch_group.dart'; +import 'package:angular/change_detection/change_detection.dart'; import 'package:angular/change_detection/dirty_checking_change_detector.dart'; +import 'package:angular/change_detection/dirty_checking_change_detector_dynamic.dart'; +import 'package:angular/change_detection/dirty_checking_change_detector_static.dart'; import 'dirty_checking_change_detector_spec.dart' hide main; void main() { - describe('WatchGroup', () { - var context; - var watchGrp; - DirtyCheckingChangeDetector changeDetector; - Logger logger; - - AST parse(String expression) { - var currentAST = new ContextReferenceAST(); - expression.split('.').forEach((name) { - currentAST = new FieldReadAST(currentAST, name); - }); - return currentAST; - } - - expectOrder(list) { - logger.clear(); - watchGrp.detectChanges(); // Clear the initial queue - logger.clear(); - watchGrp.detectChanges(); - expect(logger).toEqual(list); - } - - beforeEach(inject((Logger _logger) { - context = {}; - changeDetector = new DirtyCheckingChangeDetector(new GetterCache({})); - watchGrp = new RootWatchGroup(changeDetector, context); - logger = _logger; - })); - - describe('watch lifecycle', () { - it('should prevent reaction fn on removed', () { - context['a'] = 'hello'; - var watch ; - watchGrp.watch(parse('a'), (v, p) { - logger('removed'); - watch.remove(); - }); - watch = watchGrp.watch(parse('a'), (v, p) => logger(v)); - watchGrp.detectChanges(); - expect(logger).toEqual(['removed']); - }); - }); - - describe('property chaining', () { - it('should read property', () { - context['a'] = 'hello'; - - // should fire on initial adding - expect(watchGrp.fieldCost).toEqual(0); - var watch = watchGrp.watch(parse('a'), (v, p) => logger(v)); - expect(watch.expression).toEqual('a'); - expect(watchGrp.fieldCost).toEqual(1); - watchGrp.detectChanges(); - expect(logger).toEqual(['hello']); - - // make sore no new changes are logged on extra detectChanges - watchGrp.detectChanges(); - expect(logger).toEqual(['hello']); - - // Should detect value change - context['a'] = 'bye'; - watchGrp.detectChanges(); - expect(logger).toEqual(['hello', 'bye']); - - // should cleanup after itself - watch.remove(); - expect(watchGrp.fieldCost).toEqual(0); - context['a'] = 'cant see me'; - watchGrp.detectChanges(); - expect(logger).toEqual(['hello', 'bye']); - }); - - it('should read property chain', () { - context['a'] = {'b': 'hello'}; - - // should fire on initial adding - expect(watchGrp.fieldCost).toEqual(0); - expect(changeDetector.count).toEqual(0); - var watch = watchGrp.watch(parse('a.b'), (v, p) => logger(v)); - expect(watch.expression).toEqual('a.b'); - expect(watchGrp.fieldCost).toEqual(2); - expect(changeDetector.count).toEqual(2); - watchGrp.detectChanges(); - expect(logger).toEqual(['hello']); - - // make sore no new changes are logged on extra detectChanges - watchGrp.detectChanges(); - expect(logger).toEqual(['hello']); - - // make sure no changes or logged when intermediary object changes - context['a'] = {'b': 'hello'}; - watchGrp.detectChanges(); - expect(logger).toEqual(['hello']); - - // Should detect value change - context['a'] = {'b': 'hello2'}; - watchGrp.detectChanges(); - expect(logger).toEqual(['hello', 'hello2']); - - // Should detect value change - context['a']['b'] = 'bye'; - watchGrp.detectChanges(); - expect(logger).toEqual(['hello', 'hello2', 'bye']); - - // should cleanup after itself - watch.remove(); - expect(watchGrp.fieldCost).toEqual(0); - context['a']['b'] = 'cant see me'; - watchGrp.detectChanges(); - expect(logger).toEqual(['hello', 'hello2', 'bye']); - }); - - it('should reuse handlers', () { - var user1 = {'first': 'misko', 'last': 'hevery'}; - var user2 = {'first': 'misko', 'last': 'Hevery'}; - - context['user'] = user1; + var staticFieldGetterFactory = new StaticFieldGetterFactory({ + 'methodA': (o) => o.methodA, + 'toUpperCase': (o) => o.toUpperCase, + 'toLowerCase': (o) => o.toLowerCase, + 'count': (o) => o.count, + }); - // should fire on initial adding - expect(watchGrp.fieldCost).toEqual(0); - var watch = watchGrp.watch(parse('user'), (v, p) => logger(v)); - var watchFirst = watchGrp.watch(parse('user.first'), (v, p) => logger(v)); - var watchLast = watchGrp.watch(parse('user.last'), (v, p) => logger(v)); - expect(watchGrp.fieldCost).toEqual(3); + they(FieldGetterFactory fieldGetterFactory, String modeName) { + describe('WatchGroup-$modeName', () { + var context; + var watchGrp; + DirtyCheckingChangeDetector changeDetector; + Logger logger; + + AST parse(String expression) { + var currentAST = new ContextReferenceAST(); + expression.split('.').forEach((name) { + currentAST = new FieldReadAST(currentAST, name); + }); + return currentAST; + } - watchGrp.detectChanges(); - expect(logger).toEqual([user1, 'misko', 'hevery']); + expectOrder(list) { + logger.clear(); + watchGrp.detectChanges(); // Clear the initial queue logger.clear(); - - context['user'] = user2; watchGrp.detectChanges(); - expect(logger).toEqual([user2, 'Hevery']); + expect(logger).toEqual(list); + } + + beforeEach(inject((Logger _logger) { + context = {}; + changeDetector = new DirtyCheckingChangeDetector(fieldGetterFactory); + watchGrp = new RootWatchGroup(fieldGetterFactory, changeDetector, context); + logger = _logger; + })); + + describe('watch lifecycle', () { + it('should prevent reaction fn on removed', () { + context['a'] = 'hello'; + var watch ; + watchGrp.watch(parse('a'), (v, p) { + logger('removed'); + watch.remove(); + }); + watch = watchGrp.watch(parse('a'), (v, p) => logger(v)); + watchGrp.detectChanges(); + expect(logger).toEqual(['removed']); + }); + }); + describe('property chaining', () { + it('should read property', () { + context['a'] = 'hello'; - watch.remove(); - expect(watchGrp.fieldCost).toEqual(3); + // should fire on initial adding + expect(watchGrp.fieldCost).toEqual(0); + var watch = watchGrp.watch(parse('a'), (v, p) => logger(v)); + expect(watch.expression).toEqual('a'); + expect(watchGrp.fieldCost).toEqual(1); + watchGrp.detectChanges(); + expect(logger).toEqual(['hello']); - watchFirst.remove(); - expect(watchGrp.fieldCost).toEqual(2); + // make sore no new changes are logged on extra detectChanges + watchGrp.detectChanges(); + expect(logger).toEqual(['hello']); - watchLast.remove(); - expect(watchGrp.fieldCost).toEqual(0); + // Should detect value change + context['a'] = 'bye'; + watchGrp.detectChanges(); + expect(logger).toEqual(['hello', 'bye']); - expect(() => watch.remove()).toThrow('Already deleted!'); - }); + // should cleanup after itself + watch.remove(); + expect(watchGrp.fieldCost).toEqual(0); + context['a'] = 'cant see me'; + watchGrp.detectChanges(); + expect(logger).toEqual(['hello', 'bye']); + }); - it('should eval pure FunctionApply', () { - context['a'] = {'val': 1}; + it('should read property chain', () { + context['a'] = {'b': 'hello'}; + + // should fire on initial adding + expect(watchGrp.fieldCost).toEqual(0); + expect(changeDetector.count).toEqual(0); + var watch = watchGrp.watch(parse('a.b'), (v, p) => logger(v)); + expect(watch.expression).toEqual('a.b'); + expect(watchGrp.fieldCost).toEqual(2); + expect(changeDetector.count).toEqual(2); + watchGrp.detectChanges(); + expect(logger).toEqual(['hello']); + + // make sore no new changes are logged on extra detectChanges + watchGrp.detectChanges(); + expect(logger).toEqual(['hello']); + + // make sure no changes or logged when intermediary object changes + context['a'] = {'b': 'hello'}; + watchGrp.detectChanges(); + expect(logger).toEqual(['hello']); + + // Should detect value change + context['a'] = {'b': 'hello2'}; + watchGrp.detectChanges(); + expect(logger).toEqual(['hello', 'hello2']); + + // Should detect value change + context['a']['b'] = 'bye'; + watchGrp.detectChanges(); + expect(logger).toEqual(['hello', 'hello2', 'bye']); + + // should cleanup after itself + watch.remove(); + expect(watchGrp.fieldCost).toEqual(0); + context['a']['b'] = 'cant see me'; + watchGrp.detectChanges(); + expect(logger).toEqual(['hello', 'hello2', 'bye']); + }); - FunctionApply fn = new LoggingFunctionApply(logger); - var watch = watchGrp.watch( - new PureFunctionAST('add', fn, [parse('a.val')]), - (v, p) => logger(v) - ); + it('should reuse handlers', () { + var user1 = {'first': 'misko', 'last': 'hevery'}; + var user2 = {'first': 'misko', 'last': 'Hevery'}; - // a; a.val; b; b.val; - expect(watchGrp.fieldCost).toEqual(2); - // add - expect(watchGrp.evalCost).toEqual(1); + context['user'] = user1; - watchGrp.detectChanges(); - expect(logger).toEqual([[1], null]); - logger.clear(); + // should fire on initial adding + expect(watchGrp.fieldCost).toEqual(0); + var watch = watchGrp.watch(parse('user'), (v, p) => logger(v)); + var watchFirst = watchGrp.watch(parse('user.first'), (v, p) => logger(v)); + var watchLast = watchGrp.watch(parse('user.last'), (v, p) => logger(v)); + expect(watchGrp.fieldCost).toEqual(3); - context['a'] = {'val': 2}; - watchGrp.detectChanges(); - expect(logger).toEqual([[2]]); - }); + watchGrp.detectChanges(); + expect(logger).toEqual([user1, 'misko', 'hevery']); + logger.clear(); + context['user'] = user2; + watchGrp.detectChanges(); + expect(logger).toEqual([user2, 'Hevery']); - it('should eval pure function', () { - context['a'] = {'val': 1}; - context['b'] = {'val': 2}; - var watch = watchGrp.watch( - new PureFunctionAST('add', - (a, b) { logger('+'); return a+b; }, - [parse('a.val'), parse('b.val')] - ), - (v, p) => logger(v) - ); + watch.remove(); + expect(watchGrp.fieldCost).toEqual(3); - // a; a.val; b; b.val; - expect(watchGrp.fieldCost).toEqual(4); - // add - expect(watchGrp.evalCost).toEqual(1); + watchFirst.remove(); + expect(watchGrp.fieldCost).toEqual(2); - watchGrp.detectChanges(); - expect(logger).toEqual(['+', 3]); + watchLast.remove(); + expect(watchGrp.fieldCost).toEqual(0); - // extra checks should not trigger functions - watchGrp.detectChanges(); - watchGrp.detectChanges(); - expect(logger).toEqual(['+', 3]); + expect(() => watch.remove()).toThrow('Already deleted!'); + }); - // multiple arg changes should only trigger function once. - context['a']['val'] = 3; - context['b']['val'] = 4; + it('should eval pure FunctionApply', () { + context['a'] = {'val': 1}; - watchGrp.detectChanges(); - expect(logger).toEqual(['+', 3, '+', 7]); + FunctionApply fn = new LoggingFunctionApply(logger); + var watch = watchGrp.watch( + new PureFunctionAST('add', fn, [parse('a.val')]), + (v, p) => logger(v) + ); - watch.remove(); - expect(watchGrp.fieldCost).toEqual(0); - expect(watchGrp.evalCost).toEqual(0); + // a; a.val; b; b.val; + expect(watchGrp.fieldCost).toEqual(2); + // add + expect(watchGrp.evalCost).toEqual(1); - context['a']['val'] = 0; - context['b']['val'] = 0; + watchGrp.detectChanges(); + expect(logger).toEqual([[1], null]); + logger.clear(); - watchGrp.detectChanges(); - expect(logger).toEqual(['+', 3, '+', 7]); - }); + context['a'] = {'val': 2}; + watchGrp.detectChanges(); + expect(logger).toEqual([[2]]); + }); - it('should eval chained pure function', () { - context['a'] = {'val': 1}; - context['b'] = {'val': 2}; - context['c'] = {'val': 3}; + it('should eval pure function', () { + context['a'] = {'val': 1}; + context['b'] = {'val': 2}; - var a_plus_b = new PureFunctionAST('add1', - (a, b) { logger('$a+$b'); return a + b; }, - [parse('a.val'), parse('b.val')]); + var watch = watchGrp.watch( + new PureFunctionAST('add', + (a, b) { logger('+'); return a+b; }, + [parse('a.val'), parse('b.val')] + ), + (v, p) => logger(v) + ); - var a_plus_b_plus_c = new PureFunctionAST('add2', - (b, c) { logger('$b+$c'); return b + c; }, - [a_plus_b, parse('c.val')]); + // a; a.val; b; b.val; + expect(watchGrp.fieldCost).toEqual(4); + // add + expect(watchGrp.evalCost).toEqual(1); - var watch = watchGrp.watch(a_plus_b_plus_c, (v, p) => logger(v)); + watchGrp.detectChanges(); + expect(logger).toEqual(['+', 3]); - // a; a.val; b; b.val; c; c.val; - expect(watchGrp.fieldCost).toEqual(6); - // add - expect(watchGrp.evalCost).toEqual(2); + // extra checks should not trigger functions + watchGrp.detectChanges(); + watchGrp.detectChanges(); + expect(logger).toEqual(['+', 3]); - watchGrp.detectChanges(); - expect(logger).toEqual(['1+2', '3+3', 6]); - logger.clear(); + // multiple arg changes should only trigger function once. + context['a']['val'] = 3; + context['b']['val'] = 4; - // extra checks should not trigger functions - watchGrp.detectChanges(); - watchGrp.detectChanges(); - expect(logger).toEqual([]); - logger.clear(); + watchGrp.detectChanges(); + expect(logger).toEqual(['+', 3, '+', 7]); - // multiple arg changes should only trigger function once. - context['a']['val'] = 3; - context['b']['val'] = 4; - context['c']['val'] = 5; - watchGrp.detectChanges(); - expect(logger).toEqual(['3+4', '7+5', 12]); - logger.clear(); + watch.remove(); + expect(watchGrp.fieldCost).toEqual(0); + expect(watchGrp.evalCost).toEqual(0); - context['a']['val'] = 9; - watchGrp.detectChanges(); - expect(logger).toEqual(['9+4', '13+5', 18]); - logger.clear(); + context['a']['val'] = 0; + context['b']['val'] = 0; - context['c']['val'] = 9; - watchGrp.detectChanges(); - expect(logger).toEqual(['13+9', 22]); - logger.clear(); + watchGrp.detectChanges(); + expect(logger).toEqual(['+', 3, '+', 7]); + }); - watch.remove(); - expect(watchGrp.fieldCost).toEqual(0); - expect(watchGrp.evalCost).toEqual(0); + it('should eval chained pure function', () { + context['a'] = {'val': 1}; + context['b'] = {'val': 2}; + context['c'] = {'val': 3}; - context['a']['val'] = 0; - context['b']['val'] = 0; + var a_plus_b = new PureFunctionAST('add1', + (a, b) { logger('$a+$b'); return a + b; }, + [parse('a.val'), parse('b.val')]); - watchGrp.detectChanges(); - expect(logger).toEqual([]); - }); + var a_plus_b_plus_c = new PureFunctionAST('add2', + (b, c) { logger('$b+$c'); return b + c; }, + [a_plus_b, parse('c.val')]); + var watch = watchGrp.watch(a_plus_b_plus_c, (v, p) => logger(v)); - it('should eval closure', () { - var obj; - obj = { - 'methodA': (arg1) { - logger('methodA($arg1) => ${obj['valA']}'); - return obj['valA']; - }, - 'valA': 'A' - }; - context['obj'] = obj; - context['arg0'] = 1; - - var watch = watchGrp.watch( - new MethodAST(parse('obj'), 'methodA', [parse('arg0')]), - (v, p) => logger(v) - ); - - // obj, arg0; - expect(watchGrp.fieldCost).toEqual(2); - // methodA() - expect(watchGrp.evalCost).toEqual(1); + // a; a.val; b; b.val; c; c.val; + expect(watchGrp.fieldCost).toEqual(6); + // add + expect(watchGrp.evalCost).toEqual(2); - watchGrp.detectChanges(); - expect(logger).toEqual(['methodA(1) => A', 'A']); - logger.clear(); + watchGrp.detectChanges(); + expect(logger).toEqual(['1+2', '3+3', 6]); + logger.clear(); - watchGrp.detectChanges(); - watchGrp.detectChanges(); - expect(logger).toEqual(['methodA(1) => A', 'methodA(1) => A']); - logger.clear(); + // extra checks should not trigger functions + watchGrp.detectChanges(); + watchGrp.detectChanges(); + expect(logger).toEqual([]); + logger.clear(); - obj['valA'] = 'B'; - context['arg0'] = 2; + // multiple arg changes should only trigger function once. + context['a']['val'] = 3; + context['b']['val'] = 4; + context['c']['val'] = 5; + watchGrp.detectChanges(); + expect(logger).toEqual(['3+4', '7+5', 12]); + logger.clear(); - watchGrp.detectChanges(); - expect(logger).toEqual(['methodA(2) => B', 'B']); - logger.clear(); + context['a']['val'] = 9; + watchGrp.detectChanges(); + expect(logger).toEqual(['9+4', '13+5', 18]); + logger.clear(); - watch.remove(); - expect(watchGrp.fieldCost).toEqual(0); - expect(watchGrp.evalCost).toEqual(0); + context['c']['val'] = 9; + watchGrp.detectChanges(); + expect(logger).toEqual(['13+9', 22]); + logger.clear(); - obj['valA'] = 'C'; - context['arg0'] = 3; - watchGrp.detectChanges(); - expect(logger).toEqual([]); - }); + watch.remove(); + expect(watchGrp.fieldCost).toEqual(0); + expect(watchGrp.evalCost).toEqual(0); + context['a']['val'] = 0; + context['b']['val'] = 0; - it('should eval method', () { - var obj = new MyClass(logger); - obj.valA = 'A'; - context['obj'] = obj; - context['arg0'] = 1; + watchGrp.detectChanges(); + expect(logger).toEqual([]); + }); - var watch = watchGrp.watch( - new MethodAST(parse('obj'), 'methodA', [parse('arg0')]), - (v, p) => logger(v) - ); - // obj, arg0; - expect(watchGrp.fieldCost).toEqual(2); - // methodA() - expect(watchGrp.evalCost).toEqual(1); + it('should eval closure', () { + var obj; + obj = { + 'methodA': (arg1) { + logger('methodA($arg1) => ${obj['valA']}'); + return obj['valA']; + }, + 'valA': 'A' + }; + context['obj'] = obj; + context['arg0'] = 1; + + var watch = watchGrp.watch( + new MethodAST(parse('obj'), 'methodA', [parse('arg0')]), + (v, p) => logger(v) + ); + + // obj, arg0; + expect(watchGrp.fieldCost).toEqual(2); + // methodA() + expect(watchGrp.evalCost).toEqual(1); + + watchGrp.detectChanges(); + expect(logger).toEqual(['methodA(1) => A', 'A']); + logger.clear(); + + watchGrp.detectChanges(); + watchGrp.detectChanges(); + expect(logger).toEqual(['methodA(1) => A', 'methodA(1) => A']); + logger.clear(); + + obj['valA'] = 'B'; + context['arg0'] = 2; + + watchGrp.detectChanges(); + expect(logger).toEqual(['methodA(2) => B', 'B']); + logger.clear(); - watchGrp.detectChanges(); - expect(logger).toEqual(['methodA(1) => A', 'A']); - logger.clear(); + watch.remove(); + expect(watchGrp.fieldCost).toEqual(0); + expect(watchGrp.evalCost).toEqual(0); - watchGrp.detectChanges(); - watchGrp.detectChanges(); - expect(logger).toEqual(['methodA(1) => A', 'methodA(1) => A']); - logger.clear(); + obj['valA'] = 'C'; + context['arg0'] = 3; - obj.valA = 'B'; - context['arg0'] = 2; + watchGrp.detectChanges(); + expect(logger).toEqual([]); + }); - watchGrp.detectChanges(); - expect(logger).toEqual(['methodA(2) => B', 'B']); - logger.clear(); - watch.remove(); - expect(watchGrp.fieldCost).toEqual(0); - expect(watchGrp.evalCost).toEqual(0); + it('should eval method', () { + var obj = new MyClass(logger); + obj.valA = 'A'; + context['obj'] = obj; + context['arg0'] = 1; - obj.valA = 'C'; - context['arg0'] = 3; + var watch = watchGrp.watch( + new MethodAST(parse('obj'), 'methodA', [parse('arg0')]), + (v, p) => logger(v) + ); - watchGrp.detectChanges(); - expect(logger).toEqual([]); - }); + // obj, arg0; + expect(watchGrp.fieldCost).toEqual(2); + // methodA() + expect(watchGrp.evalCost).toEqual(1); - it('should eval method chain', () { - var obj1 = new MyClass(logger); - var obj2 = new MyClass(logger); - obj1.valA = obj2; - obj2.valA = 'A'; - context['obj'] = obj1; - context['arg0'] = 0; - context['arg1'] = 1; - - // obj.methodA(arg0) - var ast = new MethodAST(parse('obj'), 'methodA', [parse('arg0')]); - ast = new MethodAST(ast, 'methodA', [parse('arg1')]); - var watch = watchGrp.watch(ast, (v, p) => logger(v)); - - // obj, arg0, arg1; - expect(watchGrp.fieldCost).toEqual(3); - // methodA(), methodA() - expect(watchGrp.evalCost).toEqual(2); + watchGrp.detectChanges(); + expect(logger).toEqual(['methodA(1) => A', 'A']); + logger.clear(); - watchGrp.detectChanges(); - expect(logger).toEqual(['methodA(0) => MyClass', 'methodA(1) => A', 'A']); - logger.clear(); + watchGrp.detectChanges(); + watchGrp.detectChanges(); + expect(logger).toEqual(['methodA(1) => A', 'methodA(1) => A']); + logger.clear(); - watchGrp.detectChanges(); - watchGrp.detectChanges(); - expect(logger).toEqual(['methodA(0) => MyClass', 'methodA(1) => A', - 'methodA(0) => MyClass', 'methodA(1) => A']); - logger.clear(); + obj.valA = 'B'; + context['arg0'] = 2; - obj2.valA = 'B'; - context['arg0'] = 10; - context['arg1'] = 11; + watchGrp.detectChanges(); + expect(logger).toEqual(['methodA(2) => B', 'B']); + logger.clear(); - watchGrp.detectChanges(); - expect(logger).toEqual(['methodA(10) => MyClass', 'methodA(11) => B', 'B']); - logger.clear(); + watch.remove(); + expect(watchGrp.fieldCost).toEqual(0); + expect(watchGrp.evalCost).toEqual(0); - watch.remove(); - expect(watchGrp.fieldCost).toEqual(0); - expect(watchGrp.evalCost).toEqual(0); + obj.valA = 'C'; + context['arg0'] = 3; - obj2.valA = 'C'; - context['arg0'] = 20; - context['arg1'] = 21; + watchGrp.detectChanges(); + expect(logger).toEqual([]); + }); - watchGrp.detectChanges(); - expect(logger).toEqual([]); - }); + it('should eval method chain', () { + var obj1 = new MyClass(logger); + var obj2 = new MyClass(logger); + obj1.valA = obj2; + obj2.valA = 'A'; + context['obj'] = obj1; + context['arg0'] = 0; + context['arg1'] = 1; + + // obj.methodA(arg0) + var ast = new MethodAST(parse('obj'), 'methodA', [parse('arg0')]); + ast = new MethodAST(ast, 'methodA', [parse('arg1')]); + var watch = watchGrp.watch(ast, (v, p) => logger(v)); + + // obj, arg0, arg1; + expect(watchGrp.fieldCost).toEqual(3); + // methodA(), methodA() + expect(watchGrp.evalCost).toEqual(2); + + watchGrp.detectChanges(); + expect(logger).toEqual(['methodA(0) => MyClass', 'methodA(1) => A', 'A']); + logger.clear(); + + watchGrp.detectChanges(); + watchGrp.detectChanges(); + expect(logger).toEqual(['methodA(0) => MyClass', 'methodA(1) => A', + 'methodA(0) => MyClass', 'methodA(1) => A']); + logger.clear(); + + obj2.valA = 'B'; + context['arg0'] = 10; + context['arg1'] = 11; + + watchGrp.detectChanges(); + expect(logger).toEqual(['methodA(10) => MyClass', 'methodA(11) => B', 'B']); + logger.clear(); - it('should not return null when evaling method first time', () { - context['text'] ='abc'; - var ast = new MethodAST(parse('text'), 'toUpperCase', []); - var watch = watchGrp.watch(ast, (v, p) => logger(v)); + watch.remove(); + expect(watchGrp.fieldCost).toEqual(0); + expect(watchGrp.evalCost).toEqual(0); - watchGrp.detectChanges(); - expect(logger).toEqual(['ABC']); - }); + obj2.valA = 'C'; + context['arg0'] = 20; + context['arg1'] = 21; - it('should not eval a function if registered during reaction', () { - context['text'] ='abc'; - var ast = new MethodAST(parse('text'), 'toLowerCase', []); - var watch = watchGrp.watch(ast, (v, p) { - var ast = new MethodAST(parse('text'), 'toUpperCase', []); - watchGrp.watch(ast, (v, p) { - logger(v); - }); + watchGrp.detectChanges(); + expect(logger).toEqual([]); }); - watchGrp.detectChanges(); - watchGrp.detectChanges(); - expect(logger).toEqual(['ABC']); - }); - + it('should not return null when evaling method first time', () { + context['text'] ='abc'; + var ast = new MethodAST(parse('text'), 'toUpperCase', []); + var watch = watchGrp.watch(ast, (v, p) => logger(v)); - it('should eval function eagerly when registered during reaction', () { - var fn = (arg) { logger('fn($arg)'); return arg; }; - context['obj'] = {'fn': fn}; - context['arg1'] = 'OUT'; - context['arg2'] = 'IN'; - var ast = new MethodAST(parse('obj'), 'fn', [parse('arg1')]); - var watch = watchGrp.watch(ast, (v, p) { - var ast = new MethodAST(parse('obj'), 'fn', [parse('arg2')]); - watchGrp.watch(ast, (v, p) { - logger('reaction: $v'); - }); + watchGrp.detectChanges(); + expect(logger).toEqual(['ABC']); }); - expect(logger).toEqual([]); - watchGrp.detectChanges(); - expect(logger).toEqual(['fn(OUT)', 'fn(IN)', 'reaction: IN']); - logger.clear(); - watchGrp.detectChanges(); - expect(logger).toEqual(['fn(OUT)', 'fn(IN)']); - }); - + it('should not eval a function if registered during reaction', () { + context['text'] ='abc'; + var ast = new MethodAST(parse('text'), 'toLowerCase', []); + var watch = watchGrp.watch(ast, (v, p) { + var ast = new MethodAST(parse('text'), 'toUpperCase', []); + watchGrp.watch(ast, (v, p) { + logger(v); + }); + }); - it('should read connstant', () { - // should fire on initial adding - expect(watchGrp.fieldCost).toEqual(0); - var watch = watchGrp.watch(new ConstantAST(123), (v, p) => logger(v)); - expect(watch.expression).toEqual('123'); - expect(watchGrp.fieldCost).toEqual(0); - watchGrp.detectChanges(); - expect(logger).toEqual([123]); + watchGrp.detectChanges(); + watchGrp.detectChanges(); + expect(logger).toEqual(['ABC']); + }); - // make sore no new changes are logged on extra detectChanges - watchGrp.detectChanges(); - expect(logger).toEqual([123]); - }); - it('should wrap iterable in ObservableList', () { - context['list'] = []; - var watch = watchGrp.watch(new CollectionAST(parse('list')), (v, p) => logger(v)); + it('should eval function eagerly when registered during reaction', () { + var fn = (arg) { logger('fn($arg)'); return arg; }; + context['obj'] = {'fn': fn}; + context['arg1'] = 'OUT'; + context['arg2'] = 'IN'; + var ast = new MethodAST(parse('obj'), 'fn', [parse('arg1')]); + var watch = watchGrp.watch(ast, (v, p) { + var ast = new MethodAST(parse('obj'), 'fn', [parse('arg2')]); + watchGrp.watch(ast, (v, p) { + logger('reaction: $v'); + }); + }); - expect(watchGrp.fieldCost).toEqual(1); - expect(watchGrp.collectionCost).toEqual(1); - expect(watchGrp.evalCost).toEqual(0); + expect(logger).toEqual([]); + watchGrp.detectChanges(); + expect(logger).toEqual(['fn(OUT)', 'fn(IN)', 'reaction: IN']); + logger.clear(); + watchGrp.detectChanges(); + expect(logger).toEqual(['fn(OUT)', 'fn(IN)']); + }); - watchGrp.detectChanges(); - expect(logger.length).toEqual(1); - expect(logger[0], toEqualCollectionRecord( - collection: [], - additions: [], - moves: [], - removals: [])); - logger.clear(); - context['list'] = [1]; - watchGrp.detectChanges(); - expect(logger.length).toEqual(1); - expect(logger[0], toEqualCollectionRecord( - collection: ['1[null -> 0]'], - additions: ['1[null -> 0]'], - moves: [], - removals: [])); - logger.clear(); + it('should read connstant', () { + // should fire on initial adding + expect(watchGrp.fieldCost).toEqual(0); + var watch = watchGrp.watch(new ConstantAST(123), (v, p) => logger(v)); + expect(watch.expression).toEqual('123'); + expect(watchGrp.fieldCost).toEqual(0); + watchGrp.detectChanges(); + expect(logger).toEqual([123]); - watch.remove(); - expect(watchGrp.fieldCost).toEqual(0); - expect(watchGrp.collectionCost).toEqual(0); - expect(watchGrp.evalCost).toEqual(0); - }); + // make sore no new changes are logged on extra detectChanges + watchGrp.detectChanges(); + expect(logger).toEqual([123]); + }); - it('should watch literal arrays made of expressions', () { - context['a'] = 1; - var ast = new CollectionAST( - new PureFunctionAST('[a]', new ArrayFn(), [parse('a')]) - ); - var watch = watchGrp.watch(ast, (v, p) => logger(v)); - watchGrp.detectChanges(); - expect(logger[0], toEqualCollectionRecord( - collection: ['1[null -> 0]'], - additions: ['1[null -> 0]'], - moves: [], - removals: [])); - logger.clear(); + it('should wrap iterable in ObservableList', () { + context['list'] = []; + var watch = watchGrp.watch(new CollectionAST(parse('list')), (v, p) => logger(v)); + + expect(watchGrp.fieldCost).toEqual(1); + expect(watchGrp.collectionCost).toEqual(1); + expect(watchGrp.evalCost).toEqual(0); + + watchGrp.detectChanges(); + expect(logger.length).toEqual(1); + expect(logger[0], toEqualCollectionRecord( + collection: [], + additions: [], + moves: [], + removals: [])); + logger.clear(); + + context['list'] = [1]; + watchGrp.detectChanges(); + expect(logger.length).toEqual(1); + expect(logger[0], toEqualCollectionRecord( + collection: ['1[null -> 0]'], + additions: ['1[null -> 0]'], + moves: [], + removals: [])); + logger.clear(); - context['a'] = 2; - watchGrp.detectChanges(); - expect(logger[0], toEqualCollectionRecord( - collection: ['2[null -> 0]'], - additions: ['2[null -> 0]'], - moves: [], - removals: ['1[0 -> null]'])); - logger.clear(); - }); + watch.remove(); + expect(watchGrp.fieldCost).toEqual(0); + expect(watchGrp.collectionCost).toEqual(0); + expect(watchGrp.evalCost).toEqual(0); + }); - it('should watch pure function whose result goes to pure function', () { - context['a'] = 1; - var ast = new PureFunctionAST( - '-', - (v) => -v, - [new PureFunctionAST('++', (v) => v + 1, [parse('a')])] - ); - var watch = watchGrp.watch(ast, (v, p) => logger(v)); - - expect(watchGrp.detectChanges()).not.toBe(null); - expect(logger).toEqual([-2]); - logger.clear(); + it('should watch literal arrays made of expressions', () { + context['a'] = 1; + var ast = new CollectionAST( + new PureFunctionAST('[a]', new ArrayFn(), [parse('a')]) + ); + var watch = watchGrp.watch(ast, (v, p) => logger(v)); + watchGrp.detectChanges(); + expect(logger[0], toEqualCollectionRecord( + collection: ['1[null -> 0]'], + additions: ['1[null -> 0]'], + moves: [], + removals: [])); + logger.clear(); + + context['a'] = 2; + watchGrp.detectChanges(); + expect(logger[0], toEqualCollectionRecord( + collection: ['2[null -> 0]'], + additions: ['2[null -> 0]'], + moves: [], + removals: ['1[0 -> null]'])); + logger.clear(); + }); - context['a'] = 2; - expect(watchGrp.detectChanges()).not.toBe(null); - expect(logger).toEqual([-3]); + it('should watch pure function whose result goes to pure function', () { + context['a'] = 1; + var ast = new PureFunctionAST( + '-', + (v) => -v, + [new PureFunctionAST('++', (v) => v + 1, [parse('a')])] + ); + var watch = watchGrp.watch(ast, (v, p) => logger(v)); + + expect(watchGrp.detectChanges()).not.toBe(null); + expect(logger).toEqual([-2]); + logger.clear(); + + context['a'] = 2; + expect(watchGrp.detectChanges()).not.toBe(null); + expect(logger).toEqual([-3]); + }); }); - }); - - describe('child group', () { - it('should remove all field watches in group and group\'s children', () { - watchGrp.watch(parse('a'), (v, p) => logger('0a')); - var child1a = watchGrp.newGroup(new PrototypeMap(context)); - var child1b = watchGrp.newGroup(new PrototypeMap(context)); - var child2 = child1a.newGroup(new PrototypeMap(context)); - child1a.watch(parse('a'), (v, p) => logger('1a')); - child1b.watch(parse('a'), (v, p) => logger('1b')); - watchGrp.watch(parse('a'), (v, p) => logger('0A')); - child1a.watch(parse('a'), (v, p) => logger('1A')); - child2.watch(parse('a'), (v, p) => logger('2A')); - - // flush initial reaction functions - expect(watchGrp.detectChanges()).toEqual(6); - // expect(logger).toEqual(['0a', '0A', '1a', '1A', '2A', '1b']); - expect(logger).toEqual(['0a', '1a', '1b', '0A', '1A', '2A']); // we go by registration order - expect(watchGrp.fieldCost).toEqual(1); - expect(watchGrp.totalFieldCost).toEqual(4); - logger.clear(); - context['a'] = 1; - expect(watchGrp.detectChanges()).toEqual(6); - expect(logger).toEqual(['0a', '0A', '1a', '1A', '2A', '1b']); // we go by group order - logger.clear(); - - context['a'] = 2; - child1a.remove(); // should also remove child2 - expect(watchGrp.detectChanges()).toEqual(3); - expect(logger).toEqual(['0a', '0A', '1b']); - expect(watchGrp.fieldCost).toEqual(1); - expect(watchGrp.totalFieldCost).toEqual(2); - }); + describe('child group', () { + it('should remove all field watches in group and group\'s children', () { + watchGrp.watch(parse('a'), (v, p) => logger('0a')); + var child1a = watchGrp.newGroup(new PrototypeMap(context)); + var child1b = watchGrp.newGroup(new PrototypeMap(context)); + var child2 = child1a.newGroup(new PrototypeMap(context)); + child1a.watch(parse('a'), (v, p) => logger('1a')); + child1b.watch(parse('a'), (v, p) => logger('1b')); + watchGrp.watch(parse('a'), (v, p) => logger('0A')); + child1a.watch(parse('a'), (v, p) => logger('1A')); + child2.watch(parse('a'), (v, p) => logger('2A')); + + // flush initial reaction functions + expect(watchGrp.detectChanges()).toEqual(6); + // expect(logger).toEqual(['0a', '0A', '1a', '1A', '2A', '1b']); + expect(logger).toEqual(['0a', '1a', '1b', '0A', '1A', '2A']); // we go by registration order + expect(watchGrp.fieldCost).toEqual(1); + expect(watchGrp.totalFieldCost).toEqual(4); + logger.clear(); + + context['a'] = 1; + expect(watchGrp.detectChanges()).toEqual(6); + expect(logger).toEqual(['0a', '0A', '1a', '1A', '2A', '1b']); // we go by group order + logger.clear(); + + context['a'] = 2; + child1a.remove(); // should also remove child2 + expect(watchGrp.detectChanges()).toEqual(3); + expect(logger).toEqual(['0a', '0A', '1b']); + expect(watchGrp.fieldCost).toEqual(1); + expect(watchGrp.totalFieldCost).toEqual(2); + }); - it('should remove all method watches in group and group\'s children', () { - context['my'] = new MyClass(logger); - AST countMethod = new MethodAST(parse('my'), 'count', []); - watchGrp.watch(countMethod, (v, p) => logger('0a')); - expectOrder(['0a']); - - var child1a = watchGrp.newGroup(new PrototypeMap(context)); - var child1b = watchGrp.newGroup(new PrototypeMap(context)); - var child2 = child1a.newGroup(new PrototypeMap(context)); - child1a.watch(countMethod, (v, p) => logger('1a')); - expectOrder(['0a', '1a']); - child1b.watch(countMethod, (v, p) => logger('1b')); - expectOrder(['0a', '1a', '1b']); - watchGrp.watch(countMethod, (v, p) => logger('0A')); - expectOrder(['0a', '0A', '1a', '1b']); - child1a.watch(countMethod, (v, p) => logger('1A')); - expectOrder(['0a', '0A', '1a', '1A', '1b']); - child2.watch(countMethod, (v, p) => logger('2A')); - expectOrder(['0a', '0A', '1a', '1A', '2A', '1b']); - - // flush initial reaction functions - expect(watchGrp.detectChanges()).toEqual(6); - expectOrder(['0a', '0A', '1a', '1A', '2A', '1b']); - - child1a.remove(); // should also remove child2 - expect(watchGrp.detectChanges()).toEqual(3); - expectOrder(['0a', '0A', '1b']); - }); + it('should remove all method watches in group and group\'s children', () { + context['my'] = new MyClass(logger); + AST countMethod = new MethodAST(parse('my'), 'count', []); + watchGrp.watch(countMethod, (v, p) => logger('0a')); + expectOrder(['0a']); + + var child1a = watchGrp.newGroup(new PrototypeMap(context)); + var child1b = watchGrp.newGroup(new PrototypeMap(context)); + var child2 = child1a.newGroup(new PrototypeMap(context)); + child1a.watch(countMethod, (v, p) => logger('1a')); + expectOrder(['0a', '1a']); + child1b.watch(countMethod, (v, p) => logger('1b')); + expectOrder(['0a', '1a', '1b']); + watchGrp.watch(countMethod, (v, p) => logger('0A')); + expectOrder(['0a', '0A', '1a', '1b']); + child1a.watch(countMethod, (v, p) => logger('1A')); + expectOrder(['0a', '0A', '1a', '1A', '1b']); + child2.watch(countMethod, (v, p) => logger('2A')); + expectOrder(['0a', '0A', '1a', '1A', '2A', '1b']); + + // flush initial reaction functions + expect(watchGrp.detectChanges()).toEqual(6); + expectOrder(['0a', '0A', '1a', '1A', '2A', '1b']); + + child1a.remove(); // should also remove child2 + expect(watchGrp.detectChanges()).toEqual(3); + expectOrder(['0a', '0A', '1b']); + }); - it('should add watches within its own group', () { - context['my'] = new MyClass(logger); - AST countMethod = new MethodAST(parse('my'), 'count', []); - var ra = watchGrp.watch(countMethod, (v, p) => logger('a')); - var child = watchGrp.newGroup(new PrototypeMap(context)); - var cb = child.watch(countMethod, (v, p) => logger('b')); + it('should add watches within its own group', () { + context['my'] = new MyClass(logger); + AST countMethod = new MethodAST(parse('my'), 'count', []); + var ra = watchGrp.watch(countMethod, (v, p) => logger('a')); + var child = watchGrp.newGroup(new PrototypeMap(context)); + var cb = child.watch(countMethod, (v, p) => logger('b')); - expectOrder(['a', 'b']); - expectOrder(['a', 'b']); + expectOrder(['a', 'b']); + expectOrder(['a', 'b']); - ra.remove(); - expectOrder(['b']); + ra.remove(); + expectOrder(['b']); - cb.remove(); - expectOrder([]); + cb.remove(); + expectOrder([]); - // TODO: add them back in wrong order, assert events in right order - cb = child.watch(countMethod, (v, p) => logger('b')); - ra = watchGrp.watch(countMethod, (v, p) => logger('a'));; - expectOrder(['a', 'b']); - }); + // TODO: add them back in wrong order, assert events in right order + cb = child.watch(countMethod, (v, p) => logger('b')); + ra = watchGrp.watch(countMethod, (v, p) => logger('a'));; + expectOrder(['a', 'b']); + }); - it('should not call reaction function on removed group', () { - var log = []; - context['name'] = 'misko'; - var child = watchGrp.newGroup(context); - watchGrp.watch(parse('name'), (v, _) { - log.add('root $v'); - if (v == 'destroy') { - child.remove(); - } + it('should not call reaction function on removed group', () { + var log = []; + context['name'] = 'misko'; + var child = watchGrp.newGroup(context); + watchGrp.watch(parse('name'), (v, _) { + log.add('root $v'); + if (v == 'destroy') { + child.remove(); + } + }); + child.watch(parse('name'), (v, _) => log.add('child $v')); + watchGrp.detectChanges(); + expect(log).toEqual(['root misko', 'child misko']); + log.clear(); + + context['name'] = 'destroy'; + watchGrp.detectChanges(); + expect(log).toEqual(['root destroy']); }); - child.watch(parse('name'), (v, _) => log.add('child $v')); - watchGrp.detectChanges(); - expect(log).toEqual(['root misko', 'child misko']); - log.clear(); - context['name'] = 'destroy'; - watchGrp.detectChanges(); - expect(log).toEqual(['root destroy']); - }); + it('should watch children', () { + var childContext = new PrototypeMap(context); + context['a'] = 'OK'; + context['b'] = 'BAD'; + childContext['b'] = 'OK'; + watchGrp.watch(parse('a'), (v, p) => logger(v)); + watchGrp.newGroup(childContext).watch(parse('b'), (v, p) => logger(v)); - it('should watch children', () { - var childContext = new PrototypeMap(context); - context['a'] = 'OK'; - context['b'] = 'BAD'; - childContext['b'] = 'OK'; - watchGrp.watch(parse('a'), (v, p) => logger(v)); - watchGrp.newGroup(childContext).watch(parse('b'), (v, p) => logger(v)); + watchGrp.detectChanges(); + expect(logger).toEqual(['OK', 'OK']); + logger.clear(); - watchGrp.detectChanges(); - expect(logger).toEqual(['OK', 'OK']); - logger.clear(); + context['a'] = 'A'; + childContext['b'] = 'B'; - context['a'] = 'A'; - childContext['b'] = 'B'; - - watchGrp.detectChanges(); - expect(logger).toEqual(['A', 'B']); - logger.clear(); + watchGrp.detectChanges(); + expect(logger).toEqual(['A', 'B']); + logger.clear(); + }); }); }); - - }); + } + they(new DynamicFieldGetterFactory(), 'dynamic'); + they(staticFieldGetterFactory, 'static'); } class MyClass { diff --git a/test/core/core_directive_spec.dart b/test/core/core_directive_spec.dart index fc9dd5a9c..1ab08c9d2 100644 --- a/test/core/core_directive_spec.dart +++ b/test/core/core_directive_spec.dart @@ -1,6 +1,7 @@ library core_directive_spec; import '../_specs.dart'; +import 'package:angular/angular_dynamic.dart'; void main() { describe('DirectiveMap', () { @@ -43,15 +44,14 @@ void main() { baseModule = new Module() ..type(DirectiveMap) ..type(DirectiveSelectorFactory) - ..type(MetadataExtractor) - ..type(FieldMetadataExtractor); + ..type(MetadataExtractor); }); it('should throw when annotation is for existing mapping', () { var module = new Module() ..type(Bad1Component); - var injector = new DynamicInjector(modules: [baseModule, module]); + var injector = new NgDynamicApp().addModule(module).createInjector(); expect(() { injector.get(DirectiveMap); }).toThrow('Mapping for attribute foo is already defined (while ' @@ -62,7 +62,7 @@ void main() { var module = new Module() ..type(Bad2Component); - var injector = new DynamicInjector(modules: [baseModule, module]); + var injector = new NgDynamicApp().addModule(module).createInjector(); expect(() { injector.get(DirectiveMap); }).toThrow('Attribute annotation for foo is defined more than once ' diff --git a/test/core/registry_spec.dart b/test/core/registry_spec.dart index 1c9c6070b..2facdefe1 100644 --- a/test/core/registry_spec.dart +++ b/test/core/registry_spec.dart @@ -1,17 +1,17 @@ library registry_spec; import '../_specs.dart'; +import 'package:angular/angular_dynamic.dart'; main() { describe('RegistryMap', () { it('should allow for multiple registry keys to be added', () { var module = new Module() ..type(MyMap) - ..type(MetadataExtractor) ..type(A1) ..type(A2); - var injector = new DynamicInjector(modules: [module]); + var injector = new NgDynamicApp().addModule(module).createInjector(); expect(() { injector.get(MyMap); }).not.toThrow(); @@ -20,10 +20,9 @@ main() { it('should iterate over all types', () { var module = new Module() ..type(MyMap) - ..type(MetadataExtractor) ..type(A1); - var injector = new DynamicInjector(modules: [module]); + var injector = new NgDynamicApp().addModule(module).createInjector(); var keys = []; var types = []; var map = injector.get(MyMap); @@ -35,10 +34,9 @@ main() { it('should safely ignore typedefs', () { var module = new Module() ..type(MyMap) - ..type(MetadataExtractor) ..value(MyTypedef, (String _) => null); - var injector = new DynamicInjector(modules: [module]); + var injector = new NgDynamicApp().addModule(module).createInjector(); expect(() => injector.get(MyMap), isNot(throws)); }); }); diff --git a/test/core/scope_spec.dart b/test/core/scope_spec.dart index c2d2259bb..f5c841dd9 100644 --- a/test/core/scope_spec.dart +++ b/test/core/scope_spec.dart @@ -11,7 +11,6 @@ void main() { beforeEach(module((Module module) { Map context = {}; module - ..value(GetterCache, new GetterCache({})) ..type(ChangeDetector, implementedBy: DirtyCheckingChangeDetector) ..value(Object, context) ..value(Map, context) diff --git a/test/core_dom/block_spec.dart b/test/core_dom/block_spec.dart index fefa1f299..f6c7bc0ee 100644 --- a/test/core_dom/block_spec.dart +++ b/test/core_dom/block_spec.dart @@ -1,6 +1,7 @@ library block_spec; import '../_specs.dart'; +import 'package:angular/angular_dynamic.dart'; class Log { List log = []; @@ -193,8 +194,9 @@ main() { ..type(AFilter) ..type(ADirective); - Injector rootInjector = - new DynamicInjector(modules: [new AngularModule(), rootModule]); + Injector rootInjector = new NgDynamicApp() + .addModule(rootModule) + .createInjector(); Log log = rootInjector.get(Log); Scope rootScope = rootInjector.get(Scope); diff --git a/test/introspection_spec.dart b/test/introspection_spec.dart index d30c9b139..8fe9cf206 100644 --- a/test/introspection_spec.dart +++ b/test/introspection_spec.dart @@ -2,6 +2,7 @@ library introspection_spec; import '_specs.dart'; import 'dart:js' as js; +import 'package:angular/angular_dynamic.dart'; void main() { describe('introspection', () { @@ -43,7 +44,7 @@ void main() { var elt = $('
')[0]; // Make it possible to find the element from JS document.body.append(elt); - ngBootstrap(element: elt); + (new NgDynamicApp()..element = elt).run(); expect(js.context['ngProbe']).toBeDefined(); expect(js.context['ngScope']).toBeDefined(); diff --git a/test/routing/routing_spec.dart b/test/routing/routing_spec.dart index 933cc1ca1..2a82adf7e 100644 --- a/test/routing/routing_spec.dart +++ b/test/routing/routing_spec.dart @@ -2,6 +2,7 @@ library routing_spec; import '../_specs.dart'; import 'package:angular/mock/module.dart'; +import 'package:angular/angular_dynamic.dart'; import 'dart:async'; main() { @@ -44,10 +45,10 @@ main() { }); initRouter(initializer) { - var module = new Module() - ..value(RouteInitializerFn, initializer); - var injector = new DynamicInjector( - modules: [new AngularModule(), new AngularMockModule(), module]); + var injector = new NgDynamicApp() + .addModule(new AngularMockModule()) + .addModule(new Module()..value(RouteInitializerFn, initializer)) + .createInjector(); injector.get(NgRoutingHelper); // force routing initialization router = injector.get(Router); _ = injector.get(TestBed); From 64f90fb2367000e13108328be3f7d7684ed47780 Mon Sep 17 00:00:00 2001 From: Pete Blois Date: Mon, 10 Mar 2014 09:14:40 -0700 Subject: [PATCH 2/4] First step of integrating transformers to angular. --- example/pubspec.lock | 23 +- example/pubspec.yaml | 9 + example/web/todo.dart | 17 +- lib/auto_modules.dart | 64 +++ .../transformer/expression_generator.dart | 196 +++++++ lib/tools/transformer/metadata_extractor.dart | 340 ++++++++++++ lib/tools/transformer/metadata_generator.dart | 154 ++++++ lib/tools/transformer/options.dart | 58 ++ lib/transformer.dart | 129 +++++ pubspec.lock | 23 +- pubspec.yaml | 7 +- .../expression_extractor_spec.dart | 81 +++ .../transformer/metadata_generator_spec.dart | 504 ++++++++++++++++++ 13 files changed, 1586 insertions(+), 19 deletions(-) create mode 100644 lib/auto_modules.dart create mode 100644 lib/tools/transformer/expression_generator.dart create mode 100644 lib/tools/transformer/metadata_extractor.dart create mode 100644 lib/tools/transformer/metadata_generator.dart create mode 100644 lib/tools/transformer/options.dart create mode 100644 lib/transformer.dart create mode 100644 test/tools/transformer/expression_extractor_spec.dart create mode 100644 test/tools/transformer/metadata_generator_spec.dart diff --git a/example/pubspec.lock b/example/pubspec.lock index 08762dadd..6d13f3cd4 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -4,7 +4,7 @@ packages: analyzer: description: analyzer source: hosted - version: "0.11.10" + version: "0.12.2" angular: description: path: ".." @@ -15,17 +15,28 @@ packages: description: args source: hosted version: "0.9.0" + barback: + description: barback + source: hosted + version: "0.11.1" browser: description: browser source: hosted version: "0.9.1" + code_transformers: + description: code_transformers + source: hosted + version: "0.0.1-dev.2" collection: description: collection source: hosted version: "0.9.1" di: - description: di - source: hosted + description: + ref: null + resolved-ref: "88e0d48101517e1d3bc84f9d38d4a1b619db65aa" + url: "https://github.com/angular/di.dart.git" + source: git version: "0.0.33" html5lib: description: html5lib @@ -34,11 +45,15 @@ packages: intl: description: intl source: hosted - version: "0.9.7" + version: "0.8.10+4" logging: description: logging source: hosted version: "0.9.1+1" + meta: + description: meta + source: hosted + version: "0.8.8" path: description: path source: hosted diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 0aff3ae1d..888a5bb11 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -5,3 +5,12 @@ dependencies: path: ../ browser: any unittest: any + +transformers: +- angular: + dart_entry: web/todo.dart + html_files: web/todo.html + +dependency_overrides: + di: + git: https://github.com/angular/di.dart.git diff --git a/example/web/todo.dart b/example/web/todo.dart index c480c9636..cc90015f5 100644 --- a/example/web/todo.dart +++ b/example/web/todo.dart @@ -2,18 +2,12 @@ library todo; import 'package:angular/angular.dart'; import 'package:angular/playback/playback_http.dart'; +import 'package:angular/auto_modules.dart' as auto; +import 'package:di/auto_injector.dart'; import 'todo.dart'; import 'dart:html'; -// This annotation allows Dart to shake away any classes -// not used from Dart code nor listed in another @MirrorsUsed. -// -// If you create classes that are referenced from the Angular -// expressions, you must include a library target in @MirrorsUsed. -@MirrorsUsed(override: '*') -import 'dart:mirrors'; - class Item { String text; bool done; @@ -105,7 +99,9 @@ main() { print(window.location.search); var module = new Module() ..type(TodoController) - ..type(PlaybackHttpBackendConfig); + ..type(PlaybackHttpBackendConfig) + ..install(auto.defaultExpressionModule) + ..install(auto.defaultMetadataModule); // If these is a query in the URL, use the server-backed // TodoController. Otherwise, use the stored-data controller. @@ -128,5 +124,6 @@ main() { module.type(HttpBackend, implementedBy: PlaybackHttpBackend); } - ngBootstrap(module: module); + ngBootstrap(module: module, + injectorFactory: (modules) => defaultInjector(modules: modules)); } diff --git a/lib/auto_modules.dart b/lib/auto_modules.dart new file mode 100644 index 000000000..8b52d4ee9 --- /dev/null +++ b/lib/auto_modules.dart @@ -0,0 +1,64 @@ +/** + * Library to automatically switch between the dynamic injector and a static + * injector created by a pub build task. + * + * ## Step 1: Hook up the build step + * Edit ```pubspec.yaml``` to add the di transformer to the list of + * transformers. + * + * name: transformer_demo + * version: 0.0.1 + * dependencies: + * browser: any + * inject: any + * transformers: + * - angular: + * dart_entry: web/main.dart + * injectableAnnotations: NgInjectableService + * + * It's important to have the ```dart_entry``` entry to indicate the entry + * point of the application. + * + * By default, any classes or constructors annotated with @inject will be + * injected, but additional annotations can be specified with the annotations + * argument. + * + * ## Step 2: Annotate your types + * + * class Engine { + * @inject + * Engine(); + * } + * + * or + * + * @NgInjectableServide // custom annotation provided in pubspec.yaml + * class Car {} + * + * Note that all injectable classes must be in source files in lib/ directories. + * + * ## Step 3: Use the auto injector + * Modify your entry script to use the [defaultAutoInjector] as the injector, + * or alternatively the [defaultInjectorModule]. + * + * This must be done from the file registered as the dart_entry in pubspec.yaml + * as this is the only file which will be modified to include the generated + * injector. + */ +library angular.auto_modules; + +import 'package:di/di.dart'; +import 'package:di/dynamic_injector.dart'; + + +@MirrorsUsed(override: '*') +import 'dart:mirrors' show MirrorsUsed; + +// Empty since the default is the dynamic expression module. +Module get defaultExpressionModule => new Module(); + +// Empty since the default is the dynamic metadata module. +Module get defaultMetadataModule => new Module(); + +// Empty since the default is the template cache module. +Module get defaultTemplateCacheModule => new Module(); diff --git a/lib/tools/transformer/expression_generator.dart b/lib/tools/transformer/expression_generator.dart new file mode 100644 index 000000000..924526ee9 --- /dev/null +++ b/lib/tools/transformer/expression_generator.dart @@ -0,0 +1,196 @@ +library angular.tools.transformer.expression_generator; + +import 'dart:async'; +import 'dart:math' as math; +import 'package:analyzer/src/generated/element.dart'; +import 'package:angular/core/module.dart'; +import 'package:angular/core/parser/parser.dart'; +import 'package:angular/tools/html_extractor.dart'; +import 'package:angular/tools/parser_getter_setter/generator.dart'; +import 'package:angular/tools/source_crawler.dart'; +import 'package:angular/tools/source_metadata_extractor.dart'; +import 'package:angular/tools/transformer/options.dart'; +import 'package:barback/barback.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:di/di.dart'; +import 'package:di/dynamic_injector.dart'; +import 'package:di/transformer/refactor.dart'; +import 'package:path/path.dart' as path; + + +const String _generatedExpressionFilename = 'generated_static_expressions.dart'; + +/** + * Transformer which gathers all expressions from the HTML source files and + * Dart source files of an application and packages them for static evaluation. + * + * This will also modify the main Dart source file to import the generated + * expressions and modify all references to NG_EXPRESSION_MODULE to refer to + * the generated expressions. + */ +class ExpressionGenerator extends ResolverTransformer { + final TransformOptions options; + + ExpressionGenerator(this.options, Resolvers resolvers) { + this.resolvers = resolvers; + } + + Future isPrimary(Asset input) => options.isDartEntry(input.id); + + Future applyResolver(Transform transform, Resolver resolver) { + var asset = transform.primaryInput; + var outputBuffer = new StringBuffer(); + + _writeStaticExpressionHeader(asset.id, outputBuffer); + + var sourceMetadataExtractor = new SourceMetadataExtractor(); + var directives = + sourceMetadataExtractor.gatherDirectiveInfo(null, + new _LibrarySourceCrawler(resolver.libraries)); + + var htmlExtractor = new HtmlExpressionExtractor(directives); + return _getHtmlSources(transform) + .forEach(htmlExtractor.parseHtml) + .then((_) { + var module = new Module() + ..type(Parser, implementedBy: DynamicParser) + ..type(ParserBackend, implementedBy: DartGetterSetterGen); + var injector = + new DynamicInjector(modules: [module], allowImplicitInjection: true); + + injector.get(_ParserGetterSetter).generateParser( + htmlExtractor.expressions.toList(), outputBuffer); + + var outputId = + new AssetId(asset.id.package, 'lib/$_generatedExpressionFilename'); + transform.addOutput( + new Asset.fromString(outputId, outputBuffer.toString())); + + transformIdentifiers(transform, resolver, + identifier: 'angular.auto_modules.defaultExpressionModule', + replacement: 'expressionModule', + importPrefix: 'generated_static_expressions', + importUrl: _generatedExpressionFilename); + }); + } + + /** + * Gets a stream consisting of the contents of all HTML source files to be + * scoured for expressions. + */ + Stream _getHtmlSources(Transform transform) { + var controller = new StreamController(); + if (options.htmlFiles == null) { + controller.close(); + return controller.stream; + } + Future.wait(options.htmlFiles.map((path) { + var htmlId = new AssetId(transform.primaryInput.id.package, path); + return transform.readInputAsString(htmlId); + }).map((future) { + return future.then(controller.add).catchError(controller.addError); + })).then((_) { + controller.close(); + }); + return controller.stream; + } +} + +void _writeStaticExpressionHeader(AssetId id, StringSink sink) { + var libPath = path.withoutExtension(id.path).replaceAll('/', '.'); + sink.write(''' +library ${id.package}.$libPath.generated_expressions; + +import 'package:angular/angular.dart'; +import 'package:angular/core/parser/dynamic_parser.dart' show ClosureMap; + +Module get expressionModule => new Module() + ..value(ClosureMap, new StaticClosureMap()); + +'''); +} + +class _LibrarySourceCrawler implements SourceCrawler { + final List libraries; + _LibrarySourceCrawler(this.libraries); + + void crawl(String entryPoint, CompilationUnitVisitor visitor) { + libraries.expand((lib) => lib.units) + .map((compilationUnitElement) => compilationUnitElement.node) + .forEach(visitor); + } +} + +class _ParserGetterSetter { + final Parser parser; + final ParserBackend backend; + _ParserGetterSetter(this.parser, this.backend); + + generateParser(List exprs, StringSink sink) { + exprs.forEach((expr) { + try { + parser(expr); + } catch (e) { + // Ignore exceptions. + } + }); + + DartGetterSetterGen backend = this.backend; + sink.write(generateClosureMap(backend.properties, backend.calls)); + } + + String generateClosureMap(Set properties, + Map> calls) { + return ''' +class StaticClosureMap extends ClosureMap { + Map _getters = ${generateGetterMap(properties)}; + Map _setters = ${generateSetterMap(properties)}; + List> _functions = ${generateFunctionMap(calls)}; + + Getter lookupGetter(String name) + => _getters[name]; + Setter lookupSetter(String name) + => _setters[name]; + lookupFunction(String name, int arity) + => (arity < _functions.length) ? _functions[arity][name] : null; +} +'''; + } + + generateGetterMap(Iterable keys) { + var lines = keys.map((key) => 'r"${key}": (o) => o.$key'); + return '{\n ${lines.join(",\n ")}\n }'; + } + + generateSetterMap(Iterable keys) { + var lines = keys.map((key) => 'r"${key}": (o, v) => o.$key = v'); + return '{\n ${lines.join(",\n ")}\n }'; + } + + generateFunctionMap(Map> calls) { + Map> arities = {}; + calls.forEach((name, callArities) { + callArities.forEach((arity){ + arities.putIfAbsent(arity, () => new Set()).add(name); + }); + }); + + var maxArity = arities.isEmpty ? 0 : + arities.keys.reduce((x, y) => math.max(x, y)); + + var maps = new Iterable.generate(maxArity, (arity) { + var names = arities[arity]; + if (names == null) { + return '{\n }'; + } else { + var args = new List.generate(arity, (e) => "a$e").join(','); + var p = args.isEmpty ? '' : ', $args'; + var lines = names.map((name) => 'r"$name": (o$p) => o.$name($args)'); + return '{\n ${lines.join(",\n ")}\n }'; + } + }); + + return '[${maps.join(",")}]'; + } +} + diff --git a/lib/tools/transformer/metadata_extractor.dart b/lib/tools/transformer/metadata_extractor.dart new file mode 100644 index 000000000..fefcf25e4 --- /dev/null +++ b/lib/tools/transformer/metadata_extractor.dart @@ -0,0 +1,340 @@ +library angular.metadata_extractor; + +import 'package:analyzer/src/generated/ast.dart'; +import 'package:analyzer/src/generated/element.dart'; +import 'package:barback/barback.dart'; +import 'package:code_transformers/resolver.dart'; + +class AnnotatedType { + final ClassElement type; + Iterable annotations; + + final Map members = {}; + + AnnotatedType(this.type); + + /** + * Finds all the libraries referenced by the annotations + */ + Iterable get referencedLibraries { + var libs = new Set(); + libs.add(type.library); + + var libCollector = new _LibraryCollector(); + for (var annotation in annotations) { + annotation.accept(libCollector); + } + for (var annotation in members.values) { + annotation.accept(libCollector); + } + libs.addAll(libCollector.libraries); + + return libs; + } + + void writeClassAnnotations(StringBuffer sink, TransformLogger logger, + Resolver resolver, Map prefixes) { + sink.write(' ${prefixes[type.library]}${type.name}: [\n'); + var writer = new _AnnotationWriter(sink, prefixes); + for (var annotation in annotations) { + sink.write(' '); + if (writer.writeAnnotation(annotation)) { + sink.write(',\n'); + } else { + sink.write('null,\n'); + logger.warning('Unable to serialize annotation $annotation.', + asset: resolver.getSourceAssetId(annotation.parent.element), + span: resolver.getSourceSpan(annotation.parent.element)); + } + } + sink.write(' ],\n'); + } + + void writeMemberAnnotations(StringBuffer sink, TransformLogger logger, + Resolver resolver, Map prefixes) { + if (members.isEmpty) return; + + sink.write(' ${prefixes[type.library]}${type.name}: {\n'); + + var writer = new _AnnotationWriter(sink, prefixes); + members.forEach((memberName, annotation) { + sink.write(' \'$memberName\': '); + if (writer.writeAnnotation(annotation)) { + sink.write(',\n'); + } else { + sink.write('null,\n'); + logger.warning('Unable to serialize annotation $annotation.', + asset: resolver.getSourceAssetId(annotation.parent.element), + span: resolver.getSourceSpan(annotation.parent.element)); + } + }); + sink.write(' },\n'); + } +} + +/** + * Helper which finds all libraries referenced within the provided AST. + */ +class _LibraryCollector extends GeneralizingASTVisitor { + final Set libraries = new Set(); + void visitSimpleIdentifier(SimpleIdentifier s) { + var element = s.bestElement; + if (element != null) { + libraries.add(element.library); + } + } +} + +/** + * Helper class which writes annotations out to the buffer. + * This does not support every syntax possible, but will return false when + * the annotation cannot be serialized. + */ +class _AnnotationWriter { + final StringBuffer sink; + final Map prefixes; + + _AnnotationWriter(this.sink, this.prefixes); + + /** + * Returns true if the annotation was successfully serialized. + * If the annotation could not be written then the buffer is returned to its + * original state. + */ + bool writeAnnotation(Annotation annotation) { + // Record the current location in the buffer and if writing fails then + // back up the buffer to where we started. + var len = sink.length; + if (!_writeAnnotation(annotation)) { + var str = sink.toString(); + sink.clear(); + sink.write(str.substring(0, len)); + return false; + } + return true; + } + + bool _writeAnnotation(Annotation annotation) { + var element = annotation.element; + if (element is ConstructorElement) { + sink.write('const ${prefixes[element.library]}' + '${element.enclosingElement.name}'); + // Named constructors + if (!element.name.isEmpty) { + sink.write('.${element.name}'); + } + sink.write('('); + if (!_writeArguments(annotation)) return false; + sink.write(')'); + return true; + } else if (element is PropertyAccessorElement) { + sink.write('${prefixes[element.library]}${element.name}'); + return true; + } + + return false; + } + + /** Writes the arguments for a type constructor. */ + bool _writeArguments(Annotation annotation) { + var args = annotation.arguments; + var index = 0; + for (var arg in args.arguments) { + if (arg is NamedExpression) { + sink.write('${arg.name.label.name}: '); + if (!_writeExpression(arg.expression)) return false; + } else { + if (!_writeExpression(arg)) return false; + } + if (++index < args.arguments.length) { + sink.write(', '); + } + } + return true; + } + + /** Writes an expression. */ + bool _writeExpression(Expression expression) { + if (expression is StringLiteral) { + var str = expression.stringValue + .replaceAll(r'\', r'\\') + .replaceAll('\'', '\\\''); + sink.write('\'$str\''); + return true; + } + if (expression is ListLiteral) { + sink.write('const ['); + for (var element in expression.elements) { + if (!_writeExpression(element)) return false; + sink.write(','); + } + sink.write(']'); + return true; + } + if (expression is MapLiteral) { + sink.write('const {'); + var index = 0; + for (var entry in expression.entries) { + if (!_writeExpression(entry.key)) return false; + sink.write(': '); + if (!_writeExpression(entry.value)) return false; + if (++index < expression.entries.length) { + sink.write(', '); + } + } + sink.write('}'); + return true; + } + if (expression is Identifier) { + var element = expression.bestElement; + if (element == null || !element.isPublic) return false; + + if (element is ClassElement) { + sink.write('${prefixes[element.library]}${element.name}'); + return true; + } + if (element is PropertyAccessorElement) { + var variable = element.variable; + if (variable is FieldElement) { + var cls = variable.enclosingElement; + sink.write('${prefixes[cls.library]}${cls.name}.${variable.name}'); + return true; + } else if (variable is TopLevelVariableElement) { + sink.write('${prefixes[variable.library]}${variable.name}'); + return true; + } + print('variable ${variable.runtimeType} $variable'); + } + print('element ${element.runtimeType} $element'); + } + if (expression is BooleanLiteral) { + sink.write(expression.value); + return true; + } + if (expression is DoubleLiteral) { + sink.write(expression.value); + return true; + } + if (expression is IntegerLiteral) { + sink.write(expression.value); + return true; + } + if (expression is NullLiteral) { + sink.write('null'); + return true; + } + print('expression ${expression.runtimeType} $expression'); + return false; + } +} + +class AnnotationExtractor { + final TransformLogger logger; + final Resolver resolver; + + static const List _angularAnnotationNames = const [ + 'angular.core.NgAttr', + 'angular.core.NgOneWay', + 'angular.core.NgOneWayOneTime', + 'angular.core.NgTwoWay', + 'angular.core.NgCallback' + ]; + + /// Resolved annotations that this will pick up for members. + final List _annotationElements = []; + + AnnotationExtractor(this.logger, this.resolver) { + for (var annotation in _angularAnnotationNames) { + var type = resolver.getType(annotation); + if (type == null) { + logger.warning('Unable to resolve $annotation, skipping metadata.'); + continue; + } + _annotationElements.add(type.unnamedConstructor); + } + } + + AnnotatedType extractAnnotations(ClassElement cls) { + if (resolver.getImportUri(cls.library) == null) { + logger.warning('Dropping annotations for ${cls.name} because the ' + 'containing file cannot be imported (must be in a lib folder).', + asset: resolver.getSourceAssetId(cls), + span: resolver.getSourceSpan(cls)); + return null; + } + + var visitor = new _AnnotationVisitor(_annotationElements); + cls.node.accept(visitor); + + if (!visitor.hasAnnotations) return null; + + var type = new AnnotatedType(cls); + type.annotations = visitor.classAnnotations + .where((annotation) { + var element = annotation.element; + if (element != null && !element.isPublic) { + logger.warning('Annotation $annotation is not public.', + asset: resolver.getSourceAssetId(annotation.parent.element), + span: resolver.getSourceSpan(annotation.parent.element)); + return false; + } + if (element is ConstructorElement && + !element.enclosingElement.isPublic) { + logger.warning('Annotation $annotation is not public.', + asset: resolver.getSourceAssetId(annotation.parent.element), + span: resolver.getSourceSpan(annotation.parent.element)); + return false; + } + return true; + }).toList(); + + + visitor.memberAnnotations.forEach((memberName, annotations) { + if (annotations.length > 1) { + logger.warning('$memberName can only have one annotation.', + asset: resolver.getSourceAssetId(annotations[0].parent.element), + span: resolver.getSourceSpan(annotations[0].parent.element)); + return; + } + + type.members[memberName] = annotations[0]; + }); + + if (type.annotations.isEmpty && type.members.isEmpty) return null; + + return type; + } +} + + +/** + * AST visitor which walks the current AST and finds all annotated + * classes and members. + */ +class _AnnotationVisitor extends GeneralizingASTVisitor { + final List allowedMemberAnnotations; + final List classAnnotations = []; + final Map> memberAnnotations = {}; + + _AnnotationVisitor(this.allowedMemberAnnotations); + + void visitAnnotation(Annotation annotation) { + var parent = annotation.parent; + if (parent is! Declaration) return; + + if (parent.element is ClassElement) { + classAnnotations.add(annotation); + } else if (allowedMemberAnnotations.contains(annotation.element)) { + if (parent is MethodDeclaration) { + memberAnnotations.putIfAbsent(parent.name.name, () => []) + .add(annotation); + } else if (parent is FieldDeclaration) { + var name = parent.fields.variables.first.name.name; + memberAnnotations.putIfAbsent(name, () => []).add(annotation); + } + } + } + + bool get hasAnnotations => + !classAnnotations.isEmpty || !memberAnnotations.isEmpty; +} diff --git a/lib/tools/transformer/metadata_generator.dart b/lib/tools/transformer/metadata_generator.dart new file mode 100644 index 000000000..ca0fb419a --- /dev/null +++ b/lib/tools/transformer/metadata_generator.dart @@ -0,0 +1,154 @@ +library angular.tools.transformer.metadata_generator; + +import 'dart:async'; +import 'package:analyzer/src/generated/element.dart'; +import 'package:angular/tools/transformer/options.dart'; +import 'package:barback/barback.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:di/transformer/refactor.dart'; +import 'package:path/path.dart' as path; + +import 'metadata_extractor.dart'; + +const String _generatedMetadataFilename = 'generated_metadata.dart'; + +class MetadataGenerator extends ResolverTransformer { + final TransformOptions options; + + MetadataGenerator(this.options, Resolvers resolvers) { + this.resolvers = resolvers; + } + + Future isPrimary(Asset input) => new Future.value( + options.isDartEntry(input.id)); + + void applyResolver(Transform transform, Resolver resolver) { + var asset = transform.primaryInput; + var extractor = new AnnotationExtractor(transform.logger, resolver); + + var outputBuffer = new StringBuffer(); + _writeHeader(asset.id, outputBuffer); + + var annotatedTypes = resolver.libraries + .where((lib) => !lib.isInSdk) + .expand((lib) => lib.units) + .expand((unit) => unit.types) + .map(extractor.extractAnnotations) + .where((annotations) => annotations != null).toList(); + + var libs = annotatedTypes.expand((type) => type.referencedLibraries) + .toSet(); + + var importPrefixes = {}; + var index = 0; + for (var lib in libs) { + if (lib.isDartCore) { + importPrefixes[lib] = ''; + continue; + } + + var prefix = 'import_${index++}'; + var url = resolver.getImportUri(lib); + outputBuffer.write('import \'$url\' as $prefix;\n'); + importPrefixes[lib] = '$prefix.'; + } + + _writePreamble(outputBuffer); + + _writeClassPreamble(outputBuffer); + for (var type in annotatedTypes) { + type.writeClassAnnotations( + outputBuffer, transform.logger, resolver, importPrefixes); + } + _writeClassEpilogue(outputBuffer); + + _writeMemberPreamble(outputBuffer); + for (var type in annotatedTypes) { + type.writeMemberAnnotations( + outputBuffer, transform.logger, resolver, importPrefixes); + } + _writeMemberEpilogue(outputBuffer); + + var outputId = + new AssetId(asset.id.package, 'lib/$_generatedMetadataFilename'); + transform.addOutput( + new Asset.fromString(outputId, outputBuffer.toString())); + + transformIdentifiers(transform, resolver, + identifier: 'angular.auto_modules.defaultMetadataModule', + replacement: 'metadataModule', + importPrefix: 'generated_metadata', + importUrl: _generatedMetadataFilename); + } +} + +void _writeHeader(AssetId id, StringSink sink) { + var libPath = path.withoutExtension(id.path).replaceAll('/', '.'); + sink.write(''' +library ${id.package}.$libPath.generated_metadata; + +import 'package:angular/angular.dart' show AttrFieldAnnotation, FieldMetadataExtractor, MetadataExtractor; +import 'package:di/di.dart' show Module; + +'''); +} + +void _writePreamble(StringSink sink) { + sink.write(''' +Module get metadataModule => new Module() + ..value(MetadataExtractor, new _StaticMetadataExtractor()) + ..value(FieldMetadataExtractor, new _StaticFieldMetadataExtractor()); + +class _StaticMetadataExtractor implements MetadataExtractor { + Iterable call(Type type) { + var annotations = _classAnnotations[type]; + if (annotations != null) { + return annotations; + } + return []; + } +} + +class _StaticFieldMetadataExtractor implements FieldMetadataExtractor { + Map call(Type type) { + var annotations = _memberAnnotations[type]; + if (annotations != null) { + return annotations; + } + return {}; + } +} + +'''); +} + +void _writeClassPreamble(StringSink sink) { + sink.write(''' +final Map _classAnnotations = { +'''); +} + +void _writeClassEpilogue(StringSink sink) { + sink.write(''' +}; +'''); +} + +void _writeMemberPreamble(StringSink sink) { + sink.write(''' + +final Map> _memberAnnotations = { +'''); +} + +void _writeMemberEpilogue(StringSink sink) { + sink.write(''' +}; +'''); +} + +void _writeFooter(StringSink sink) { + sink.write(''' +}; +'''); +} diff --git a/lib/tools/transformer/options.dart b/lib/tools/transformer/options.dart new file mode 100644 index 000000000..3fe347db6 --- /dev/null +++ b/lib/tools/transformer/options.dart @@ -0,0 +1,58 @@ +library angular.tools.transformer.options; + +import 'dart:async'; + +import 'package:barback/barback.dart'; +import 'package:di/transformer/options.dart' as di; +import 'package:path/path.dart' as path; + +/** Options used by Angular transformers */ +class TransformOptions { + + /** + * The file path of the primary Dart entry point (main) for the application. + * This is used as the starting point to find all expressions used by the + * application. + */ + final String dartEntry; + + /** + * List of html file paths which may contain Angular expressions. + * The paths are relative to the package home and are represented using posix + * style, which matches the representation used in asset ids in barback. + */ + final List htmlFiles; + + /** + * Path to the Dart SDK directory, for resolving Dart libraries. + */ + final String sdkDirectory; + + /** + * Template cache path modifiers + */ + final Map templateUriRewrites; + + /** + * Dependency injection options. + */ + final di.TransformOptions diOptions; + + TransformOptions({String dartEntry, + String sdkDirectory, List htmlFiles, + Map templateUriRewrites, + di.TransformOptions diOptions}) + : dartEntry = dartEntry, + sdkDirectory = sdkDirectory, + htmlFiles = htmlFiles != null ? htmlFiles : [], + templateUriRewrites = templateUriRewrites != null ? + templateUriRewrites : {}, + diOptions = diOptions { + if (sdkDirectory == null) + throw new ArgumentError('sdkDirectory must be provided.'); + } + + // Don't need to check package as transformers only run for primary package. + Future isDartEntry(AssetId id) => + new Future.value(id.path == dartEntry || dartEntry == '*'); +} diff --git a/lib/transformer.dart b/lib/transformer.dart new file mode 100644 index 000000000..977c8cbb5 --- /dev/null +++ b/lib/transformer.dart @@ -0,0 +1,129 @@ +library angular.transformer; + +import 'dart:io'; +import 'package:angular/tools/transformer/expression_generator.dart'; +import 'package:angular/tools/transformer/metadata_generator.dart'; +import 'package:angular/tools/transformer/options.dart'; +import 'package:barback/barback.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:di/transformer/injector_generator.dart' as di; +import 'package:di/transformer/options.dart' as di; +import 'package:path/path.dart' as path; + + + /** + * The Angular transformer, which internally runs several phases that will: + * + * * Extract all expressions for evaluation at runtime without using Mirrors. + * * Extract all classes being dependency injected into a static injector. + * * Extract all metadata for cached reflection. + */ +class AngularTransformerGroup implements TransformerGroup { + final Iterable phases; + + AngularTransformerGroup(TransformOptions options) + : phases = _createPhases(options); + + AngularTransformerGroup.asPlugin(BarbackSettings settings) + : this(_parseSettings(settings.configuration)); +} + +TransformOptions _parseSettings(Map args) { + // Default angular annotations for injectable types + var annotations = [ + 'angular.core.service.NgInjectableService', + 'angular.core.NgDirective', + 'angular.core.NgController', + 'angular.core.NgComponent', + 'angular.core.NgFilter']; + annotations.addAll(_readStringListValue(args, 'injectable_annotations')); + + var injectedTypes = ['perf_api.Profiler', + 'angular.core.parser.static_parser.StaticParser']; + injectedTypes.addAll(_readStringListValue(args, 'injected_types')); + + var sdkDir = _readStringValue(args, 'dart_sdk', required: false); + if (sdkDir == null) { + // Assume the Pub executable is always coming from the SDK. + sdkDir = path.dirname(path.dirname(Platform.executable)); + } + + var dartEntry = _readStringValue(args, 'dart_entry'); + + var diOptions = new di.TransformOptions( + dartEntries: [dartEntry], + injectableAnnotations: annotations, + injectedTypes: injectedTypes, + sdkDirectory: sdkDir); + + return new TransformOptions( + dartEntry: dartEntry, + htmlFiles: _readStringListValue(args, 'html_files'), + sdkDirectory: sdkDir, + templateUriRewrites: _readStringMapValue(args, 'template_uri_rewrites'), + diOptions: diOptions); +} + +_readStringValue(Map args, String name, {bool required: true}) { + var value = args[name]; + if (value == null) { + if (required) { + print('Angular transformer parameter "$name" ' + 'has no value in pubspec.yaml.'); + } + return null; + } + if (value is! String) { + print('Angular transformer parameter "$name" value ' + 'is not a string in pubspec.yaml.'); + return null; + } + return value; +} + +_readStringListValue(Map args, String name) { + var value = args[name]; + if (value == null) return []; + var results = []; + bool error; + if (value is List) { + results = value; + error = value.any((e) => e is! String); + } else if (value is String) { + results = [value]; + error = false; + } else { + error = true; + } + if (error) { + print('Angular transformer parameter "$name" ' + 'has an invalid value in pubspec.yaml.'); + } + return results; +} + +Map _readStringMapValue(Map args, String name) { + var value = args[name]; + if (value == null) return {}; + if (value is! Map) { + print('Angular transformer parameter "$name" ' + 'is expected to be a map parameter.'); + return {}; + } + if (value.keys.any((e) => e is! String) || + value.values.any((e) => e is! String)) { + print('Angular transformer parameter "$name" ' + 'is expected to be a map of strings.'); + return {}; + } + return value; +} + +List> _createPhases(TransformOptions options) { + var resolvers = new Resolvers(options.sdkDirectory); + return [ + [new ExpressionGenerator(options, resolvers)], + [new di.InjectorGenerator(options.diOptions, resolvers)], + [new MetadataGenerator(options, resolvers)], + ]; +} diff --git a/pubspec.lock b/pubspec.lock index 7b1f2bc3c..43f30bb74 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -4,11 +4,15 @@ packages: analyzer: description: analyzer source: hosted - version: "0.11.10" + version: "0.12.2" args: description: args source: hosted version: "0.9.0" + barback: + description: barback + source: hosted + version: "0.11.1" benchmark_harness: description: benchmark_harness source: hosted @@ -17,13 +21,20 @@ packages: description: browser source: hosted version: "0.9.1" + code_transformers: + description: code_transformers + source: hosted + version: "0.0.1-dev.2" collection: description: collection source: hosted version: "0.9.1" di: - description: di - source: hosted + description: + ref: null + resolved-ref: "88e0d48101517e1d3bc84f9d38d4a1b619db65aa" + url: "https://github.com/angular/di.dart.git" + source: git version: "0.0.33" html5lib: description: html5lib @@ -32,11 +43,15 @@ packages: intl: description: intl source: hosted - version: "0.9.6" + version: "0.8.10+4" logging: description: logging source: hosted version: "0.9.1+1" + meta: + description: meta + source: hosted + version: "0.8.8" path: description: path source: hosted diff --git a/pubspec.yaml b/pubspec.yaml index 2668738e7..d349e1111 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,8 +10,9 @@ homepage: https://angulardart.org environment: sdk: '>=1.2.0' dependencies: - analyzer: '>=0.10.0 <0.12.0' + analyzer: '>=0.12.0 <0.13.0' browser: '>=0.8.7 <0.10.0' + code_transformers: '>=0.0.1-dev.2 <0.1.0' collection: '>=0.9.1 <1.0.0' di: '>=0.0.32 <0.1.0' html5lib: '>=0.8.7 <0.10.0' @@ -22,3 +23,7 @@ dependencies: dev_dependencies: benchmark_harness: '>=1.0.0' unittest: '>=0.8.7 <0.10.0' + +dependency_overrides: + di: + git: https://github.com/angular/di.dart.git diff --git a/test/tools/transformer/expression_extractor_spec.dart b/test/tools/transformer/expression_extractor_spec.dart new file mode 100644 index 000000000..de7a68f9e --- /dev/null +++ b/test/tools/transformer/expression_extractor_spec.dart @@ -0,0 +1,81 @@ +library angular.test.tools.transformer.expression_extractor_spec; + +import 'package:angular/tools/transformer/options.dart'; +import 'package:angular/tools/transformer/expression_generator.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:code_transformers/tests.dart' as tests; +import '../../jasmine_syntax.dart'; + +main() { + describe('expression_extractor', () { + var htmlFiles = []; + var options = new TransformOptions( + dartEntry: 'web/main.dart', + htmlFiles: htmlFiles, + sdkDirectory: dartSdkDirectory); + var resolvers = new Resolvers(dartSdkDirectory); + + var phases = [ + [new ExpressionGenerator(options, resolvers)] + ]; + + it('should extract expressions', () { + htmlFiles.add('web/index.html'); + return tests.applyTransformers(phases, + inputs: { + 'angular|lib/auto_modules.dart': PACKAGE_AUTO, + 'a|web/main.dart': ''' +library foo; +import 'package:angular/auto_modules.dart'; +''', + 'a|web/index.html': ''' +
{{some.getter}}
+''' + }, + results: { + 'a|lib/generated_static_expressions.dart': ''' +$HEADER + Map _getters = { + r"some": (o) => o.some, + r"getter": (o) => o.getter + }; + Map _setters = { + r"some": (o, v) => o.some = v, + r"getter": (o, v) => o.getter = v + }; + List> _functions = []; +$FOOTER +''' + }).then((_) { + htmlFiles.clear(); + }); + }); + }); +} + +const String HEADER = ''' +library a.web.main.generated_expressions; + +import 'package:angular/angular.dart'; +import 'package:angular/core/parser/dynamic_parser.dart' show ClosureMap; + +Module get expressionModule => new Module() + ..value(ClosureMap, new StaticClosureMap()); + +class StaticClosureMap extends ClosureMap {'''; + +const String FOOTER = ''' + + Getter lookupGetter(String name) + => _getters[name]; + Setter lookupSetter(String name) + => _setters[name]; + lookupFunction(String name, int arity) + => (arity < _functions.length) ? _functions[arity][name] : null; +}'''; + +const String PACKAGE_AUTO = ''' +library angular.auto_modules; + +Module get defaultExpressionModule => new Module(); +'''; diff --git a/test/tools/transformer/metadata_generator_spec.dart b/test/tools/transformer/metadata_generator_spec.dart new file mode 100644 index 000000000..69528c504 --- /dev/null +++ b/test/tools/transformer/metadata_generator_spec.dart @@ -0,0 +1,504 @@ +library angular.test.tools.transformer.metadata_generator_spec; + +import 'dart:async'; + +import 'package:angular/tools/transformer/options.dart'; +import 'package:angular/tools/transformer/metadata_generator.dart'; +import 'package:barback/barback.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:code_transformers/tests.dart' as tests; + +import '../../jasmine_syntax.dart'; + +main() { + describe('metadata_generator', () { + var options = new TransformOptions( + dartEntry: 'web/main.dart', + sdkDirectory: dartSdkDirectory); + + var resolvers = new Resolvers(dartSdkDirectory); + + var phases = [ + [new MetadataGenerator(options, resolvers)] + ]; + + it('should extract member metadata', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + + @NgDirective(selector: r'[*=/{{.*}}/]') + @proxy + class Engine { + @NgOneWay('another-expression') + String anotherExpression; + + @NgCallback('callback') + set callback(Function) {} + + set twoWayStuff(String abc) {} + @NgTwoWay('two-way-stuff') + String get twoWayStuff => null; + } + ''' + }, + imports: [ + 'import \'package:a/a.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + 'const import_1.NgDirective(selector: \'[*=/{{.*}}/]\')', + 'proxy', + ] + }, + classMembers: { + 'import_0.Engine': { + 'anotherExpression': 'const import_1.NgOneWay(\'another-expression\')', + 'callback': 'const import_1.NgCallback(\'callback\')', + 'twoWayStuff': 'const import_1.NgTwoWay(\'two-way-stuff\')', + } + }); + }); + + it('should warn on un-importable files', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|web/a.dart': ''' + import 'package:angular/angular.dart'; + + @NgDirective(selector: r'[*=/{{.*}}/]') + class Engine {} + ''' + }, + messages: ['warning: Dropping annotations for Engine because the ' + 'containing file cannot be imported (must be in a lib folder). ' + '(a.dart 2 16)']); + }); + + it('should warn on multiple annotations', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + + class Engine { + @NgCallback('callback') + @NgOneWay('another-expression') + set callback(Function) {} + } + ''' + }, + messages: ['warning: callback can only have one annotation. ' + '(package:a/a.dart 3 18)']); + }); + + it('should warn on multiple annotations (across getter/setter)', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + + class Engine { + @NgCallback('callback') + set callback(Function) {} + + @NgOneWay('another-expression') + get callback() {} + } + ''' + }, + messages: ['warning: callback can only have one annotation. ' + '(package:a/a.dart 3 18)']); + }); + + it('should extract map arguments', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + + @NgDirective(map: const {'ng-value': '&ngValue', 'key': 'value'}) + class Engine {} + ''' + }, + imports: [ + 'import \'package:a/a.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + 'const import_1.NgDirective(map: const {\'ng-value\': \'&ngValue\', \'key\': \'value\'})', + ] + }); + }); + + it('should extract list arguments', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + + @NgDirective(publishTypes: const [TextChangeListener]) + class Engine {} + ''' + }, + imports: [ + 'import \'package:a/a.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + 'const import_1.NgDirective(publishTypes: const [import_1.TextChangeListener,])', + ] + }); + }); + + it('should extract primitive literals', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + + @NgOneWay(true) + @NgOneWay(1.0) + @NgOneWay(1) + @NgOneWay(null) + class Engine {} + ''' + }, + imports: [ + 'import \'package:a/a.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + 'const import_1.NgOneWay(true)', + 'const import_1.NgOneWay(1.0)', + 'const import_1.NgOneWay(1)', + 'const import_1.NgOneWay(null)', + ] + }); + }); + + it('should skip and warn on unserializable annotations', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + + @Foo + class Engine {} + + @NgDirective(publishTypes: const [Foo]) + class Car {} + ''' + }, + imports: [ + 'import \'package:a/a.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + 'null', + ], + 'import_0.Car': [ + 'null', + ] + }, + messages: [ + 'warning: Unable to serialize annotation @Foo. ' + '(package:a/a.dart 2 16)', + 'warning: Unable to serialize annotation ' + '@NgDirective(publishTypes: const [Foo]). ' + '(package:a/a.dart 5 16)', + ]); + }); + + it('should extract types across libs', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + import 'package:a/b.dart'; + + @NgDirective(publishTypes: const [Car]) + class Engine {} + ''', + 'a|lib/b.dart': ''' + class Car {} + ''', + }, + imports: [ + 'import \'package:a/a.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + 'import \'package:a/b.dart\' as import_2;', + ], + classes: { + 'import_0.Engine': [ + 'const import_1.NgDirective(publishTypes: const [import_2.Car,])', + ] + }); + }); + + it('should not gather non-member annotations', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + + class Engine { + Engine() { + @NgDirective() + print('something'); + } + } + ''', + }); + }); + + it('properly escapes strings', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': r''' + import 'package:angular/angular.dart'; + + @NgOneWay('foo\' \\') + class Engine { + } + ''', + }, + imports: [ + 'import \'package:a/a.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + r'''const import_1.NgOneWay('foo\' \\')''', + ] + }); + }); + + it('should reference static and global properties', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + + @NgDirective(visibility: NgDirective.CHILDREN_VISIBILITY) + @NgDirective(visibility: CONST_VALUE) + class Engine {} + + const int CONST_VALUE = 2; + ''', + }, + imports: [ + 'import \'package:a/a.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + '''const import_1.NgDirective(visibility: import_1.NgDirective.CHILDREN_VISIBILITY)''', + '''const import_1.NgDirective(visibility: import_0.CONST_VALUE)''', + ] + }); + }); + + it('should not extract private annotations', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + + @_Foo() + @_foo + class Engine { + } + + class _Foo { + const _Foo(); + } + const _Foo _foo = const _Foo(); + ''', + }, + messages: [ + 'warning: Annotation @_Foo() is not public. ' + '(package:a/a.dart 2 16)', + 'warning: Annotation @_foo is not public. (package:a/a.dart 2 16)', + ]); + }); + + it('supports named constructors', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + + @Foo.bar() + @Foo._private() + class Engine { + } + + class Foo { + const Foo.bar(); + const Foo._private(); + } + ''', + }, + imports: [ + 'import \'package:a/a.dart\' as import_0;', + ], + classes: { + 'import_0.Engine': [ + '''const import_0.Foo.bar()''', + ] + }, + messages: [ + 'warning: Annotation @Foo._private() is not public. ' + '(package:a/a.dart 2 16)', + ]); + }); + }); +} + +Future generates(List> phases, + {Map inputs, Iterable imports: const [], + Map classes: const {}, + Map classMembers: const {}, + Iterable messages: const []}) { + + var buffer = new StringBuffer(); + buffer.write('$HEADER\n'); + for (var i in imports) { + buffer.write('$i\n'); + } + buffer.write('$BOILER_PLATE\n'); + for (var className in classes.keys) { + buffer.write(' $className: [\n'); + for (var annotation in classes[className]) { + buffer.write(' $annotation,\n'); + } + buffer.write(' ],\n'); + } + buffer.write('$MEMBER_PREAMBLE\n'); + for (var className in classMembers.keys) { + buffer.write(' $className: {\n'); + var members = classMembers[className]; + for (var memberName in members.keys) { + buffer.write(' \'$memberName\': ${members[memberName]},\n'); + } + buffer.write(' },\n'); + } + + buffer.write('$FOOTER\n'); + + return tests.applyTransformers(phases, + inputs: inputs, + results: { + 'a|lib/generated_metadata.dart': buffer.toString() + }, + messages: messages); +} + +const String HEADER = ''' +library a.web.main.generated_metadata; + +import 'package:angular/angular.dart' show AttrFieldAnnotation, FieldMetadataExtractor, MetadataExtractor; +import 'package:di/di.dart' show Module; +'''; + +const String BOILER_PLATE = ''' +Module get metadataModule => new Module() + ..value(MetadataExtractor, new _StaticMetadataExtractor()) + ..value(FieldMetadataExtractor, new _StaticFieldMetadataExtractor()); + +class _StaticMetadataExtractor implements MetadataExtractor { + Iterable call(Type type) { + var annotations = _classAnnotations[type]; + if (annotations != null) { + return annotations; + } + return []; + } +} + +class _StaticFieldMetadataExtractor implements FieldMetadataExtractor { + Map call(Type type) { + var annotations = _memberAnnotations[type]; + if (annotations != null) { + return annotations; + } + return {}; + } +} + +final Map _classAnnotations = {'''; + +const String MEMBER_PREAMBLE = ''' +}; + +final Map> _memberAnnotations = {'''; + +const String FOOTER = ''' +};'''; + + +const String PACKAGE_ANGULAR = ''' +library angular.core; + +class NgDirective { + const NgDirective({selector, publishTypes, map, visibility}); + + static const int CHILDREN_VISIBILITY = 1; +} + +class NgOneWay { + const NgOneWay(arg); +} + +class NgTwoWay { + const NgTwoWay(arg); +} + +class NgCallback { + const NgCallback(arg); +} + +class NgAttr { + const NgAttr(); +} +class NgOneWayOneTime { + const NgOneWayOneTime(arg); +} + +class TextChangeListener {} +'''; From c841cb224834e61c1f5adae8c1f29a92387da859 Mon Sep 17 00:00:00 2001 From: Pete Blois Date: Mon, 10 Mar 2014 18:46:29 -0700 Subject: [PATCH 3/4] Adding static transformers This adds transformers which generate code for metadata and expressions and switches an app from NgDynamicApp to NgStaticApp. This is currently only configured to run on the Todo & BouncingBalls examples. --- example/pubspec.yaml | 8 +- example/web/bouncing_balls.dart | 103 +------- example/web/todo.dart | 11 +- example/web/todo.html | 2 +- lib/angular_static.dart | 10 +- lib/auto_modules.dart | 64 ----- lib/core/parser/parser.dart | 2 - lib/core/registry_dynamic.dart | 1 - lib/mock/http_backend.dart | 10 +- lib/mock/module.dart | 9 +- lib/mock/test_injection.dart | 7 +- lib/playback/playback_http.dart | 2 +- .../transformer/expression_generator.dart | 40 ++- lib/tools/transformer/metadata_extractor.dart | 173 ++++++++---- lib/tools/transformer/metadata_generator.dart | 67 ++--- lib/tools/transformer/options.dart | 10 +- .../transformer/static_angular_generator.dart | 101 +++++++ lib/transformer.dart | 15 +- test/_specs.dart | 2 + test/jasmine_syntax.dart | 4 +- test/tools/transformer/all.dart | 11 + ...ec.dart => expression_generator_spec.dart} | 53 ++-- .../transformer/metadata_generator_spec.dart | 246 +++++++++--------- .../static_angular_generator_spec.dart | 111 ++++++++ 24 files changed, 586 insertions(+), 476 deletions(-) delete mode 100644 lib/auto_modules.dart create mode 100644 lib/tools/transformer/static_angular_generator.dart create mode 100644 test/tools/transformer/all.dart rename test/tools/transformer/{expression_extractor_spec.dart => expression_generator_spec.dart} (57%) create mode 100644 test/tools/transformer/static_angular_generator_spec.dart diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 888a5bb11..3e0e3b133 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -8,8 +8,12 @@ dependencies: transformers: - angular: - dart_entry: web/todo.dart - html_files: web/todo.html + dart_entries: + - web/todo.dart + - web/bouncing_balls.dart + html_files: # Need to split out to per-entry assets + - web/todo.html + - web/bouncing_balls.html dependency_overrides: di: diff --git a/example/web/bouncing_balls.dart b/example/web/bouncing_balls.dart index b5718d072..c5bdd3437 100644 --- a/example/web/bouncing_balls.dart +++ b/example/web/bouncing_balls.dart @@ -1,6 +1,6 @@ import 'package:perf_api/perf_api.dart'; import 'package:angular/angular.dart'; -import 'package:angular/angular_static.dart'; +import 'package:angular/angular_dynamic.dart'; import 'package:angular/change_detection/change_detection.dart'; import 'dart:html'; import 'dart:math'; @@ -125,106 +125,7 @@ class MyModule extends Module { } main() { - var getters = { - 'x': (o) => o.x, - 'y': (o) => o.y, - 'bounce': (o) => o.bounce, - 'fps': (o) => o.fps, - 'balls': (o) => o.balls, - 'length': (o) => o.length, - 'digestTime': (o) => o.digestTime, - 'ballClassName': (o) => o.ballClassName, - 'position': (o) => o.position, - 'onClick': (o) => o.onClick, - 'ball': (o) => o.ball, - 'color': (o) => o.color, - 'changeCount': (o) => o.changeCount, - 'playPause': (o) => o.playPause, - 'toggleCSS': (o) => o.toggleCSS, - 'timeDigest': (o) => o.timeDigest, - 'expression': (o) => o.expression, - 'fps': (o) => o.fps, - 'length': (o) => o.length, - 'digestTime': (o) => o.digestTime, - 'ballClassName': (o) => o.ballClassName, - }; - var setters = { - 'position': (o, v) => o.position = v, - 'onClick': (o, v) => o.onClick = v, - 'ball': (o, v) => o.ball = v, - 'x': (o, v) => o.x = v, - 'y': (o, v) => o.y = v, - 'balls': (o, v) => o.balls = v, - 'bounce': (o, v) => o.bounce = v, - 'expression': (o, v) => o.expression = v, - 'fps': (o, v) => o.fps = v, - 'length': (o, v) => o.length = v, - 'digestTime': (o, v) => o.digestTime = v, - 'ballClassName': (o, v) => o.ballClassName = v, - }; - var metadata = { - BounceController: [new NgController(selector: '[bounce-controller]', publishAs: 'bounce')], - BallPositionDirective: [new NgDirective(selector: '[ball-position]', map: const { "ball-position": '=>position'})], - NgEventDirective: [new NgDirective(selector: '[ng-click]', map: const {'ng-click': '&onClick'})], - NgADirective: [new NgDirective(selector: 'a[href]')], - NgRepeatDirective: [new NgDirective(children: NgAnnotation.TRANSCLUDE_CHILDREN, selector: '[ng-repeat]', map: const {'.': '@expression'})], - NgTextMustacheDirective: [new NgDirective(selector: r':contains(/{{.*}}/)')], - NgAttrMustacheDirective: [new NgDirective(selector: r'[*=/{{.*}}/]')], - }; - var types = { - Profiler: (t) => new Profiler(), - DirectiveSelectorFactory: (t) => new DirectiveSelectorFactory(), - DirectiveMap: (t) => new DirectiveMap(t(Injector), t(MetadataExtractor), t(DirectiveSelectorFactory)), - Lexer: (t) => new Lexer(), - ClosureMap: (t) => new StaticClosureMap(getters, setters), // TODO: types don't match - DynamicParserBackend: (t) => new DynamicParserBackend(t(ClosureMap)), - DynamicParser: (t) => new DynamicParser(t(Lexer), t(ParserBackend)), - Compiler: (t) => new Compiler(t(Profiler), t(Parser), t(Expando)), - AstParser: (t) => new AstParser(t(Parser)), - FilterMap: (t) => new FilterMap(t(Injector), t(MetadataExtractor)), - ExceptionHandler: (t) => new ExceptionHandler(), - FieldGetterFactory: (t) => new StaticFieldGetterFactory(getters), - ScopeDigestTTL: (t) => new ScopeDigestTTL(), - ScopeStats: (t) => new ScopeStats(), - RootScope: (t) => new RootScope(t(Object), t(AstParser), t(Parser), t(FieldGetterFactory), t(FilterMap), t(ExceptionHandler), t(ScopeDigestTTL), t(NgZone), t(ScopeStats)), - NgAnimate: (t) => new NgAnimate(), - Interpolate: (t) => new Interpolate(t(Parser)), - - NgEventDirective: (t) => new NgEventDirective(t(Element), t(Scope)), - NgADirective: (t) => new NgADirective(t(Element)), - NgRepeatDirective: (t) => new NgRepeatDirective(t(BlockHole), t(BoundBlockFactory), t(Scope), t(Parser), t(AstParser), t(FilterMap)), - - BounceController: (t) => new BounceController(t(Scope)), - BallPositionDirective: (t) => new BallPositionDirective(t(Element), t(Scope)), - }; - new NgStaticApp(types, metadata, getters) + new NgDynamicApp() .addModule(new MyModule()) .run(); } - -class StaticClosureMap extends ClosureMap { - final Map getters; - final Map setters; - - StaticClosureMap(this.getters, this.setters); - - Getter lookupGetter(String name) { - Getter getter = getters[name]; - if (getter == null) throw "No getter for '$name'."; - return getter; - } - - Setter lookupSetter(String name) { - Setter setter = setters[name]; - if (setter == null) throw "No setter for '$name'."; - return setter; - } - - Function lookupFunction(String name, int arity) { - var fn = lookupGetter(name); - return (o, [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13]) { - var args = [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13]; - return Function.apply(fn(o), args.getRange(0, arity).toList()); - }; - } -} diff --git a/example/web/todo.dart b/example/web/todo.dart index c40cd12cd..0da49f35f 100644 --- a/example/web/todo.dart +++ b/example/web/todo.dart @@ -3,18 +3,9 @@ library todo; import 'package:angular/angular.dart'; import 'package:angular/angular_dynamic.dart'; import 'package:angular/playback/playback_http.dart'; -import 'todo.dart'; import 'dart:html'; -// This annotation allows Dart to shake away any classes -// not used from Dart code nor listed in another @MirrorsUsed. -// -// If you create classes that are referenced from the Angular -// expressions, you must include a library target in @MirrorsUsed. -@MirrorsUsed(override: '*') -import 'dart:mirrors'; - class Item { String text; bool done; @@ -40,6 +31,7 @@ abstract class ServerController { // An implementation of ServerController that does nothing. +@NgInjectableService() class NoServerController implements ServerController { init(TodoController todo) { } } @@ -47,6 +39,7 @@ class NoServerController implements ServerController { // An implementation of ServerController that fetches items from // the server over HTTP. +@NgInjectableService() class HttpServerController implements ServerController { final Http _http; HttpServerController(this._http); diff --git a/example/web/todo.html b/example/web/todo.html index aeeed8732..cbd32cc92 100644 --- a/example/web/todo.html +++ b/example/web/todo.html @@ -5,7 +5,7 @@ Things To Do - + diff --git a/lib/angular_static.dart b/lib/angular_static.dart index 717aee384..d250f7d22 100644 --- a/lib/angular_static.dart +++ b/lib/angular_static.dart @@ -1,5 +1,9 @@ library angular.static; +// REMOVE once all mirrors dependencies are gone. +@MirrorsUsed(override: const ['*'], targets: const []) +import 'dart:mirrors'; + import 'package:di/static_injector.dart'; import 'package:angular/angular.dart'; import 'package:angular/core/registry_static.dart'; @@ -11,10 +15,12 @@ class NgStaticApp extends NgApp { NgStaticApp(Map this.typeFactories, Map metadata, - Map fieldGetters) { + Map fieldGetters, + ClosureMap closureMap) { ngModule ..value(MetadataExtractor, new StaticMetadataExtractor(metadata)) - ..value(FieldGetterFactory, new StaticFieldGetterFactory(fieldGetters)); + ..value(FieldGetterFactory, new StaticFieldGetterFactory(fieldGetters)) + ..value(ClosureMap, closureMap); } Injector createInjector() diff --git a/lib/auto_modules.dart b/lib/auto_modules.dart deleted file mode 100644 index 8b52d4ee9..000000000 --- a/lib/auto_modules.dart +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Library to automatically switch between the dynamic injector and a static - * injector created by a pub build task. - * - * ## Step 1: Hook up the build step - * Edit ```pubspec.yaml``` to add the di transformer to the list of - * transformers. - * - * name: transformer_demo - * version: 0.0.1 - * dependencies: - * browser: any - * inject: any - * transformers: - * - angular: - * dart_entry: web/main.dart - * injectableAnnotations: NgInjectableService - * - * It's important to have the ```dart_entry``` entry to indicate the entry - * point of the application. - * - * By default, any classes or constructors annotated with @inject will be - * injected, but additional annotations can be specified with the annotations - * argument. - * - * ## Step 2: Annotate your types - * - * class Engine { - * @inject - * Engine(); - * } - * - * or - * - * @NgInjectableServide // custom annotation provided in pubspec.yaml - * class Car {} - * - * Note that all injectable classes must be in source files in lib/ directories. - * - * ## Step 3: Use the auto injector - * Modify your entry script to use the [defaultAutoInjector] as the injector, - * or alternatively the [defaultInjectorModule]. - * - * This must be done from the file registered as the dart_entry in pubspec.yaml - * as this is the only file which will be modified to include the generated - * injector. - */ -library angular.auto_modules; - -import 'package:di/di.dart'; -import 'package:di/dynamic_injector.dart'; - - -@MirrorsUsed(override: '*') -import 'dart:mirrors' show MirrorsUsed; - -// Empty since the default is the dynamic expression module. -Module get defaultExpressionModule => new Module(); - -// Empty since the default is the dynamic metadata module. -Module get defaultMetadataModule => new Module(); - -// Empty since the default is the template cache module. -Module get defaultTemplateCacheModule => new Module(); diff --git a/lib/core/parser/parser.dart b/lib/core/parser/parser.dart index 0e8af1a81..a205cfebb 100644 --- a/lib/core/parser/parser.dart +++ b/lib/core/parser/parser.dart @@ -4,8 +4,6 @@ export 'package:angular/core/parser/syntax.dart' show Visitor, Expression, BoundExpression; export 'package:angular/core/parser/dynamic_parser.dart' show DynamicParser, DynamicParserBackend, ClosureMap; -export 'package:angular/core/parser/static_parser.dart' - show StaticParser, StaticParserFunctions; typedef LocalsWrapper(context, locals); typedef Getter(self); diff --git a/lib/core/registry_dynamic.dart b/lib/core/registry_dynamic.dart index 37de9cd00..49fff9537 100644 --- a/lib/core/registry_dynamic.dart +++ b/lib/core/registry_dynamic.dart @@ -3,7 +3,6 @@ library angular.core_dynamic; import 'dart:mirrors'; import 'package:angular/core/module.dart'; -@NgInjectableService() class DynamicMetadataExtractor implements MetadataExtractor { final _fieldAnnotations = [ reflectType(NgAttr), diff --git a/lib/mock/http_backend.dart b/lib/mock/http_backend.dart index 20a8e97a7..5108b8fb4 100644 --- a/lib/mock/http_backend.dart +++ b/lib/mock/http_backend.dart @@ -1,4 +1,12 @@ -part of angular.mock; +library angular.mock.http_backend; + +import 'dart:async' as dart_async; +import 'dart:convert' show JSON; +import 'dart:html'; + +import 'package:angular/angular.dart'; +import 'package:angular/utils.dart' as utils; + class _MockXhr { var $$method, $$url, $$async, $$reqHeaders, $$respHeaders; diff --git a/lib/mock/module.dart b/lib/mock/module.dart index f62cf96e6..a56a9acc2 100644 --- a/lib/mock/module.dart +++ b/lib/mock/module.dart @@ -2,26 +2,23 @@ library angular.mock; import 'dart:async' as dart_async; import 'dart:collection' show ListBase; -import 'dart:convert' show JSON; import 'dart:html'; import 'dart:js' as js; import 'package:angular/angular.dart'; -import 'package:angular/angular_dynamic.dart'; -import 'package:angular/utils.dart' as utils; import 'package:di/di.dart'; -import 'package:di/dynamic_injector.dart'; import 'package:unittest/mock.dart'; +import 'http_backend.dart'; + +export 'http_backend.dart'; export 'zone.dart'; part 'debug.dart'; part 'exception_handler.dart'; -part 'http_backend.dart'; part 'log.dart'; part 'probe.dart'; part 'test_bed.dart'; part 'mock_window.dart'; -part 'test_injection.dart'; /** * Use in addition to [AngularModule] in your tests. diff --git a/lib/mock/test_injection.dart b/lib/mock/test_injection.dart index 471eaa424..0d14668b7 100644 --- a/lib/mock/test_injection.dart +++ b/lib/mock/test_injection.dart @@ -1,4 +1,9 @@ -part of angular.mock; +library angular.mock.test_injection; + +import 'package:angular/angular_dynamic.dart'; +import 'package:angular/mock/module.dart'; +import 'package:di/di.dart'; +import 'package:di/dynamic_injector.dart'; _SpecInjector _currentSpecInjector = null; diff --git a/lib/playback/playback_http.dart b/lib/playback/playback_http.dart index f6253a3d9..8b3d6afaa 100644 --- a/lib/playback/playback_http.dart +++ b/lib/playback/playback_http.dart @@ -6,7 +6,7 @@ import 'dart:html'; import 'package:angular/core_dom/module.dart'; import 'package:angular/core/service.dart'; -import 'package:angular/mock/module.dart' as mock; +import 'package:angular/mock/http_backend.dart' as mock; import 'package:angular/playback/playback_data.dart' as playback_data; diff --git a/lib/tools/transformer/expression_generator.dart b/lib/tools/transformer/expression_generator.dart index 924526ee9..94f94a9d4 100644 --- a/lib/tools/transformer/expression_generator.dart +++ b/lib/tools/transformer/expression_generator.dart @@ -3,7 +3,6 @@ library angular.tools.transformer.expression_generator; import 'dart:async'; import 'dart:math' as math; import 'package:analyzer/src/generated/element.dart'; -import 'package:angular/core/module.dart'; import 'package:angular/core/parser/parser.dart'; import 'package:angular/tools/html_extractor.dart'; import 'package:angular/tools/parser_getter_setter/generator.dart'; @@ -14,12 +13,8 @@ import 'package:barback/barback.dart'; import 'package:code_transformers/resolver.dart'; import 'package:di/di.dart'; import 'package:di/dynamic_injector.dart'; -import 'package:di/transformer/refactor.dart'; import 'package:path/path.dart' as path; - -const String _generatedExpressionFilename = 'generated_static_expressions.dart'; - /** * Transformer which gathers all expressions from the HTML source files and * Dart source files of an application and packages them for static evaluation. @@ -28,7 +23,7 @@ const String _generatedExpressionFilename = 'generated_static_expressions.dart'; * expressions and modify all references to NG_EXPRESSION_MODULE to refer to * the generated expressions. */ -class ExpressionGenerator extends ResolverTransformer { +class ExpressionGenerator extends Transformer with ResolverTransformer { final TransformOptions options; ExpressionGenerator(this.options, Resolvers resolvers) { @@ -61,16 +56,15 @@ class ExpressionGenerator extends ResolverTransformer { injector.get(_ParserGetterSetter).generateParser( htmlExtractor.expressions.toList(), outputBuffer); - var outputId = - new AssetId(asset.id.package, 'lib/$_generatedExpressionFilename'); + var id = transform.primaryInput.id; + var outputFilename = '${path.url.basenameWithoutExtension(id.path)}' + '_static_expressions.dart'; + var outputPath = path.url.join(path.url.dirname(id.path), outputFilename); + var outputId = new AssetId(id.package, outputPath); transform.addOutput( new Asset.fromString(outputId, outputBuffer.toString())); - transformIdentifiers(transform, resolver, - identifier: 'angular.auto_modules.defaultExpressionModule', - replacement: 'expressionModule', - importPrefix: 'generated_static_expressions', - importUrl: _generatedExpressionFilename); + transform.addOutput(asset); }); } @@ -143,28 +137,26 @@ class _ParserGetterSetter { Map> calls) { return ''' class StaticClosureMap extends ClosureMap { - Map _getters = ${generateGetterMap(properties)}; - Map _setters = ${generateSetterMap(properties)}; - List> _functions = ${generateFunctionMap(calls)}; - - Getter lookupGetter(String name) - => _getters[name]; - Setter lookupSetter(String name) - => _setters[name]; + Getter lookupGetter(String name) => getters[name]; + Setter lookupSetter(String name) => setters[name]; lookupFunction(String name, int arity) - => (arity < _functions.length) ? _functions[arity][name] : null; + => (arity < functions.length) ? functions[arity][name] : null; } + +final Map getters = ${generateGetterMap(properties)}; +final Map setters = ${generateSetterMap(properties)}; +final List> functions = ${generateFunctionMap(calls)}; '''; } generateGetterMap(Iterable keys) { var lines = keys.map((key) => 'r"${key}": (o) => o.$key'); - return '{\n ${lines.join(",\n ")}\n }'; + return '{\n ${lines.join(",\n ")}\n}'; } generateSetterMap(Iterable keys) { var lines = keys.map((key) => 'r"${key}": (o, v) => o.$key = v'); - return '{\n ${lines.join(",\n ")}\n }'; + return '{\n ${lines.join(",\n ")}\n}'; } generateFunctionMap(Map> calls) { diff --git a/lib/tools/transformer/metadata_extractor.dart b/lib/tools/transformer/metadata_extractor.dart index fefcf25e4..83cd56dd3 100644 --- a/lib/tools/transformer/metadata_extractor.dart +++ b/lib/tools/transformer/metadata_extractor.dart @@ -2,6 +2,8 @@ library angular.metadata_extractor; import 'package:analyzer/src/generated/ast.dart'; import 'package:analyzer/src/generated/element.dart'; +import 'package:analyzer/src/generated/scanner.dart'; +import 'package:analyzer/src/generated/utilities_dart.dart' show ParameterKind; import 'package:barback/barback.dart'; import 'package:code_transformers/resolver.dart'; @@ -9,8 +11,6 @@ class AnnotatedType { final ClassElement type; Iterable annotations; - final Map members = {}; - AnnotatedType(this.type); /** @@ -24,9 +24,6 @@ class AnnotatedType { for (var annotation in annotations) { annotation.accept(libCollector); } - for (var annotation in members.values) { - annotation.accept(libCollector); - } libs.addAll(libCollector.libraries); return libs; @@ -34,7 +31,7 @@ class AnnotatedType { void writeClassAnnotations(StringBuffer sink, TransformLogger logger, Resolver resolver, Map prefixes) { - sink.write(' ${prefixes[type.library]}${type.name}: [\n'); + sink.write(' ${prefixes[type.library]}${type.name}: const [\n'); var writer = new _AnnotationWriter(sink, prefixes); for (var annotation in annotations) { sink.write(' '); @@ -49,27 +46,6 @@ class AnnotatedType { } sink.write(' ],\n'); } - - void writeMemberAnnotations(StringBuffer sink, TransformLogger logger, - Resolver resolver, Map prefixes) { - if (members.isEmpty) return; - - sink.write(' ${prefixes[type.library]}${type.name}: {\n'); - - var writer = new _AnnotationWriter(sink, prefixes); - members.forEach((memberName, annotation) { - sink.write(' \'$memberName\': '); - if (writer.writeAnnotation(annotation)) { - sink.write(',\n'); - } else { - sink.write('null,\n'); - logger.warning('Unable to serialize annotation $annotation.', - asset: resolver.getSourceAssetId(annotation.parent.element), - span: resolver.getSourceSpan(annotation.parent.element)); - } - }); - sink.write(' },\n'); - } } /** @@ -231,6 +207,7 @@ class _AnnotationWriter { class AnnotationExtractor { final TransformLogger logger; final Resolver resolver; + final AssetId outputId; static const List _angularAnnotationNames = const [ 'angular.core.NgAttr', @@ -240,10 +217,20 @@ class AnnotationExtractor { 'angular.core.NgCallback' ]; + static const Map _annotationToMapping = const { + 'NgAttr': '@', + 'NgOneWay': '=>', + 'NgOneWayOneTime': '=>!', + 'NgTwoWay': '<=>', + 'NgCallback': '&', + }; + + ClassElement ngAnnotationType; + /// Resolved annotations that this will pick up for members. final List _annotationElements = []; - AnnotationExtractor(this.logger, this.resolver) { + AnnotationExtractor(this.logger, this.resolver, this.outputId) { for (var annotation in _angularAnnotationNames) { var type = resolver.getType(annotation); if (type == null) { @@ -252,14 +239,17 @@ class AnnotationExtractor { } _annotationElements.add(type.unnamedConstructor); } + ngAnnotationType = resolver.getType('angular.core.NgAnnotation'); + if (ngAnnotationType == null) { + logger.warning('Unable to resolve NgAnnotation, ' + 'skipping member annotations.'); + } } AnnotatedType extractAnnotations(ClassElement cls) { - if (resolver.getImportUri(cls.library) == null) { - logger.warning('Dropping annotations for ${cls.name} because the ' - 'containing file cannot be imported (must be in a lib folder).', - asset: resolver.getSourceAssetId(cls), - span: resolver.getSourceSpan(cls)); + if (resolver.getImportUri(cls.library, from: outputId) == null) { + warn('Dropping annotations for ${cls.name} because the ' + 'containing file cannot be imported (must be in a lib folder).', cls); return null; } @@ -273,37 +263,132 @@ class AnnotationExtractor { .where((annotation) { var element = annotation.element; if (element != null && !element.isPublic) { - logger.warning('Annotation $annotation is not public.', - asset: resolver.getSourceAssetId(annotation.parent.element), - span: resolver.getSourceSpan(annotation.parent.element)); + warn('Annotation $annotation is not public.', + annotation.parent.element); return false; } if (element is ConstructorElement && !element.enclosingElement.isPublic) { - logger.warning('Annotation $annotation is not public.', - asset: resolver.getSourceAssetId(annotation.parent.element), - span: resolver.getSourceSpan(annotation.parent.element)); + warn('Annotation $annotation is not public.', + annotation.parent.element); return false; } return true; }).toList(); + var memberAnnotations = {}; visitor.memberAnnotations.forEach((memberName, annotations) { if (annotations.length > 1) { - logger.warning('$memberName can only have one annotation.', - asset: resolver.getSourceAssetId(annotations[0].parent.element), - span: resolver.getSourceSpan(annotations[0].parent.element)); + warn('$memberName can only have one annotation.', + annotations[0].parent.element); return; } - type.members[memberName] = annotations[0]; + memberAnnotations[memberName] = annotations[0]; }); - if (type.annotations.isEmpty && type.members.isEmpty) return null; + if (memberAnnotations.isNotEmpty) { + _foldMemberAnnotations(memberAnnotations, type); + } + + if (type.annotations.isEmpty) return null; return type; } + + /// Folds all AttrFieldAnnotations into the NgAnnotation annotation on the + /// class. + _foldMemberAnnotations(Map memberAnnotations, + AnnotatedType type) { + // Filter down to NgAnnotation constructors. + var ngAnnotations = type.annotations.where((a) { + var element = a.element; + if (element is! ConstructorElement) return false; + return element.enclosingElement.type.isAssignableTo( + ngAnnotationType.type); + }); + if (ngAnnotations.isEmpty) { + warn('Found field annotation but no class directives.', type.type); + return; + } + + var mapType = resolver.getType('dart.core.Map').type; + // Find acceptable constructors- ones which take a param named 'map' + var acceptableAnnotations = ngAnnotations.where((a) { + var ctor = a.element; + + for (var param in ctor.parameters) { + if (param.parameterKind != ParameterKind.NAMED) { + continue; + } + if (param.name == 'map' && param.type.isAssignableTo(mapType)) { + return true; + } + } + return false; + }); + + if (acceptableAnnotations.isEmpty) { + warn('Could not find a constructor for member annotations in ' + '$ngAnnotations', type.type); + return; + } + + // Default to using the first acceptable annotation- not sure if + // more than one should ever occur. + var annotation = acceptableAnnotations.first; + var mapArg = annotation.arguments.arguments.firstWhere((arg) => + (arg is NamedExpression) && (arg.name.label.name == 'map'), + orElse: () => null); + + // If we don't have a 'map' parameter yet, add one. + if (mapArg == null) { + var map = new MapLiteral(null, null, null, [], null); + var label = new Label(new SimpleIdentifier(stringToken('map')), + new _GeneratedToken(TokenType.COLON, ':')); + mapArg = new NamedExpression(label, map); + annotation.arguments.arguments.add(mapArg); + } + + var map = mapArg.expression; + if (map is! MapLiteral) { + warn('Expected \'map\' argument of $annotation to be a map literal', + type.type); + return; + } + memberAnnotations.forEach((memberName, annotation) { + var key = annotation.arguments.arguments.first; + // If the key already exists then it means we have two annotations for + // same member. + if (map.entries.any((entry) => entry.key.toString() == key.toString())) { + warn('Directive $annotation already contains an entry for $key', + type.type); + return; + } + + var typeName = annotation.element.enclosingElement.name; + var value = '${_annotationToMapping[typeName]}$memberName'; + var entry = new MapLiteralEntry( + key, + new _GeneratedToken(TokenType.COLON, ':'), + new SimpleStringLiteral(stringToken(value), value)); + map.entries.add(entry); + }); + } + + Token stringToken(String str) => new _GeneratedToken(TokenType.STRING, str); + + void warn(String msg, Element element) { + logger.warning(msg, asset: resolver.getSourceAssetId(element), + span: resolver.getSourceSpan(element)); + } +} + +/// Subclass for tokens which we're generating here. +class _GeneratedToken extends Token { + final String lexeme; + _GeneratedToken(TokenType type, this.lexeme) : super(type, 0); } diff --git a/lib/tools/transformer/metadata_generator.dart b/lib/tools/transformer/metadata_generator.dart index ca0fb419a..0768cf662 100644 --- a/lib/tools/transformer/metadata_generator.dart +++ b/lib/tools/transformer/metadata_generator.dart @@ -10,9 +10,7 @@ import 'package:path/path.dart' as path; import 'metadata_extractor.dart'; -const String _generatedMetadataFilename = 'generated_metadata.dart'; - -class MetadataGenerator extends ResolverTransformer { +class MetadataGenerator extends Transformer with ResolverTransformer { final TransformOptions options; MetadataGenerator(this.options, Resolvers resolvers) { @@ -24,7 +22,14 @@ class MetadataGenerator extends ResolverTransformer { void applyResolver(Transform transform, Resolver resolver) { var asset = transform.primaryInput; - var extractor = new AnnotationExtractor(transform.logger, resolver); + var id = asset.id; + var outputFilename = '${path.url.basenameWithoutExtension(id.path)}' + '_static_metadata.dart'; + var outputPath = path.url.join(path.url.dirname(id.path), outputFilename); + var outputId = new AssetId(id.package, outputPath); + + var extractor = new AnnotationExtractor(transform.logger, resolver, + outputId); var outputBuffer = new StringBuffer(); _writeHeader(asset.id, outputBuffer); @@ -48,7 +53,7 @@ class MetadataGenerator extends ResolverTransformer { } var prefix = 'import_${index++}'; - var url = resolver.getImportUri(lib); + var url = resolver.getImportUri(lib, from: outputId); outputBuffer.write('import \'$url\' as $prefix;\n'); importPrefixes[lib] = '$prefix.'; } @@ -62,23 +67,9 @@ class MetadataGenerator extends ResolverTransformer { } _writeClassEpilogue(outputBuffer); - _writeMemberPreamble(outputBuffer); - for (var type in annotatedTypes) { - type.writeMemberAnnotations( - outputBuffer, transform.logger, resolver, importPrefixes); - } - _writeMemberEpilogue(outputBuffer); - - var outputId = - new AssetId(asset.id.package, 'lib/$_generatedMetadataFilename'); - transform.addOutput( - new Asset.fromString(outputId, outputBuffer.toString())); - - transformIdentifiers(transform, resolver, - identifier: 'angular.auto_modules.defaultMetadataModule', - replacement: 'metadataModule', - importPrefix: 'generated_metadata', - importUrl: _generatedMetadataFilename); + transform.addOutput( + new Asset.fromString(outputId, outputBuffer.toString())); + transform.addOutput(asset); } } @@ -87,7 +78,7 @@ void _writeHeader(AssetId id, StringSink sink) { sink.write(''' library ${id.package}.$libPath.generated_metadata; -import 'package:angular/angular.dart' show AttrFieldAnnotation, FieldMetadataExtractor, MetadataExtractor; +import 'package:angular/angular.dart' show MetadataExtractor; import 'package:di/di.dart' show Module; '''); @@ -96,12 +87,11 @@ import 'package:di/di.dart' show Module; void _writePreamble(StringSink sink) { sink.write(''' Module get metadataModule => new Module() - ..value(MetadataExtractor, new _StaticMetadataExtractor()) - ..value(FieldMetadataExtractor, new _StaticFieldMetadataExtractor()); + ..value(MetadataExtractor, new _StaticMetadataExtractor()); class _StaticMetadataExtractor implements MetadataExtractor { Iterable call(Type type) { - var annotations = _classAnnotations[type]; + var annotations = typeAnnotations[type]; if (annotations != null) { return annotations; } @@ -109,22 +99,12 @@ class _StaticMetadataExtractor implements MetadataExtractor { } } -class _StaticFieldMetadataExtractor implements FieldMetadataExtractor { - Map call(Type type) { - var annotations = _memberAnnotations[type]; - if (annotations != null) { - return annotations; - } - return {}; - } -} - '''); } void _writeClassPreamble(StringSink sink) { sink.write(''' -final Map _classAnnotations = { +final Map typeAnnotations = { '''); } @@ -134,19 +114,6 @@ void _writeClassEpilogue(StringSink sink) { '''); } -void _writeMemberPreamble(StringSink sink) { - sink.write(''' - -final Map> _memberAnnotations = { -'''); -} - -void _writeMemberEpilogue(StringSink sink) { - sink.write(''' -}; -'''); -} - void _writeFooter(StringSink sink) { sink.write(''' }; diff --git a/lib/tools/transformer/options.dart b/lib/tools/transformer/options.dart index 3fe347db6..ce681013c 100644 --- a/lib/tools/transformer/options.dart +++ b/lib/tools/transformer/options.dart @@ -10,11 +10,11 @@ import 'package:path/path.dart' as path; class TransformOptions { /** - * The file path of the primary Dart entry point (main) for the application. + * The file paths of the primary Dart entry point (main) for the application. * This is used as the starting point to find all expressions used by the * application. */ - final String dartEntry; + final Set dartEntries; /** * List of html file paths which may contain Angular expressions. @@ -38,11 +38,11 @@ class TransformOptions { */ final di.TransformOptions diOptions; - TransformOptions({String dartEntry, + TransformOptions({List dartEntries, String sdkDirectory, List htmlFiles, Map templateUriRewrites, di.TransformOptions diOptions}) - : dartEntry = dartEntry, + : dartEntries = dartEntries.toSet(), sdkDirectory = sdkDirectory, htmlFiles = htmlFiles != null ? htmlFiles : [], templateUriRewrites = templateUriRewrites != null ? @@ -54,5 +54,5 @@ class TransformOptions { // Don't need to check package as transformers only run for primary package. Future isDartEntry(AssetId id) => - new Future.value(id.path == dartEntry || dartEntry == '*'); + new Future.value(dartEntries.contains(id.path)); } diff --git a/lib/tools/transformer/static_angular_generator.dart b/lib/tools/transformer/static_angular_generator.dart new file mode 100644 index 000000000..5b0132571 --- /dev/null +++ b/lib/tools/transformer/static_angular_generator.dart @@ -0,0 +1,101 @@ +library angular.tools.transformer.static_angular_generator; + +import 'dart:async'; +import 'package:analyzer/src/generated/ast.dart'; +import 'package:analyzer/src/generated/element.dart'; +import 'package:angular/tools/transformer/options.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:di/transformer/refactor.dart'; +import 'package:barback/barback.dart'; +import 'package:path/path.dart' as path; +import 'package:source_maps/refactor.dart' show TextEditTransaction; + +class StaticAngularGenerator extends Transformer with ResolverTransformer { + final TransformOptions options; + + StaticAngularGenerator(this.options, Resolvers resolvers) { + this.resolvers = resolvers; + } + + Future isPrimary(Asset input) => new Future.value( + options.isDartEntry(input.id)); + + void applyResolver(Transform transform, Resolver resolver) { + var asset = transform.primaryInput; + + var dynamicApp = resolver.getType('angular.dynamic.NgDynamicApp'); + if (dynamicApp == null) { + // No dynamic app imports, exit. + transform.addOutput(transform.primaryInput); + return; + } + + var id = asset.id; + var lib = resolver.entryLibrary; + var transaction = resolver.createTextEditTransaction(lib); + var unit = lib.definingCompilationUnit.node; + + for (var directive in unit.directives) { + if (directive is ImportDirective && + directive.uri.stringValue == 'package:angular/angular_dynamic.dart') { + var uri = directive.uri; + transaction.edit(uri.beginToken.offset, uri.end, + '\'package:angular/angular_static.dart\''); + } + } + + var dynamicToStatic = new _NgDynamicToStaticVisitor( + dynamicApp.unnamedConstructor, transaction); + unit.accept(dynamicToStatic); + + var generatedFilePrefix = '${path.url.basenameWithoutExtension(id.path)}'; + _addImport(transaction, unit, + '${generatedFilePrefix}_static_expressions.dart', + 'generated_static_expressions'); + _addImport(transaction, unit, + '${generatedFilePrefix}_static_metadata.dart', + 'generated_static_metadata'); + _addImport(transaction, unit, + '${generatedFilePrefix}_static_injector.dart', + 'generated_static_injector'); + + var printer = transaction.commit(); + var url = id.path.startsWith('lib/') + ? 'package:${id.package}/${id.path.substring(4)}' : id.path; + printer.build(url); + transform.addOutput(new Asset.fromString(id, printer.text)); + } +} + +/// Injects an import into the list of imports in the file. +void _addImport(TextEditTransaction transaction, CompilationUnit unit, + String uri, String prefix) { + var last = unit.directives.where((d) => d is ImportDirective).last; + transaction.edit(last.end, last.end, '\nimport \'$uri\' as $prefix;'); +} + +class _NgDynamicToStaticVisitor extends GeneralizingASTVisitor { + final ConstructorElement constructor; + final TextEditTransaction transaction; + _NgDynamicToStaticVisitor(this.constructor, this.transaction); + + visitInstanceCreationExpression(InstanceCreationExpression node) { + if (node.staticElement == constructor) { + var ctorName = node.constructorName; + var ctorStr = ctorName.toString(); + var prefix = ctorStr.indexOf('.') == -1 ? '' : + ctorStr.substring(0, ctorStr.indexOf('.') + 1); + + transaction.edit(ctorName.beginToken.offset, ctorName.end, + '${prefix}NgStaticApp'); + + var args = node.argumentList; + transaction.edit(args.beginToken.offset + 1, args.end - 1, + 'generated_static_injector.factories, ' + 'generated_static_metadata.typeAnnotations, ' + 'generated_static_expressions.getters, ' + 'new generated_static_expressions.StaticClosureMap()'); + } + return super.visitNode(node); + } +} diff --git a/lib/transformer.dart b/lib/transformer.dart index 977c8cbb5..43a2648ed 100644 --- a/lib/transformer.dart +++ b/lib/transformer.dart @@ -3,6 +3,7 @@ library angular.transformer; import 'dart:io'; import 'package:angular/tools/transformer/expression_generator.dart'; import 'package:angular/tools/transformer/metadata_generator.dart'; +import 'package:angular/tools/transformer/static_angular_generator.dart'; import 'package:angular/tools/transformer/options.dart'; import 'package:barback/barback.dart'; import 'package:code_transformers/resolver.dart'; @@ -38,8 +39,11 @@ TransformOptions _parseSettings(Map args) { 'angular.core.NgFilter']; annotations.addAll(_readStringListValue(args, 'injectable_annotations')); - var injectedTypes = ['perf_api.Profiler', - 'angular.core.parser.static_parser.StaticParser']; + var injectedTypes = [ + 'perf_api.Profiler', + 'angular.core.RootScope', + 'angular.core.AstParser', + 'angular.core.dom.NgAnimate']; injectedTypes.addAll(_readStringListValue(args, 'injected_types')); var sdkDir = _readStringValue(args, 'dart_sdk', required: false); @@ -48,16 +52,16 @@ TransformOptions _parseSettings(Map args) { sdkDir = path.dirname(path.dirname(Platform.executable)); } - var dartEntry = _readStringValue(args, 'dart_entry'); + var dartEntries = _readStringListValue(args, 'dart_entries'); var diOptions = new di.TransformOptions( - dartEntries: [dartEntry], + dartEntries: dartEntries, injectableAnnotations: annotations, injectedTypes: injectedTypes, sdkDirectory: sdkDir); return new TransformOptions( - dartEntry: dartEntry, + dartEntries: dartEntries, htmlFiles: _readStringListValue(args, 'html_files'), sdkDirectory: sdkDir, templateUriRewrites: _readStringMapValue(args, 'template_uri_rewrites'), @@ -125,5 +129,6 @@ List> _createPhases(TransformOptions options) { [new ExpressionGenerator(options, resolvers)], [new di.InjectorGenerator(options.diOptions, resolvers)], [new MetadataGenerator(options, resolvers)], + [new StaticAngularGenerator(options, resolvers)], ]; } diff --git a/test/_specs.dart b/test/_specs.dart index f6f63d8d0..0896ac1e6 100644 --- a/test/_specs.dart +++ b/test/_specs.dart @@ -4,6 +4,7 @@ import 'dart:html'; import 'package:unittest/unittest.dart' as unit; import 'package:angular/angular.dart'; import 'package:angular/mock/module.dart'; +import 'package:angular/mock/test_injection.dart'; import 'package:collection/wrappers.dart' show DelegatingList; import 'jasmine_syntax.dart'; @@ -16,6 +17,7 @@ export 'package:di/dynamic_injector.dart'; export 'package:angular/angular.dart'; export 'package:angular/animate/module.dart'; export 'package:angular/mock/module.dart'; +export 'package:angular/mock/test_injection.dart'; export 'package:perf_api/perf_api.dart'; es(String html) { diff --git a/test/jasmine_syntax.dart b/test/jasmine_syntax.dart index 13def2068..8e1bfa48d 100644 --- a/test/jasmine_syntax.dart +++ b/test/jasmine_syntax.dart @@ -7,9 +7,9 @@ Function _wrapFn; _maybeWrapFn(fn) => () { if (_wrapFn != null) { - _wrapFn(fn)(); + return _wrapFn(fn)(); } else { - fn(); + return fn(); } }; diff --git a/test/tools/transformer/all.dart b/test/tools/transformer/all.dart new file mode 100644 index 000000000..fa81732f4 --- /dev/null +++ b/test/tools/transformer/all.dart @@ -0,0 +1,11 @@ +library all_transformer_tests; + +import 'expression_generator_spec.dart' as expression_generator_spec; +import 'metadata_generator_spec.dart' as metadata_generator_spec; +import 'static_angular_generator_spec.dart' as static_angular_generator_spec; + +main() { + expression_generator_spec.main(); + metadata_generator_spec.main(); + static_angular_generator_spec.main(); +} diff --git a/test/tools/transformer/expression_extractor_spec.dart b/test/tools/transformer/expression_generator_spec.dart similarity index 57% rename from test/tools/transformer/expression_extractor_spec.dart rename to test/tools/transformer/expression_generator_spec.dart index de7a68f9e..d36a15426 100644 --- a/test/tools/transformer/expression_extractor_spec.dart +++ b/test/tools/transformer/expression_generator_spec.dart @@ -7,10 +7,10 @@ import 'package:code_transformers/tests.dart' as tests; import '../../jasmine_syntax.dart'; main() { - describe('expression_extractor', () { + describe('ExpressionGenerator', () { var htmlFiles = []; var options = new TransformOptions( - dartEntry: 'web/main.dart', + dartEntries: ['web/main.dart'], htmlFiles: htmlFiles, sdkDirectory: dartSdkDirectory); var resolvers = new Resolvers(dartSdkDirectory); @@ -23,37 +23,34 @@ main() { htmlFiles.add('web/index.html'); return tests.applyTransformers(phases, inputs: { - 'angular|lib/auto_modules.dart': PACKAGE_AUTO, 'a|web/main.dart': ''' library foo; -import 'package:angular/auto_modules.dart'; ''', 'a|web/index.html': '''
{{some.getter}}
''' }, results: { - 'a|lib/generated_static_expressions.dart': ''' -$HEADER - Map _getters = { - r"some": (o) => o.some, - r"getter": (o) => o.getter - }; - Map _setters = { - r"some": (o, v) => o.some = v, - r"getter": (o, v) => o.getter = v - }; - List> _functions = []; -$FOOTER + 'a|web/main_static_expressions.dart': ''' +$header +final Map getters = { + r"some": (o) => o.some, + r"getter": (o) => o.getter +}; +final Map setters = { + r"some": (o, v) => o.some = v, + r"getter": (o, v) => o.getter = v +}; +final List> functions = []; ''' - }).then((_) { + }).whenComplete(() { htmlFiles.clear(); }); }); }); } -const String HEADER = ''' +const String header = ''' library a.web.main.generated_expressions; import 'package:angular/angular.dart'; @@ -62,20 +59,10 @@ import 'package:angular/core/parser/dynamic_parser.dart' show ClosureMap; Module get expressionModule => new Module() ..value(ClosureMap, new StaticClosureMap()); -class StaticClosureMap extends ClosureMap {'''; - -const String FOOTER = ''' - - Getter lookupGetter(String name) - => _getters[name]; - Setter lookupSetter(String name) - => _setters[name]; +class StaticClosureMap extends ClosureMap { + Getter lookupGetter(String name) => getters[name]; + Setter lookupSetter(String name) => setters[name]; lookupFunction(String name, int arity) - => (arity < _functions.length) ? _functions[arity][name] : null; -}'''; - -const String PACKAGE_AUTO = ''' -library angular.auto_modules; - -Module get defaultExpressionModule => new Module(); + => (arity < functions.length) ? functions[arity][name] : null; +} '''; diff --git a/test/tools/transformer/metadata_generator_spec.dart b/test/tools/transformer/metadata_generator_spec.dart index 69528c504..61e9ebe07 100644 --- a/test/tools/transformer/metadata_generator_spec.dart +++ b/test/tools/transformer/metadata_generator_spec.dart @@ -11,9 +11,9 @@ import 'package:code_transformers/tests.dart' as tests; import '../../jasmine_syntax.dart'; main() { - describe('metadata_generator', () { + describe('MetadataGenerator', () { var options = new TransformOptions( - dartEntry: 'web/main.dart', + dartEntries: ['web/main.dart'], sdkDirectory: dartSdkDirectory); var resolvers = new Resolvers(dartSdkDirectory); @@ -25,9 +25,8 @@ main() { it('should extract member metadata', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; @NgDirective(selector: r'[*=/{{.*}}/]') @@ -46,66 +45,104 @@ main() { ''' }, imports: [ - 'import \'package:a/a.dart\' as import_0;', + 'import \'main.dart\' as import_0;', 'import \'package:angular/angular.dart\' as import_1;', ], classes: { 'import_0.Engine': [ - 'const import_1.NgDirective(selector: \'[*=/{{.*}}/]\')', + 'const import_1.NgDirective(selector: \'[*=/{{.*}}/]\', map: const {' + '\'another-expression\': \'=>anotherExpression\', ' + '\'callback\': \'&callback\', ' + '\'two-way-stuff\': \'<=>twoWayStuff\'' + '})', 'proxy', ] - }, - classMembers: { - 'import_0.Engine': { - 'anotherExpression': 'const import_1.NgOneWay(\'another-expression\')', - 'callback': 'const import_1.NgCallback(\'callback\')', - 'twoWayStuff': 'const import_1.NgTwoWay(\'two-way-stuff\')', - } }); }); - it('should warn on un-importable files', () { + it('should warn on multiple annotations', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|web/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; - @NgDirective(selector: r'[*=/{{.*}}/]') - class Engine {} + class Engine { + @NgCallback('callback') + @NgOneWay('another-expression') + set callback(Function) {} + } ''' }, - messages: ['warning: Dropping annotations for Engine because the ' - 'containing file cannot be imported (must be in a lib folder). ' - '(a.dart 2 16)']); + messages: ['warning: callback can only have one annotation. ' + '(main.dart 3 18)']); }); - it('should warn on multiple annotations', () { + it('should warn on duplicated annotations', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; + @NgDirective(map: {'another-expression': '=>anotherExpression'}) class Engine { - @NgCallback('callback') @NgOneWay('another-expression') - set callback(Function) {} + set anotherExpression(Function) {} } ''' }, - messages: ['warning: callback can only have one annotation. ' - '(package:a/a.dart 3 18)']); + imports: [ + 'import \'main.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + 'const import_1.NgDirective(map: const {' + '\'another-expression\': \'=>anotherExpression\'})', + ] + }, + messages: ['warning: Directive @NgOneWay(\'another-expression\') ' + 'already contains an entry for \'another-expression\' ' + '(main.dart 2 16)' + ]); + }); + + it('should merge member annotations', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + @NgDirective(map: {'another-expression': '=>anotherExpression'}) + class Engine { + set anotherExpression(Function) {} + + set twoWayStuff(String abc) {} + @NgTwoWay('two-way-stuff') + String get twoWayStuff => null; + } + ''' + }, + imports: [ + 'import \'main.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + 'const import_1.NgDirective(map: const {' + '\'another-expression\': \'=>anotherExpression\', ' + '\'two-way-stuff\': \'<=>twoWayStuff\'})', + ] + }); }); it('should warn on multiple annotations (across getter/setter)', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; class Engine { @@ -118,15 +155,14 @@ main() { ''' }, messages: ['warning: callback can only have one annotation. ' - '(package:a/a.dart 3 18)']); + '(main.dart 3 18)']); }); it('should extract map arguments', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; @NgDirective(map: const {'ng-value': '&ngValue', 'key': 'value'}) @@ -134,7 +170,7 @@ main() { ''' }, imports: [ - 'import \'package:a/a.dart\' as import_0;', + 'import \'main.dart\' as import_0;', 'import \'package:angular/angular.dart\' as import_1;', ], classes: { @@ -147,9 +183,8 @@ main() { it('should extract list arguments', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; @NgDirective(publishTypes: const [TextChangeListener]) @@ -157,7 +192,7 @@ main() { ''' }, imports: [ - 'import \'package:a/a.dart\' as import_0;', + 'import \'main.dart\' as import_0;', 'import \'package:angular/angular.dart\' as import_1;', ], classes: { @@ -170,9 +205,8 @@ main() { it('should extract primitive literals', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; @NgOneWay(true) @@ -183,7 +217,7 @@ main() { ''' }, imports: [ - 'import \'package:a/a.dart\' as import_0;', + 'import \'main.dart\' as import_0;', 'import \'package:angular/angular.dart\' as import_1;', ], classes: { @@ -199,9 +233,8 @@ main() { it('should skip and warn on unserializable annotations', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; @Foo @@ -212,7 +245,7 @@ main() { ''' }, imports: [ - 'import \'package:a/a.dart\' as import_0;', + 'import \'main.dart\' as import_0;', 'import \'package:angular/angular.dart\' as import_1;', ], classes: { @@ -224,20 +257,17 @@ main() { ] }, messages: [ - 'warning: Unable to serialize annotation @Foo. ' - '(package:a/a.dart 2 16)', + 'warning: Unable to serialize annotation @Foo. (main.dart 2 16)', 'warning: Unable to serialize annotation ' - '@NgDirective(publishTypes: const [Foo]). ' - '(package:a/a.dart 5 16)', + '@NgDirective(publishTypes: const [Foo]). (main.dart 5 16)', ]); }); it('should extract types across libs', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; import 'package:a/b.dart'; @@ -249,7 +279,7 @@ main() { ''', }, imports: [ - 'import \'package:a/a.dart\' as import_0;', + 'import \'main.dart\' as import_0;', 'import \'package:angular/angular.dart\' as import_1;', 'import \'package:a/b.dart\' as import_2;', ], @@ -263,9 +293,8 @@ main() { it('should not gather non-member annotations', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; class Engine { @@ -281,9 +310,8 @@ main() { it('properly escapes strings', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': r''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': r''' import 'package:angular/angular.dart'; @NgOneWay('foo\' \\') @@ -292,7 +320,7 @@ main() { ''', }, imports: [ - 'import \'package:a/a.dart\' as import_0;', + 'import \'main.dart\' as import_0;', 'import \'package:angular/angular.dart\' as import_1;', ], classes: { @@ -305,9 +333,8 @@ main() { it('should reference static and global properties', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; @NgDirective(visibility: NgDirective.CHILDREN_VISIBILITY) @@ -318,7 +345,7 @@ main() { ''', }, imports: [ - 'import \'package:a/a.dart\' as import_0;', + 'import \'main.dart\' as import_0;', 'import \'package:angular/angular.dart\' as import_1;', ], classes: { @@ -332,9 +359,8 @@ main() { it('should not extract private annotations', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; @_Foo() @@ -349,18 +375,16 @@ main() { ''', }, messages: [ - 'warning: Annotation @_Foo() is not public. ' - '(package:a/a.dart 2 16)', - 'warning: Annotation @_foo is not public. (package:a/a.dart 2 16)', + 'warning: Annotation @_Foo() is not public. (main.dart 2 16)', + 'warning: Annotation @_foo is not public. (main.dart 2 16)', ]); }); it('supports named constructors', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; @Foo.bar() @@ -375,7 +399,7 @@ main() { ''', }, imports: [ - 'import \'package:a/a.dart\' as import_0;', + 'import \'main.dart\' as import_0;', ], classes: { 'import_0.Engine': [ @@ -384,7 +408,7 @@ main() { }, messages: [ 'warning: Annotation @Foo._private() is not public. ' - '(package:a/a.dart 2 16)', + '(main.dart 2 16)', ]); }); }); @@ -393,57 +417,46 @@ main() { Future generates(List> phases, {Map inputs, Iterable imports: const [], Map classes: const {}, - Map classMembers: const {}, Iterable messages: const []}) { var buffer = new StringBuffer(); - buffer.write('$HEADER\n'); + buffer.write('$header\n'); for (var i in imports) { buffer.write('$i\n'); } - buffer.write('$BOILER_PLATE\n'); + buffer.write('$boilerPlate\n'); for (var className in classes.keys) { - buffer.write(' $className: [\n'); + buffer.write(' $className: const [\n'); for (var annotation in classes[className]) { buffer.write(' $annotation,\n'); } buffer.write(' ],\n'); } - buffer.write('$MEMBER_PREAMBLE\n'); - for (var className in classMembers.keys) { - buffer.write(' $className: {\n'); - var members = classMembers[className]; - for (var memberName in members.keys) { - buffer.write(' \'$memberName\': ${members[memberName]},\n'); - } - buffer.write(' },\n'); - } - buffer.write('$FOOTER\n'); + buffer.write('$footer\n'); return tests.applyTransformers(phases, inputs: inputs, results: { - 'a|lib/generated_metadata.dart': buffer.toString() + 'a|web/main_static_metadata.dart': buffer.toString() }, messages: messages); } -const String HEADER = ''' +const String header = ''' library a.web.main.generated_metadata; -import 'package:angular/angular.dart' show AttrFieldAnnotation, FieldMetadataExtractor, MetadataExtractor; +import 'package:angular/angular.dart' show MetadataExtractor; import 'package:di/di.dart' show Module; '''; -const String BOILER_PLATE = ''' +const String boilerPlate = ''' Module get metadataModule => new Module() - ..value(MetadataExtractor, new _StaticMetadataExtractor()) - ..value(FieldMetadataExtractor, new _StaticFieldMetadataExtractor()); + ..value(MetadataExtractor, new _StaticMetadataExtractor()); class _StaticMetadataExtractor implements MetadataExtractor { Iterable call(Type type) { - var annotations = _classAnnotations[type]; + var annotations = typeAnnotations[type]; if (annotations != null) { return annotations; } @@ -451,32 +464,21 @@ class _StaticMetadataExtractor implements MetadataExtractor { } } -class _StaticFieldMetadataExtractor implements FieldMetadataExtractor { - Map call(Type type) { - var annotations = _memberAnnotations[type]; - if (annotations != null) { - return annotations; - } - return {}; - } -} - -final Map _classAnnotations = {'''; - -const String MEMBER_PREAMBLE = ''' -}; +final Map typeAnnotations = {'''; -final Map> _memberAnnotations = {'''; - -const String FOOTER = ''' +const String footer = ''' };'''; -const String PACKAGE_ANGULAR = ''' +const String libAngular = ''' library angular.core; -class NgDirective { - const NgDirective({selector, publishTypes, map, visibility}); +class NgAnnotation { + NgAnnotation({map: const {}}); +} + +class NgDirective extends NgAnnotation { + const NgDirective({selector, publishTypes, map, visibility}) : super(map: map); static const int CHILDREN_VISIBILITY = 1; } diff --git a/test/tools/transformer/static_angular_generator_spec.dart b/test/tools/transformer/static_angular_generator_spec.dart new file mode 100644 index 000000000..c9ece2c3a --- /dev/null +++ b/test/tools/transformer/static_angular_generator_spec.dart @@ -0,0 +1,111 @@ +library angular.test.tools.transformer.metadata_generator_spec; + +import 'dart:async'; + +import 'package:angular/tools/transformer/options.dart'; +import 'package:angular/tools/transformer/static_angular_generator.dart'; +import 'package:barback/barback.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:code_transformers/tests.dart' as tests; + +import '../../jasmine_syntax.dart'; + +main() { + describe('StaticAngularGenerator', () { + var options = new TransformOptions( + dartEntries: ['web/main.dart'], + sdkDirectory: dartSdkDirectory); + + var resolvers = new Resolvers(dartSdkDirectory); + + var phases = [ + [new StaticAngularGenerator(options, resolvers)] + ]; + + it('should modify NgDynamicApp', () { + return tests.applyTransformers(phases, + inputs: { + 'angular|lib/angular_dynamic.dart': libAngularDynamic, + 'di|lib/di.dart': libDI, + 'a|web/main.dart': ''' +import 'package:angular/angular_dynamic.dart'; +import 'package:di/di.dart' show Module; + +class MyModule extends Module {} + +main() { + var app = new NgDynamicApp() + .addModule(new MyModule()) + .run(); +} +''' + }, + results: { + 'a|web/main.dart': ''' +import 'package:angular/angular_static.dart'; +import 'package:di/di.dart' show Module; +import 'main_static_expressions.dart' as generated_static_expressions; +import 'main_static_metadata.dart' as generated_static_metadata; +import 'main_static_injector.dart' as generated_static_injector; + +class MyModule extends Module {} + +main() { + var app = new NgStaticApp(generated_static_injector.factories, generated_static_metadata.typeAnnotations, generated_static_expressions.getters, new generated_static_expressions.StaticClosureMap()) + .addModule(new MyModule()) + .run(); +} +''' + }); + }); + + it('handles prefixed app imports', () { + return tests.applyTransformers(phases, + inputs: { + 'angular|lib/angular_dynamic.dart': libAngularDynamic, + 'di|lib/di.dart': libDI, + 'a|web/main.dart': ''' +import 'package:angular/angular_dynamic.dart' as ng; +import 'package:di/di.dart' show Module; + +class MyModule extends Module {} + +main() { + var app = new ng.NgDynamicApp() + .addModule(new MyModule()) + .run(); +} +''' + }, + results: { + 'a|web/main.dart': ''' +import 'package:angular/angular_static.dart' as ng; +import 'package:di/di.dart' show Module; +import 'main_static_expressions.dart' as generated_static_expressions; +import 'main_static_metadata.dart' as generated_static_metadata; +import 'main_static_injector.dart' as generated_static_injector; + +class MyModule extends Module {} + +main() { + var app = new ng.NgStaticApp(generated_static_injector.factories, generated_static_metadata.typeAnnotations, generated_static_expressions.getters, new generated_static_expressions.StaticClosureMap()) + .addModule(new MyModule()) + .run(); +} +''' + }); + }); + }); +} + + + +const String libAngularDynamic = ''' +library angular.dynamic + +class NgDynamicApp {}; +'''; + +const String libDI = ''' +class Module {} +'''; From 128044bfe09a3ac49143064aec3fe43ac06a5993 Mon Sep 17 00:00:00 2001 From: Pete Blois Date: Mon, 17 Mar 2014 12:54:13 -0700 Subject: [PATCH 4/4] Fixing expression extraction to include all functions Was previously dropping functions with the max arity because of an off-by-one error in the function map generation. --- lib/tools/parser_getter_setter/generator.dart | 4 ++-- lib/tools/transformer/expression_generator.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/tools/parser_getter_setter/generator.dart b/lib/tools/parser_getter_setter/generator.dart index b33244e10..b58c241c3 100644 --- a/lib/tools/parser_getter_setter/generator.dart +++ b/lib/tools/parser_getter_setter/generator.dart @@ -58,7 +58,7 @@ class StaticClosureMap extends ClosureMap { => _getters[name]; Setter lookupSetter(String name) => _setters[name]; - lookupFunction(String name, int arity) + lookupFunction(String name, int arity) => (arity < _functions.length) ? _functions[arity][name] : null; } '''; @@ -84,7 +84,7 @@ class StaticClosureMap extends ClosureMap { var maxArity = arities.keys.reduce((x, y) => max(x, y)); - var maps = new Iterable.generate(maxArity, (arity) { + var maps = new Iterable.generate(maxArity + 1, (arity) { var names = arities[arity]; if (names == null) { return '{\n }'; diff --git a/lib/tools/transformer/expression_generator.dart b/lib/tools/transformer/expression_generator.dart index 94f94a9d4..2b3a9b284 100644 --- a/lib/tools/transformer/expression_generator.dart +++ b/lib/tools/transformer/expression_generator.dart @@ -170,7 +170,7 @@ final List> functions = ${generateFunctionMap(calls)}; var maxArity = arities.isEmpty ? 0 : arities.keys.reduce((x, y) => math.max(x, y)); - var maps = new Iterable.generate(maxArity, (arity) { + var maps = new Iterable.generate(maxArity + 1, (arity) { var names = arities[arity]; if (names == null) { return '{\n }';