feat(dart/transform): Generate ChangeDetector classes

Use the `ProtoViewDto` created by the render `Compiler` to create a
`ChangeDetectorDefinition`.

From there, generate a subclass of `AbstractChangeDetector` for each
`ChangeDetectorDefinition`.

Run some basic unit tests for the dynamic and JIT change detectors on
pre-generated change detectors.
This commit is contained in:
Tim Blasi
2015-05-14 13:14:45 -07:00
parent 383f0a1f30
commit 8a3b0b366f
15 changed files with 1105 additions and 217 deletions

View File

@ -44,8 +44,120 @@ import {
ProtoChangeDetector
} from 'angular2/change_detection';
import {getDefinition} from './simple_watch_config';
import {getFactoryById} from './generated/simple_watch_classes';
export function main() {
// These tests also run against pre-generated Dart Change Detectors. We will move tests up from
// the loop below as they are converted.
ListWrapper.forEach(['dynamic', 'JIT', 'Pregen'], (cdType) => {
if (cdType == "JIT" && IS_DARTIUM) return;
if (cdType == "Pregen" && !IS_DARTIUM) return;
describe(`${cdType} Change Detector`, () => {
function _getProtoChangeDetector(def: ChangeDetectorDefinition) {
var registry = null;
switch (cdType) {
case 'dynamic':
return new DynamicProtoChangeDetector(registry, def);
case 'JIT':
return new JitProtoChangeDetector(registry, def);
case 'Pregen':
return getFactoryById(def.id)(registry, def);
default:
return null;
}
}
function _bindConstValue(expression: string) {
var dispatcher = new TestDispatcher();
var protoCd = _getProtoChangeDetector(getDefinition(expression, 'propName'));
var cd = protoCd.instantiate(dispatcher);
var context = null;
var locals = null;
cd.hydrate(context, locals, null);
cd.detectChanges();
return dispatcher.log;
}
it('should support literals',
() => { expect(_bindConstValue('10')).toEqual(['propName=10']); });
it('should strip quotes from literals',
() => { expect(_bindConstValue('"str"')).toEqual(['propName=str']); });
it('should support newlines in literals',
() => { expect(_bindConstValue('"a\n\nb"')).toEqual(['propName=a\n\nb']); });
it('should support + operations',
() => { expect(_bindConstValue('10 + 2')).toEqual(['propName=12']); });
it('should support - operations',
() => { expect(_bindConstValue('10 - 2')).toEqual(['propName=8']); });
it('should support * operations',
() => { expect(_bindConstValue('10 * 2')).toEqual(['propName=20']); });
it('should support / operations', () => {
expect(_bindConstValue('10 / 2')).toEqual([`propName=${5.0}`]);
}); // dart exp=5.0, js exp=5
it('should support % operations',
() => { expect(_bindConstValue('11 % 2')).toEqual(['propName=1']); });
it('should support == operations on identical',
() => { expect(_bindConstValue('1 == 1')).toEqual(['propName=true']); });
it('should support != operations',
() => { expect(_bindConstValue('1 != 1')).toEqual(['propName=false']); });
it('should support == operations on coerceible', () => {
var expectedValue = IS_DARTIUM ? 'false' : 'true';
expect(_bindConstValue('1 == true')).toEqual([`propName=${expectedValue}`]);
});
it('should support === operations on identical',
() => { expect(_bindConstValue('1 === 1')).toEqual(['propName=true']); });
it('should support !== operations',
() => { expect(_bindConstValue('1 !== 1')).toEqual(['propName=false']); });
it('should support === operations on coerceible',
() => { expect(_bindConstValue('1 === true')).toEqual(['propName=false']); });
it('should support true < operations',
() => { expect(_bindConstValue('1 < 2')).toEqual(['propName=true']); });
it('should support false < operations',
() => { expect(_bindConstValue('2 < 1')).toEqual(['propName=false']); });
it('should support false > operations',
() => { expect(_bindConstValue('1 > 2')).toEqual(['propName=false']); });
it('should support true > operations',
() => { expect(_bindConstValue('2 > 1')).toEqual(['propName=true']); });
it('should support true <= operations',
() => { expect(_bindConstValue('1 <= 2')).toEqual(['propName=true']); });
it('should support equal <= operations',
() => { expect(_bindConstValue('2 <= 2')).toEqual(['propName=true']); });
it('should support false <= operations',
() => { expect(_bindConstValue('2 <= 1')).toEqual(['propName=false']); });
it('should support true >= operations',
() => { expect(_bindConstValue('2 >= 1')).toEqual(['propName=true']); });
it('should support equal >= operations',
() => { expect(_bindConstValue('2 >= 2')).toEqual(['propName=true']); });
it('should support false >= operations',
() => { expect(_bindConstValue('1 >= 2')).toEqual(['propName=false']); });
it('should support true && operations',
() => { expect(_bindConstValue('true && true')).toEqual(['propName=true']); });
it('should support false && operations',
() => { expect(_bindConstValue('true && false')).toEqual(['propName=false']); });
it('should support true || operations',
() => { expect(_bindConstValue('true || false')).toEqual(['propName=true']); });
it('should support false || operations',
() => { expect(_bindConstValue('false || false')).toEqual(['propName=false']); });
it('should support negate',
() => { expect(_bindConstValue('!true')).toEqual(['propName=false']); });
it('should support double negate',
() => { expect(_bindConstValue('!!true')).toEqual(['propName=true']); });
it('should support true conditionals',
() => { expect(_bindConstValue('1 < 2 ? 1 : 2')).toEqual(['propName=1']); });
it('should support false conditionals',
() => { expect(_bindConstValue('1 > 2 ? 1 : 2')).toEqual(['propName=2']); });
it('should support keyed access to a list item',
() => { expect(_bindConstValue('["foo", "bar"][0]')).toEqual(['propName=foo']); });
it('should support keyed access to a map item',
() => { expect(_bindConstValue('{"foo": "bar"}["foo"]')).toEqual(['propName=bar']); });
});
});
describe("change detection", () => {
StringMapWrapper.forEach(
{
@ -145,12 +257,6 @@ export function main() {
expect(executeWatch('a', 'a', td)).toEqual(['a=null']);
});
it("should support literals", () => {
expect(executeWatch('const', '10')).toEqual(['const=10']);
expect(executeWatch('const', '"str"')).toEqual(['const=str']);
expect(executeWatch('const', '"a\n\nb"')).toEqual(['const=a\n\nb']);
});
it('should support simple chained property access', () => {
var address = new Address('Grenoble');
var person = new Person('Victor', address);
@ -207,68 +313,6 @@ export function main() {
expect(c["dispatcher"].loggedValues[0]['z']).toEqual(1);
});
it("should support binary operations", () => {
expect(executeWatch('exp', '10 + 2')).toEqual(['exp=12']);
expect(executeWatch('exp', '10 - 2')).toEqual(['exp=8']);
expect(executeWatch('exp', '10 * 2')).toEqual(['exp=20']);
expect(executeWatch('exp', '10 / 2'))
.toEqual([`exp=${5.0}`]); // dart exp=5.0, js exp=5
expect(executeWatch('exp', '11 % 2')).toEqual(['exp=1']);
expect(executeWatch('exp', '1 == 1')).toEqual(['exp=true']);
if (IS_DARTIUM) {
expect(executeWatch('exp', '1 == "1"')).toEqual(['exp=false']);
} else {
expect(executeWatch('exp', '1 == "1"')).toEqual(['exp=true']);
}
expect(executeWatch('exp', '1 != 1')).toEqual(['exp=false']);
expect(executeWatch('exp', '1 === 1')).toEqual(['exp=true']);
expect(executeWatch('exp', '1 !== 1')).toEqual(['exp=false']);
expect(executeWatch('exp', '1 === "1"')).toEqual(['exp=false']);
expect(executeWatch('exp', '1 < 2')).toEqual(['exp=true']);
expect(executeWatch('exp', '2 < 1')).toEqual(['exp=false']);
expect(executeWatch('exp', '2 > 1')).toEqual(['exp=true']);
expect(executeWatch('exp', '2 < 1')).toEqual(['exp=false']);
expect(executeWatch('exp', '1 <= 2')).toEqual(['exp=true']);
expect(executeWatch('exp', '2 <= 2')).toEqual(['exp=true']);
expect(executeWatch('exp', '2 <= 1')).toEqual(['exp=false']);
expect(executeWatch('exp', '2 >= 1')).toEqual(['exp=true']);
expect(executeWatch('exp', '2 >= 2')).toEqual(['exp=true']);
expect(executeWatch('exp', '1 >= 2')).toEqual(['exp=false']);
expect(executeWatch('exp', 'true && true')).toEqual(['exp=true']);
expect(executeWatch('exp', 'true && false')).toEqual(['exp=false']);
expect(executeWatch('exp', 'true || false')).toEqual(['exp=true']);
expect(executeWatch('exp', 'false || false')).toEqual(['exp=false']);
});
it("should support negate", () => {
expect(executeWatch('exp', '!true')).toEqual(['exp=false']);
expect(executeWatch('exp', '!!true')).toEqual(['exp=true']);
});
it("should support conditionals", () => {
expect(executeWatch('m', '1 < 2 ? 1 : 2')).toEqual(['m=1']);
expect(executeWatch('m', '1 > 2 ? 1 : 2')).toEqual(['m=2']);
});
describe("keyed access", () => {
it("should support accessing a list item", () => {
expect(executeWatch('array[0]', '["foo", "bar"][0]')).toEqual(['array[0]=foo']);
});
it("should support accessing a map item", () => {
expect(executeWatch('map[foo]', '{"foo": "bar"}["foo"]')).toEqual(['map[foo]=bar']);
});
});
it("should support interpolation", () => {
var ast = parser.parseInterpolation("B{{a}}A", "location");
var pcd = createProtoChangeDetector([BindingRecord.createForElement(ast, 0, "memo")]);

View File

@ -0,0 +1,5 @@
// Ignore me, needed to support Angular 2 Dart.
export function getFactoryById(id: string) {
return null;
}

View File

@ -0,0 +1,36 @@
library angular2.src.transform.di_transformer;
import 'dart:convert';
import 'dart:io';
import 'package:dart_style/dart_style.dart';
import 'package:angular2/src/transform/template_compiler/change_detector_codegen.dart';
import '../simple_watch_config.dart';
void main(List<String> args) {
var buf = new StringBuffer('var $_MAP_NAME = {');
var codegen = new Codegen();
var allDefs = getAllDefinitions('propName');
for (var i = 0; i < allDefs.length; ++i) {
var className = 'ChangeDetector${i}';
codegen.generate(className, allDefs[i]);
if (i > 0) {
buf.write(',');
}
buf.write(" '''${allDefs[i].id}''': "
"$className.$PROTO_CHANGE_DETECTOR_FACTORY_METHOD");
}
buf.write('};');
print(new DartFormatter().format('''
library dart_gen_change_detectors;
${codegen.imports}
$codegen
$buf
getFactoryById(String id) => $_MAP_NAME[id];
'''));
}
const _MAP_NAME = '_idToProtoMap';

View File

@ -0,0 +1,72 @@
import {ListWrapper} from 'angular2/src/facade/collection';
import {BindingRecord, ChangeDetectorDefinition, Lexer, Parser} from 'angular2/change_detection';
var _parser = new Parser(new Lexer());
function _createChangeDetectorDefinition(id: string, propName: string,
expression: string): ChangeDetectorDefinition {
var ast = _parser.parseBinding(expression, 'location');
var bindingRecords = [BindingRecord.createForElement(ast, 0, propName)];
var strategy = null;
var variableBindings = [];
var directiveRecords = [];
return new ChangeDetectorDefinition(id, strategy, variableBindings, bindingRecords,
directiveRecords);
}
/**
* In this case, we expect `id` and `expression` to be the same string.
*/
export function getDefinition(id: string, propName: string): ChangeDetectorDefinition {
// TODO(kegluneq): Remove `propName`?
if (ListWrapper.indexOf(_availableDefinitions, id) < 0) {
throw `No ChangeDetectorDefinition for ${id} available. Please modify this file if necessary.`;
}
return _createChangeDetectorDefinition(id, propName, id);
}
/**
* Get all available ChangeDetectorDefinition objects. Used to pre-generate Dart
* `ChangeDetector` classes.
*/
export function getAllDefinitions(propName: string): List<ChangeDetectorDefinition> {
return ListWrapper.map(_availableDefinitions, (id) => getDefinition(id, propName));
}
var _availableDefinitions = [
'10',
'"str"',
'"a\n\nb"',
'10 + 2',
'10 - 2',
'10 * 2',
'10 / 2',
'11 % 2',
'1 == 1',
'1 != 1',
'1 == true',
'1 === 1',
'1 !== 1',
'1 === true',
'1 < 2',
'2 < 1',
'1 > 2',
'2 > 1',
'1 <= 2',
'2 <= 2',
'2 <= 1',
'2 >= 1',
'2 >= 2',
'1 >= 2',
'true && true',
'true && false',
'true || false',
'false || false',
'!true',
'!!true',
'1 < 2 ? 1 : 2',
'1 > 2 ? 1 : 2',
'["foo", "bar"][0]',
'{"foo": "bar"}["foo"]'
];

View File

@ -1,5 +1,8 @@
library bar.ng_deps.dart;
import 'package:angular2/src/change_detection/pregen_proto_change_detector.dart'
as _gen;
import 'bar.dart';
import 'package:angular2/src/core/annotations_impl/annotations.dart';
import 'package:angular2/src/core/annotations_impl/view.dart';
@ -18,3 +21,48 @@ void initReflector(reflector) {
]
});
}
class _MyComponent_ChangeDetector0 extends _gen.AbstractChangeDetector {
final dynamic _dispatcher;
final _gen.PipeRegistry _pipeRegistry;
final _gen.List<_gen.ProtoRecord> _protos;
final _gen.List<_gen.DirectiveRecord> _directiveRecords;
dynamic _locals = null;
dynamic _context = _gen.ChangeDetectionUtil.uninitialized();
_MyComponent_ChangeDetector0(this._dispatcher, this._pipeRegistry,
this._protos, this._directiveRecords)
: super();
void detectChangesInRecords(throwOnChange) {
var context = null;
var change_context = false;
var isChanged = false;
var currentProto;
var changes = null;
context = _context;
}
void callOnAllChangesDone() {}
void hydrate(context, locals, directives) {
mode = 'ALWAYS_CHECK';
_context = context;
_locals = locals;
}
void dehydrate() {
_context = _gen.ChangeDetectionUtil.uninitialized();
_locals = null;
}
hydrated() =>
!_gen.looseIdentical(_context, _gen.ChangeDetectionUtil.uninitialized());
static _gen.ProtoChangeDetector newProtoChangeDetector(
_gen.PipeRegistry registry, _gen.ChangeDetectorDefinition def) {
return new _gen.PregenProtoChangeDetector(
(a, b, c, d) => new _MyComponent_ChangeDetector0(a, b, c, d), registry,
def);
}
}

View File

@ -1,5 +1,6 @@
library angular2.test.transform.template_compiler.all_tests;
import 'dart:async';
import 'package:barback/barback.dart';
import 'package:angular2/src/dom/html_adapter.dart';
import 'package:angular2/src/transform/common/asset_reader.dart';
@ -18,71 +19,79 @@ void allTests() {
beforeEach(() => setLogger(new PrintLogger()));
it('should parse simple expressions in inline templates.', () async {
var inputPath =
'template_compiler/inline_expression_files/hello.ng_deps.dart';
var expected = readFile(
'template_compiler/inline_expression_files/expected/hello.ng_deps.dart');
var output = await processTemplates(reader, new AssetId('a', inputPath));
_formatThenExpectEquals(output, expected);
});
describe('registrations', () {
Future<String> process(AssetId assetId) =>
processTemplates(reader, assetId, generateChangeDetectors: false);
it('should parse simple methods in inline templates.', () async {
var inputPath = 'template_compiler/inline_method_files/hello.ng_deps.dart';
var expected = readFile(
'template_compiler/inline_method_files/expected/hello.ng_deps.dart');
var output = await processTemplates(reader, new AssetId('a', inputPath));
_formatThenExpectEquals(output, expected);
});
it('should parse simple expressions in inline templates.', () async {
var inputPath =
'template_compiler/inline_expression_files/hello.ng_deps.dart';
var expected = readFile(
'template_compiler/inline_expression_files/expected/hello.ng_deps.dart');
var output = await process(new AssetId('a', inputPath));
_formatThenExpectEquals(output, expected);
});
it('should parse simple expressions in linked templates.', () async {
var inputPath = 'template_compiler/url_expression_files/hello.ng_deps.dart';
var expected = readFile(
'template_compiler/url_expression_files/expected/hello.ng_deps.dart');
var output = await processTemplates(reader, new AssetId('a', inputPath));
_formatThenExpectEquals(output, expected);
});
it('should parse simple methods in inline templates.', () async {
var inputPath =
'template_compiler/inline_method_files/hello.ng_deps.dart';
var expected = readFile(
'template_compiler/inline_method_files/expected/hello.ng_deps.dart');
var output = await process(new AssetId('a', inputPath));
_formatThenExpectEquals(output, expected);
});
it('should parse simple methods in linked templates.', () async {
var inputPath = 'template_compiler/url_method_files/hello.ng_deps.dart';
var expected = readFile(
'template_compiler/url_method_files/expected/hello.ng_deps.dart');
var output = await processTemplates(reader, new AssetId('a', inputPath));
_formatThenExpectEquals(output, expected);
});
it('should parse simple expressions in linked templates.', () async {
var inputPath =
'template_compiler/url_expression_files/hello.ng_deps.dart';
var expected = readFile(
'template_compiler/url_expression_files/expected/hello.ng_deps.dart');
var output = await process(new AssetId('a', inputPath));
_formatThenExpectEquals(output, expected);
});
it('should not generated duplicate getters/setters', () async {
var inputPath = 'template_compiler/duplicate_files/hello.ng_deps.dart';
var expected = readFile(
'template_compiler/duplicate_files/expected/hello.ng_deps.dart');
var output = await processTemplates(reader, new AssetId('a', inputPath));
_formatThenExpectEquals(output, expected);
});
it('should parse simple methods in linked templates.', () async {
var inputPath = 'template_compiler/url_method_files/hello.ng_deps.dart';
var expected = readFile(
'template_compiler/url_method_files/expected/hello.ng_deps.dart');
var output = await process(new AssetId('a', inputPath));
_formatThenExpectEquals(output, expected);
});
it('should parse `View` directives with a single dependency.', () async {
var inputPath = 'template_compiler/one_directive_files/hello.ng_deps.dart';
var expected = readFile(
'template_compiler/one_directive_files/expected/hello.ng_deps.dart');
it('should not generated duplicate getters/setters', () async {
var inputPath = 'template_compiler/duplicate_files/hello.ng_deps.dart';
var expected = readFile(
'template_compiler/duplicate_files/expected/hello.ng_deps.dart');
var output = await process(new AssetId('a', inputPath));
_formatThenExpectEquals(output, expected);
});
var output = await processTemplates(reader, new AssetId('a', inputPath));
_formatThenExpectEquals(output, expected);
});
it('should parse `View` directives with a single dependency.', () async {
var inputPath =
'template_compiler/one_directive_files/hello.ng_deps.dart';
var expected = readFile(
'template_compiler/one_directive_files/expected/hello.ng_deps.dart');
it('should parse `View` directives with a single prefixed dependency.',
() async {
var inputPath = 'template_compiler/with_prefix_files/hello.ng_deps.dart';
var expected = readFile(
'template_compiler/with_prefix_files/expected/hello.ng_deps.dart');
var output = await process(new AssetId('a', inputPath));
_formatThenExpectEquals(output, expected);
});
var output = await processTemplates(reader, new AssetId('a', inputPath));
_formatThenExpectEquals(output, expected);
it('should parse `View` directives with a single prefixed dependency.',
() async {
var inputPath = 'template_compiler/with_prefix_files/hello.ng_deps.dart';
var expected = readFile(
'template_compiler/with_prefix_files/expected/hello.ng_deps.dart');
inputPath = 'template_compiler/with_prefix_files/goodbye.ng_deps.dart';
expected = readFile(
'template_compiler/with_prefix_files/expected/goodbye.ng_deps.dart');
var output = await process(new AssetId('a', inputPath));
_formatThenExpectEquals(output, expected);
output = await processTemplates(reader, new AssetId('a', inputPath));
_formatThenExpectEquals(output, expected);
inputPath = 'template_compiler/with_prefix_files/goodbye.ng_deps.dart';
expected = readFile(
'template_compiler/with_prefix_files/expected/goodbye.ng_deps.dart');
output = await process(new AssetId('a', inputPath));
_formatThenExpectEquals(output, expected);
});
});
}