├── test ├── samples │ ├── packages │ ├── sandbox.dart │ ├── helloworld.dart │ ├── unittest.dart │ └── watchgroup.dart ├── annotations.dart └── test_runner.dart ├── change_detection ├── lib │ ├── watch_group_static.dart │ ├── watch_group_dynamic.dart │ ├── dirty_checking_change_detector_static.dart │ ├── dirty_checking_change_detector_dynamic.dart │ ├── prototype_map.dart │ ├── ast.dart │ ├── linked_list.dart │ ├── change_detection.dart │ └── watch_group.dart ├── pubspec.yaml ├── transpile ├── pubspec.lock └── test │ ├── watch_group_spec.dart │ └── dirty_checking_change_detector_spec.dart ├── scripts ├── travis │ ├── build.sh │ └── setup.sh └── env.sh ├── .travis.yml ├── pubspec.yaml ├── .gitignore ├── lib ├── visitor_test.dart ├── writer.dart ├── replacements.dart ├── transpiler.dart ├── visitor.dart ├── visitor_null.dart └── visitor_block.dart ├── LICENSE ├── README.md ├── pubspec.lock └── dart2es6 /test/samples/packages: -------------------------------------------------------------------------------- 1 | ../../packages -------------------------------------------------------------------------------- /test/samples/sandbox.dart: -------------------------------------------------------------------------------- 1 | add(b) { 2 | var a = new List(); 3 | a.add(1); 4 | return a; 5 | } 6 | -------------------------------------------------------------------------------- /change_detection/lib/watch_group_static.dart: -------------------------------------------------------------------------------- 1 | library watch_group_static; 2 | 3 | import 'watch_group.dart'; 4 | 5 | -------------------------------------------------------------------------------- /change_detection/lib/watch_group_dynamic.dart: -------------------------------------------------------------------------------- 1 | library watch_group_dynamic; 2 | 3 | import 'watch_group.dart'; 4 | 5 | -------------------------------------------------------------------------------- /change_detection/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: change_detection 2 | version: 0.0.0 3 | 4 | environment: 5 | sdk: '>=1.5.0' 6 | dependencies: 7 | dev_dependencies: 8 | guinness: '>=0.1.11' -------------------------------------------------------------------------------- /scripts/travis/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | . ./scripts/env.sh 4 | 5 | mkdir -p test/out/preprocessor test/out/traceur test/out/transpiler 6 | dart -c test/test_runner.dart 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | env: 5 | matrix: 6 | - JOB=unit-stable 7 | - JOB=unit-dev 8 | 9 | before_script: 10 | - ./scripts/travis/setup.sh 11 | 12 | script: 13 | - ./scripts/travis/build.sh 14 | -------------------------------------------------------------------------------- /test/samples/helloworld.dart: -------------------------------------------------------------------------------- 1 | library foo; 2 | 3 | class HelloWorld { 4 | final String greeting; 5 | String name = 'World'; 6 | 7 | HelloWorld(this.greeting); 8 | 9 | greet() { 10 | return greeting + name; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dart2es6 2 | version: 0.0.0 3 | authors: 4 | - Anting Shen 5 | description: Dart to ECMAScript 6 transpiler 6 | homepage: 7 | environment: 8 | sdk: '>=1.5.0' 9 | dependencies: 10 | analyzer: '0.21.1' 11 | code_transformers: 'any' 12 | args: '>=0.11.0' 13 | path: '>=1.0.0 <2.0.0' 14 | dev_dependencies: 15 | guinness: '>=0.1.11' 16 | -------------------------------------------------------------------------------- /test/annotations.dart: -------------------------------------------------------------------------------- 1 | class Annotation { 2 | final String type; 3 | const Annotation(this.type); 4 | } 5 | 6 | const Annotation 7 | describe = const Annotation('describe'), 8 | ddescribe = const Annotation('ddescribe'), 9 | xdescribe = const Annotation('xdescribe'), 10 | it = const Annotation('it'), 11 | iit = const Annotation('iit'), 12 | xit = const Annotation('xit'); 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | packages/ 3 | test/packages 4 | test/out 5 | test/assets/packages 6 | node_modules/ 7 | change_detection/out 8 | change_detection/packages 9 | change_detection/test/out 10 | change_detection/test/packages 11 | 12 | # Or the files created by dart2js. 13 | *.dart.js 14 | *.dart.precompiled.js 15 | *.js_ 16 | *.js.deps 17 | *.js.map 18 | 19 | # Include when developing application packages. 20 | 21 | .idea 22 | -------------------------------------------------------------------------------- /change_detection/lib/dirty_checking_change_detector_static.dart: -------------------------------------------------------------------------------- 1 | library dirty_checking_change_detector_static; 2 | 3 | import 'change_detection.dart'; 4 | 5 | class StaticFieldGetterFactory implements FieldGetterFactory { 6 | Map getters; 7 | 8 | StaticFieldGetterFactory(this.getters); 9 | 10 | FieldGetter getter(Object object, String name) { 11 | var getter = getters[name]; 12 | if (getter == null) throw "Missing getter: (o) => o.$name"; 13 | return getter; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /change_detection/transpile: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | mkdir -p out 5 | 6 | # dart -c ../dart2es6 test/dirty_checking_change_detector_spec.dart -o out/dirty_checking_change_detector_spec.js 7 | 8 | FILES="lib/change_detection.dart 9 | lib/dirty_checking_change_detector.dart 10 | lib/dirty_checking_change_detector_dynamic.dart 11 | lib/dirty_checking_change_detector_static.dart 12 | lib/watch_group.dart 13 | lib/watch_group_dynamic.dart 14 | lib/watch_group_static.dart" 15 | 16 | for i in $FILES 17 | do 18 | out=${i/lib/out} 19 | out=${out/dart/js} 20 | dart -c ../dart2es6 $i -o $out 21 | done 22 | -------------------------------------------------------------------------------- /change_detection/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See http://pub.dartlang.org/doc/glossary.html#lockfile 3 | packages: 4 | collection: 5 | description: collection 6 | source: hosted 7 | version: "0.9.4" 8 | guinness: 9 | description: guinness 10 | source: hosted 11 | version: "0.1.14" 12 | matcher: 13 | description: matcher 14 | source: hosted 15 | version: "0.11.1" 16 | path: 17 | description: path 18 | source: hosted 19 | version: "1.3.0" 20 | stack_trace: 21 | description: stack_trace 22 | source: hosted 23 | version: "1.0.2" 24 | unittest: 25 | description: unittest 26 | source: hosted 27 | version: "0.11.0+4" 28 | -------------------------------------------------------------------------------- /scripts/env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ -n "$DART_SDK" ]; then 5 | DARTSDK=$DART_SDK 6 | else 7 | echo "sdk=== $DARTSDK" 8 | DART=`which dart|cat` # pipe to cat to ignore the exit code 9 | DARTSDK=`which dart | sed -e 's/\/bin\/dart$/\//'` 10 | if [ -z "$DARTSDK" ]; then 11 | DARTSDK="`pwd`/dart-sdk" 12 | fi 13 | fi 14 | 15 | export DART_SDK="$DARTSDK" 16 | export DART=${DART:-"$DARTSDK/bin/dart"} 17 | export PUB=${PUB:-"$DARTSDK/bin/pub"} 18 | export DARTANALYZER=${DARTANALYZER:-"$DARTSDK/bin/dartanalyzer"} 19 | export DARTDOC=${DARTDOC:-"$DARTSDK/bin/dartdoc"} 20 | 21 | 22 | export DART_FLAGS='--enable_type_checks --enable_asserts' 23 | export PATH=$PATH:$DARTSDK/bin -------------------------------------------------------------------------------- /change_detection/lib/dirty_checking_change_detector_dynamic.dart: -------------------------------------------------------------------------------- 1 | library dirty_checking_change_detector_dynamic; 2 | 3 | import 'change_detection.dart'; 4 | export 'change_detection.dart' show 5 | FieldGetterFactory; 6 | 7 | /** 8 | * We are using mirrors, but there is no need to import anything. 9 | */ 10 | @MirrorsUsed(targets: const [ DynamicFieldGetterFactory ], metaTargets: const [] ) 11 | import 'dart:mirrors'; 12 | 13 | class DynamicFieldGetterFactory implements FieldGetterFactory { 14 | FieldGetter getter(Object object, String name) { 15 | Symbol symbol = new Symbol(name); 16 | InstanceMirror instanceMirror = reflect(object); 17 | return (Object object) => instanceMirror.getField(symbol).reflectee; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /scripts/travis/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | case $( uname -s ) in 6 | Linux) 7 | DART_SDK_ZIP=dartsdk-linux-x64-release.zip 8 | ;; 9 | Darwin) 10 | DART_SDK_ZIP=dartsdk-macos-x64-release.zip 11 | ;; 12 | esac 13 | 14 | npm install -g traceur 15 | echo ++++++++++++++++++++++++++++++++++++++++ 16 | 17 | CHANNEL=`echo $JOB | cut -f 2 -d -` 18 | echo Fetch Dart channel: $CHANNEL 19 | 20 | echo http://storage.googleapis.com/dart-archive/channels/$CHANNEL/release/latest/sdk/$DART_SDK_ZIP 21 | curl -L http://storage.googleapis.com/dart-archive/channels/$CHANNEL/release/latest/sdk/$DART_SDK_ZIP > $DART_SDK_ZIP 22 | echo Fetched new dart version $(unzip -p $DART_SDK_ZIP dart-sdk/version) 23 | rm -rf dart-sdk 24 | unzip $DART_SDK_ZIP > /dev/null 25 | rm $DART_SDK_ZIP 26 | 27 | . ./scripts/env.sh 28 | $DART --version 29 | $PUB install 30 | -------------------------------------------------------------------------------- /lib/visitor_test.dart: -------------------------------------------------------------------------------- 1 | part of dart2es6.visitor; 2 | 3 | class TestVisitor extends MainVisitor { 4 | TestVisitor(this.path); 5 | 6 | visitClassDeclaration(ClassDeclaration node) { 7 | try { 8 | return super.visitClassDeclaration(node); 9 | } catch (e, s) { 10 | print("Transpilation failed & skipped for class `${node.name.name}`. Which threw:\n$e$s"); 11 | exitCode = 1; 12 | return "// class ${node.name.name} threw ${e.toString().split('\n')[0]}"; 13 | } 14 | } 15 | 16 | visitFunctionDeclaration(FunctionDeclaration node) { 17 | try { 18 | return super.visitFunctionDeclaration(node); 19 | } catch (e, s) { 20 | print("Transpilation failed & skipped for top level function `${node.name.name}`. " 21 | "Which threw:\n$e$s"); 22 | exitCode = 1; 23 | return "// function ${node.name.name} threw ${e.toString().split('\n')[0]}"; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Google, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /change_detection/lib/prototype_map.dart: -------------------------------------------------------------------------------- 1 | part of angular.watch_group; 2 | 3 | class PrototypeMap implements Map { 4 | final Map prototype; 5 | final Map self = new HashMap(); 6 | 7 | PrototypeMap(this.prototype); 8 | 9 | void operator []=(name, value) { 10 | self[name] = value; 11 | } 12 | V operator [](name) => self.containsKey(name) ? self[name] : prototype[name]; 13 | 14 | bool get isEmpty => self.isEmpty && prototype.isEmpty; 15 | bool get isNotEmpty => self.isNotEmpty || prototype.isNotEmpty; 16 | // todo(vbe) include prototype keys ? 17 | Iterable get keys => self.keys; 18 | // todo(vbe) include prototype values ? 19 | Iterable get values => self.values; 20 | int get length => self.length; 21 | 22 | void forEach(fn) { 23 | // todo(vbe) include prototype ? 24 | self.forEach(fn); 25 | } 26 | V remove(key) => self.remove(key); 27 | clear() => self.clear; 28 | // todo(vbe) include prototype ? 29 | bool containsKey(key) => self.containsKey(key); 30 | // todo(vbe) include prototype ? 31 | bool containsValue(key) => self.containsValue(key); 32 | void addAll(map) { 33 | self.addAll(map); 34 | } 35 | // todo(vbe) include prototype ? 36 | V putIfAbsent(key, fn) => self.putIfAbsent(key, fn); 37 | 38 | toString() => self.toString(); 39 | } 40 | -------------------------------------------------------------------------------- /lib/writer.dart: -------------------------------------------------------------------------------- 1 | library dart2es6.writer; 2 | 3 | /** 4 | * StringBuffer with indent method. Implemented inefficiently for now as a hack 5 | * Should be replaced later by efficient implementation that keeps track of individual lines 6 | * instead of redoing the entire string when indenting. 7 | */ 8 | class IndentedStringBuffer { 9 | 10 | static const String INDENT = " "; 11 | StringBuffer buffer; 12 | 13 | factory IndentedStringBuffer([obj = ""]) { 14 | if (obj is IndentedStringBuffer) return obj; 15 | return new IndentedStringBuffer._(obj); 16 | } 17 | 18 | IndentedStringBuffer._(obj): buffer = new StringBuffer(obj); 19 | 20 | get isEmpty => buffer.isEmpty; 21 | get isNotEmpty => buffer.isNotEmpty; 22 | write(obj) => buffer.write(obj); 23 | writeln(obj) => buffer.writeln(obj); 24 | writeAll(objs) => buffer.writeAll(objs); 25 | clear() => buffer.clear(); 26 | toString() => buffer.toString(); 27 | 28 | /** 29 | * returns this to allow wrapping Strings with an IndentedStringBuffer to indent them, i.e. 30 | * "hello" => new IndentedStringBuffer("hello").indent() 31 | */ 32 | IndentedStringBuffer indent([int levels = 1]) { 33 | var str = buffer.toString(); 34 | buffer.clear(); 35 | str.split('\n').forEach((line) { 36 | buffer.write(INDENT * levels); 37 | buffer.writeln(line); 38 | }); 39 | return this; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/angular/dart2es6.svg?branch=master)](https://travis-ci.org/angular/dart2es6) 2 | 3 | # dart2es6 4 | 5 | _The Dart to ECMAScript 6 transpiler_ 6 | 7 | ## For design doc & TODO's, see [wiki page](https://github.com/angular/dart2es6/wiki/Doc) 8 | 9 | ## Usage 10 | 11 | Get dependencies with `pub get` then run: 12 | 13 | `./dart2es6 input_path.dart -o output_path.js` 14 | 15 | See `--help` for more options. 16 | 17 | #### Example: Transpiling Angular change detection 18 | 19 | AngularDart's change detection library is included in the `change_detection` folder as an example. 20 | The folder included here has been modified from the original with some unsupported code removed. 21 | 22 | To transpile this example: 23 | 24 | cd change_detection 25 | ./transpile 26 | 27 | The output will be located in `change_detection/out` 28 | 29 | Uncomment line in `change_detection/transpile` to transpile change detection tests as well. 30 | 31 | Currently, change detection and its tests transpile incorrectly since some code are not supported. 32 | See design doc for more info. 33 | 34 | #### Running unit tests 35 | 36 | Install dependencies: 37 | 38 | - Install `node` and `npm` 39 | - Install `traceur`: `npm install -g traceur` 40 | 41 | Then run unit tests: 42 | 43 | mkdir -p test/out/preprocessor test/out/traceur test/out/transpiler 44 | dart -c test/test_runner.dart 45 | 46 | #### Problems? 47 | 48 | Check environment is set correctly. See `scripts/travis/setup.sh` and `scripts/travis/build.sh`. 49 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See http://pub.dartlang.org/doc/glossary.html#lockfile 3 | packages: 4 | analyzer: 5 | description: analyzer 6 | source: hosted 7 | version: "0.21.1" 8 | args: 9 | description: args 10 | source: hosted 11 | version: "0.11.0+1" 12 | barback: 13 | description: barback 14 | source: hosted 15 | version: "0.14.1+3" 16 | code_transformers: 17 | description: code_transformers 18 | source: hosted 19 | version: "0.1.6" 20 | collection: 21 | description: collection 22 | source: hosted 23 | version: "0.9.3+1" 24 | crypto: 25 | description: crypto 26 | source: hosted 27 | version: "0.9.0" 28 | guinness: 29 | description: guinness 30 | source: hosted 31 | version: "0.1.11" 32 | logging: 33 | description: logging 34 | source: hosted 35 | version: "0.9.1+1" 36 | matcher: 37 | description: matcher 38 | source: hosted 39 | version: "0.10.1+1" 40 | path: 41 | description: path 42 | source: hosted 43 | version: "1.2.1" 44 | source_maps: 45 | description: source_maps 46 | source: hosted 47 | version: "0.9.4" 48 | source_span: 49 | description: source_span 50 | source: hosted 51 | version: "1.0.0" 52 | stack_trace: 53 | description: stack_trace 54 | source: hosted 55 | version: "0.9.3+2" 56 | unittest: 57 | description: unittest 58 | source: hosted 59 | version: "0.11.0+2" 60 | watcher: 61 | description: watcher 62 | source: hosted 63 | version: "0.9.2" 64 | -------------------------------------------------------------------------------- /dart2es6: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dart 2 | import 'dart:io'; 3 | import 'package:args/args.dart'; 4 | import 'package:path/path.dart' as path; 5 | import 'lib/transpiler.dart'; 6 | 7 | main(List arguments) { 8 | var OUTPUT = 'output'; 9 | var HELP = 'help'; 10 | var TEST = 'test'; 11 | var SKIP_PART = 'skip-part'; 12 | ArgResults args; 13 | final parser = new ArgParser(allowTrailingOptions: true) 14 | ..addOption(OUTPUT, help: "Generate the output into ", abbr: 'o', defaultsTo: 'out.js') 15 | ..addFlag(HELP, help: "Display this message", negatable: false, abbr: 'h') 16 | ..addFlag(TEST, help: "On error, print and continue execution", negatable: false, abbr: 't'); 17 | 18 | StringBuffer usageBuffer = new StringBuffer("Usage: dart2es6 [options] dartfile\n") 19 | ..write('\nTranspiles Dart to ES6.\n\n Options:\n') 20 | ..write(parser.getUsage()); 21 | var usage = usageBuffer.toString(); 22 | 23 | var input, output; 24 | try { 25 | args = parser.parse(arguments); 26 | if (args[HELP]) { 27 | print(usage); 28 | exit(0); 29 | } 30 | if (args.rest.isEmpty) throw new FormatException("No input specified."); 31 | input = args.rest.first; 32 | if (args.rest.length > 1) { 33 | throw new FormatException("Extra arguments: '${args.rest.sublist(1).join(' ')}'."); 34 | } 35 | output = args[OUTPUT]; 36 | } on FormatException catch(e) { 37 | print(usage); 38 | print("\nError: " + e.message); 39 | exit(1); 40 | } 41 | 42 | File outFile = new File(output); 43 | outFile.openWrite(mode: FileMode.WRITE) 44 | ..write(new Transpiler.fromPath(input, test: args[TEST]).transpile()) 45 | ..close(); 46 | } 47 | -------------------------------------------------------------------------------- /lib/replacements.dart: -------------------------------------------------------------------------------- 1 | part of dart2es6.visitor; 2 | 3 | /** 4 | * Identifiers that will always be replaced by the JS equivalent 5 | */ 6 | Map GLOBAL_REPLACE = { 7 | 'print': 'console.log', 8 | }; 9 | 10 | /** 11 | * Dictionary of types to a map of . 12 | * JS name should be left as empty string if there is no equivalent and should error 13 | * Fields/methods not listed here will not be replaced and be left as-is in JS. 14 | */ 15 | Map> FIELD_REPLACE = { 16 | List: { 17 | 'add': 'push', 18 | } 19 | }; 20 | 21 | /** 22 | * returns true if B implements A 23 | * This is used to check if B, found in code, has a field/method of A that should be replaced. 24 | * 25 | * DartType is a weird class used by analyzer 26 | * TODO: un-hack it and compare types instead of the string repr of type & support subclasses 27 | */ 28 | _sameType(DartType a, Type b) { 29 | return a.displayName.contains(b.toString()); 30 | } 31 | 32 | /** 33 | * replaces field & method names for Dart classes, such as .add => .push for Lists 34 | * 35 | * [target] is type of the object, [field] is the field that the return value will be the JS 36 | * equivalent of. Most cases the return value will be the field that was passed in, and it will 37 | * only be different when the field for the type has an alternative name in JS. 38 | */ 39 | String _replacedField(DartType target, String field) { 40 | var key = FIELD_REPLACE.keys.where((t) => _sameType(target, t)); 41 | if (key.isEmpty) return field; 42 | assert(key.length == 1); 43 | var replace = FIELD_REPLACE[key.first][field]; 44 | if (replace == "") throw "Unsupported field/method: ${key.first}.$field."; 45 | return replace == null ? field : replace; 46 | } 47 | 48 | /** 49 | * TODO 50 | * This function should check if a function or variable declared in the global scope 51 | * shadows any global builtins or other already declared global variables to help throw a 52 | * warning to the user. 53 | */ 54 | _doesNotShadow(String name) => !GLOBAL_REPLACE.keys.contains(name); 55 | -------------------------------------------------------------------------------- /lib/transpiler.dart: -------------------------------------------------------------------------------- 1 | library dart2es6; 2 | 3 | import 'package:analyzer/analyzer.dart'; 4 | import 'package:analyzer/src/generated/java_io.dart'; 5 | import 'package:analyzer/src/generated/source_io.dart'; 6 | import 'package:analyzer/src/generated/ast.dart'; 7 | import 'package:analyzer/src/generated/sdk.dart' show DartSdk; 8 | import 'package:analyzer/src/generated/sdk_io.dart' show DirectoryBasedDartSdk; 9 | import 'package:analyzer/src/generated/element.dart'; 10 | import 'package:analyzer/src/generated/engine.dart'; 11 | import 'package:code_transformers/resolver.dart' show dartSdkDirectory; 12 | import 'visitor.dart'; 13 | import 'dart:io'; 14 | 15 | class Transpiler { 16 | CompilationUnit compilationUnit; 17 | MainVisitor visitor; 18 | String path; 19 | 20 | /** 21 | * Sets visitor, finds file, parses, resolves, and sets compilationUnit to result 22 | */ 23 | Transpiler.fromPath(String path, {test: false}) 24 | // if testing, use TestVisitor that does not quit on thrown exception, let guinness handle it 25 | : visitor = (test ? new TestVisitor(path) : new MainVisitor(path)), 26 | this.path = path { 27 | 28 | // No need to understand how the compilationUnit is obtained here, the code is just 29 | // required calls as per analyzer 0.21.1 API to build AST and resolve types. 30 | JavaSystemIO.setProperty("com.google.dart.sdk", dartSdkDirectory); 31 | DartSdk sdk = DirectoryBasedDartSdk.defaultSdk; 32 | AnalysisContext context = AnalysisEngine.instance.createAnalysisContext(); 33 | context.sourceFactory = new SourceFactory([new DartUriResolver(sdk), new FileUriResolver()]); 34 | Source source = new FileBasedSource.con1(new JavaFile(path)); 35 | ChangeSet changeSet = new ChangeSet(); 36 | changeSet.addedSource(source); 37 | context.applyChanges(changeSet); 38 | LibraryElement libElement = context.computeLibraryElement(source); 39 | 40 | compilationUnit = context.resolveCompilationUnit(source, libElement); 41 | } 42 | 43 | String transpile() { 44 | print("Transpiling $path"); 45 | return compilationUnit.accept(visitor).toString(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/samples/unittest.dart: -------------------------------------------------------------------------------- 1 | import '../annotations.dart'; 2 | 3 | globalFunction() => print('globalFunction called'); 4 | globalFunctionParam(param) { 5 | print('globalFunctionParam called with '); 6 | print(param); 7 | return param; 8 | } 9 | 10 | @describe 11 | class Operators { 12 | @it 13 | shouldAdd() => 1 + 2; 14 | @it 15 | shouldFollowParenthesizedPrecedence() => (2 + 2) * 3; 16 | @xit 17 | shouldDivideIntegers() => 20 / 4; 18 | @it 19 | shouldHandleFloatingPointDivisionOfIntegers() => 20 / 3; 20 | @it 21 | shouldHandleModulus() => 20 % 13; 22 | @it 23 | shouldConcatenateStringsImplicitly() => "return" + "A" "String"; 24 | } 25 | 26 | @describe 27 | class Methods1 { 28 | helper1(a, b) { 29 | return a + b; 30 | } 31 | @it 32 | shouldAcceptRequiredParameters() { 33 | return helper1(1, 2); 34 | } 35 | helper2([something]) { 36 | return something; 37 | } 38 | @it 39 | shouldAcceptPositionalParameters() { 40 | return helper2(3); 41 | } 42 | helper3([something = 1+1]) { 43 | return something; 44 | } 45 | @it 46 | shouldAcceptPositionalParametersWithDefaultValues() { 47 | return helper3(); 48 | } 49 | } 50 | 51 | @describe 52 | class Methods2 { 53 | @it 54 | shouldCallFunctions() { 55 | globalFunction(); 56 | return 1; 57 | } 58 | } 59 | 60 | // Named params not supported 61 | @xdescribe 62 | class Methods3 { 63 | helper1({param}) => param; 64 | @it 65 | shouldAcceptNamedParameters() { 66 | return helper1(param: 5); 67 | } 68 | helper2({param: 5}) => param; 69 | @it 70 | shouldAcceptNamedParametersWithDefaultValues() { 71 | return helper2(); 72 | } 73 | helper3(param1, {param2, param3: 4}) => param1 + param2 + param3; 74 | @it 75 | shouldAcceptAMixOfParameters() { 76 | return helper3(2, param2: 3); 77 | } 78 | } 79 | 80 | @describe 81 | class Constructors1 { 82 | var field1; 83 | var field2 = 2; 84 | var field3 = globalFunctionParam(5) + 2; 85 | Constructors1(); 86 | @it 87 | shouldSetDeclaredFieldsToNullByDefault() => field1; 88 | @it 89 | shouldSetFieldsWithDefaultValue() => field2; 90 | @it 91 | shouldCalculateFieldDefaultValues() => field3; 92 | } 93 | 94 | @describe 95 | class Constructors2 { 96 | @it 97 | shouldInitializeFieldsWithThisShorthandNotation() => new ConstructorHelper1(5).field1; 98 | @xit // TODO 99 | shouldInitializeFieldsWithInitializerList() => new ConstructorHelper2(6).field1; 100 | @it 101 | shouldSetFieldsToNullBeforeConstructorBodyExecutes() => new ConstructorHelper3().field1; 102 | } 103 | 104 | class ConstructorHelper1 { 105 | var field1; 106 | ConstructorHelper1(this.field1); 107 | } 108 | 109 | class ConstructorHelper2 { 110 | var field1; 111 | ConstructorHelper2(f) : field1 = f; 112 | } 113 | 114 | class ConstructorHelper3 { 115 | var field1; 116 | ConstructorHelper3() { 117 | globalFunctionParam(field1); 118 | } 119 | } 120 | 121 | @describe 122 | class StaticFields { 123 | static var field1; 124 | static var field2 = 0; 125 | @it 126 | shouldBeSetToNullByDefault() => field1; 127 | @it 128 | shouldBeSetToGivenValues() => field2; 129 | @it 130 | shouldBeStatic() { 131 | new StaticFieldsHelper().increment(); 132 | new StaticFieldsHelper().increment(); 133 | return StaticFieldsHelper.field1; 134 | } 135 | } 136 | 137 | class StaticFieldsHelper { 138 | static var field1 = 0; 139 | increment() => field1++; 140 | } 141 | 142 | @describe 143 | class VariableDeclarations { 144 | @it 145 | shouldDefaultToNull() { 146 | var a, b, c; 147 | return a; 148 | } 149 | @it 150 | shouldSupportAssignmentsToExpressions() { 151 | var b = 2, c = 2 + 3; 152 | return b + c; 153 | } 154 | } 155 | 156 | @describe 157 | class Blocks { 158 | @it 159 | shouldSupportIfElseAndComparisons() { 160 | if (1 > 2) { 161 | return 0; 162 | } else if (2 < 1) { 163 | return 1; 164 | } else { 165 | if (3 + 2 >= 5) { 166 | return 2; 167 | } 168 | return 3; 169 | } 170 | } 171 | @it 172 | shouldSupportForLoops() { 173 | var j = 0; 174 | for (var i = 1; i < 5; i++) { 175 | j++; 176 | } 177 | return j; 178 | } 179 | @it 180 | shouldSupportForLoopsWithEmptyParts() { 181 | var i = 0; 182 | for (; globalFunctionParam(i) < 100;) { 183 | i = 200; 184 | } 185 | for (; false;) 186 | i = 0; 187 | return i; 188 | } 189 | @it 190 | shouldSupportWhileLoops() { 191 | var i = 0; 192 | while (i < 10) i++; 193 | return i; 194 | } 195 | } 196 | 197 | @describe 198 | class Lists { 199 | @it 200 | shouldSupportLiteralsAndIndexing() { 201 | return [1, 2, 3][1]; 202 | } 203 | @xit 204 | shouldSupportConstructorArguments() { 205 | return new List(10); 206 | } 207 | @it 208 | shouldAddElementsWithPush() { 209 | var a = new List(); 210 | a.add(5); 211 | return a[0]; 212 | } 213 | } 214 | 215 | 216 | 217 | 218 | 219 | -------------------------------------------------------------------------------- /change_detection/lib/ast.dart: -------------------------------------------------------------------------------- 1 | part of angular.watch_group; 2 | 3 | 4 | /** 5 | * RULES: 6 | * - ASTs are reusable. Don't store scope/instance refs there 7 | * - Parent knows about children, not the other way around. 8 | */ 9 | abstract class AST { 10 | static final String _CONTEXT = '#'; 11 | final String expression; 12 | var parsedExp; // The parsed version of expression. 13 | AST(expression) 14 | : expression = expression.startsWith('#.') 15 | ? expression.substring(2) 16 | : expression 17 | { 18 | assert(expression!=null); 19 | } 20 | WatchRecord<_Handler> setupWatch(WatchGroup watchGroup); 21 | String toString() => expression; 22 | } 23 | 24 | /** 25 | * SYNTAX: _context_ 26 | * 27 | * This represent the initial _context_ for evaluation. 28 | */ 29 | class ContextReferenceAST extends AST { 30 | ContextReferenceAST(): super(AST._CONTEXT); 31 | WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => 32 | new _ConstantWatchRecord(watchGroup, expression, watchGroup.context); 33 | } 34 | 35 | /** 36 | * SYNTAX: _context_ 37 | * 38 | * This represent the initial _context_ for evaluation. 39 | */ 40 | class ConstantAST extends AST { 41 | final constant; 42 | 43 | ConstantAST(constant, [String expression]) 44 | : constant = constant, 45 | super(expression == null 46 | ? constant is String ? '"$constant"' : '$constant' 47 | : expression); 48 | 49 | WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => 50 | new _ConstantWatchRecord(watchGroup, expression, constant); 51 | } 52 | 53 | /** 54 | * SYNTAX: lhs.name. 55 | * 56 | * This is the '.' dot operator. 57 | */ 58 | class FieldReadAST extends AST { 59 | AST lhs; 60 | final String name; 61 | 62 | FieldReadAST(lhs, name) 63 | : lhs = lhs, 64 | name = name, 65 | super('$lhs.$name'); 66 | 67 | WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => 68 | watchGroup.addFieldWatch(lhs, name, expression); 69 | } 70 | 71 | /** 72 | * SYNTAX: fn(arg0, arg1, ...) 73 | * 74 | * Invoke a pure function. Pure means that the function has no state, and 75 | * therefore it needs to be re-computed only if its args change. 76 | */ 77 | class PureFunctionAST extends AST { 78 | final String name; 79 | final /* dartbug.com/16401 Function */ fn; 80 | final List argsAST; 81 | 82 | PureFunctionAST(name, this.fn, argsAST) 83 | : argsAST = argsAST, 84 | name = name, 85 | super('$name(${_argList(argsAST)})'); 86 | 87 | WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => 88 | watchGroup.addFunctionWatch(fn, argsAST, const {}, expression, true); 89 | } 90 | 91 | /** 92 | * SYNTAX: fn(arg0, arg1, ...) 93 | * 94 | * Invoke a pure function. Pure means that the function has no state, and 95 | * therefore it needs to be re-computed only if its args change. 96 | */ 97 | class ClosureAST extends AST { 98 | final String name; 99 | final /* dartbug.com/16401 Function */ fn; 100 | final List argsAST; 101 | 102 | ClosureAST(name, this.fn, argsAST) 103 | : argsAST = argsAST, 104 | name = name, 105 | super('$name(${_argList(argsAST)})'); 106 | 107 | WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => 108 | watchGroup.addFunctionWatch(fn, argsAST, const {}, expression, false); 109 | } 110 | 111 | /** 112 | * SYNTAX: lhs.method(arg0, arg1, ...) 113 | * 114 | * Invoke a method on [lhs] object. 115 | */ 116 | class MethodAST extends AST { 117 | final AST lhsAST; 118 | final String name; 119 | final List argsAST; 120 | final Map namedArgsAST; 121 | 122 | MethodAST(lhsAST, name, argsAST, [this.namedArgsAST = const {}]) 123 | : lhsAST = lhsAST, 124 | name = name, 125 | argsAST = argsAST, 126 | super('$lhsAST.$name(${_argList(argsAST)})'); 127 | 128 | WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => 129 | watchGroup.addMethodWatch(lhsAST, name, argsAST, namedArgsAST, expression); 130 | } 131 | 132 | 133 | class CollectionAST extends AST { 134 | final AST valueAST; 135 | CollectionAST(valueAST) 136 | : valueAST = valueAST, 137 | super('#collection($valueAST)'); 138 | 139 | WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => 140 | watchGroup.addCollectionWatch(valueAST); 141 | } 142 | 143 | String _argList(List items) => items.join(', '); 144 | 145 | /** 146 | * The name is a bit oxymoron, but it is essentially the NullObject pattern. 147 | * 148 | * This allows children to set a handler on this Record and then let it write 149 | * the initial constant value to the forwarding Record. 150 | */ 151 | class _ConstantWatchRecord extends WatchRecord<_Handler> { 152 | final currentValue; 153 | final _Handler handler; 154 | 155 | _ConstantWatchRecord(WatchGroup watchGroup, String expression, currentValue) 156 | : currentValue = currentValue, 157 | handler = new _ConstantHandler(watchGroup, expression, currentValue); 158 | 159 | bool check() => false; 160 | void remove() => null; 161 | 162 | get field => null; 163 | get previousValue => null; 164 | get object => null; 165 | set object(_) => null; 166 | get nextChange => null; 167 | } 168 | 169 | -------------------------------------------------------------------------------- /change_detection/lib/linked_list.dart: -------------------------------------------------------------------------------- 1 | part of angular.watch_group; 2 | 3 | 4 | class _LinkedListItem { 5 | I _previous, _next; 6 | } 7 | 8 | class _LinkedList { 9 | L _head, _tail; 10 | 11 | static _Handler _add(_Handler list, _LinkedListItem item) { 12 | assert(item._next == null); 13 | assert(item._previous == null); 14 | if (list._tail == null) { 15 | list._head = list._tail = item; 16 | } else { 17 | item._previous = list._tail; 18 | list._tail._next = item; 19 | list._tail = item; 20 | } 21 | return item; 22 | } 23 | 24 | static bool _isEmpty(_Handler list) => list._head == null; 25 | 26 | static void _remove(_Handler list, _Handler item) { 27 | var previous = item._previous; 28 | var next = item._next; 29 | 30 | if (previous == null) list._head = next; else previous._next = next; 31 | if (next == null) list._tail = previous; else next._previous = previous; 32 | } 33 | } 34 | 35 | class _ArgHandlerList { 36 | _ArgHandler _argHandlerHead, _argHandlerTail; 37 | 38 | static _Handler _add(_ArgHandlerList list, _ArgHandler item) { 39 | assert(item._nextArgHandler == null); 40 | assert(item._previousArgHandler == null); 41 | if (list._argHandlerTail == null) { 42 | list._argHandlerHead = list._argHandlerTail = item; 43 | } else { 44 | item._previousArgHandler = list._argHandlerTail; 45 | list._argHandlerTail._nextArgHandler = item; 46 | list._argHandlerTail = item; 47 | } 48 | return item; 49 | } 50 | 51 | static bool _isEmpty(_InvokeHandler list) => list._argHandlerHead == null; 52 | 53 | static void _remove(_InvokeHandler list, _ArgHandler item) { 54 | var previous = item._previousArgHandler; 55 | var next = item._nextArgHandler; 56 | 57 | if (previous == null) list._argHandlerHead = next; else previous._nextArgHandler = next; 58 | if (next == null) list._argHandlerTail = previous; else next._previousArgHandler = previous; 59 | } 60 | } 61 | 62 | class _WatchList { 63 | Watch _watchHead, _watchTail; 64 | 65 | static Watch _add(_WatchList list, Watch item) { 66 | assert(item._nextWatch == null); 67 | assert(item._previousWatch == null); 68 | if (list._watchTail == null) { 69 | list._watchHead = list._watchTail = item; 70 | } else { 71 | item._previousWatch = list._watchTail; 72 | list._watchTail._nextWatch = item; 73 | list._watchTail = item; 74 | } 75 | return item; 76 | } 77 | 78 | static bool _isEmpty(_Handler list) => list._watchHead == null; 79 | 80 | static void _remove(_Handler list, Watch item) { 81 | var previous = item._previousWatch; 82 | var next = item._nextWatch; 83 | 84 | if (previous == null) list._watchHead = next; else previous._nextWatch = next; 85 | if (next == null) list._watchTail = previous; else next._previousWatch = previous; 86 | } 87 | } 88 | 89 | abstract class _EvalWatchList { 90 | _EvalWatchRecord _evalWatchHead, _evalWatchTail; 91 | _EvalWatchRecord get _marker; 92 | 93 | static _EvalWatchRecord _add(_EvalWatchList list, _EvalWatchRecord item) { 94 | assert(item._nextEvalWatch == null); 95 | assert(item._prevEvalWatch == null); 96 | var prev = list._evalWatchTail; 97 | var next = prev._nextEvalWatch; 98 | 99 | if (prev == list._marker) { 100 | list._evalWatchHead = list._evalWatchTail = item; 101 | prev = prev._prevEvalWatch; 102 | list._marker._prevEvalWatch = null; 103 | list._marker._nextEvalWatch = null; 104 | } 105 | item._nextEvalWatch = next; 106 | item._prevEvalWatch = prev; 107 | 108 | if (prev != null) prev._nextEvalWatch = item; 109 | if (next != null) next._prevEvalWatch = item; 110 | 111 | return list._evalWatchTail = item; 112 | } 113 | 114 | static bool _isEmpty(_EvalWatchList list) => list._evalWatchHead == null; 115 | 116 | static void _remove(_EvalWatchList list, _EvalWatchRecord item) { 117 | assert(item.watchGrp == list); 118 | var prev = item._prevEvalWatch; 119 | var next = item._nextEvalWatch; 120 | 121 | if (list._evalWatchHead == list._evalWatchTail) { 122 | list._evalWatchHead = list._evalWatchTail = list._marker; 123 | list._marker._nextEvalWatch = next; 124 | list._marker._prevEvalWatch = prev; 125 | if (prev != null) prev._nextEvalWatch = list._marker; 126 | if (next != null) next._prevEvalWatch = list._marker; 127 | } else { 128 | if (item == list._evalWatchHead) list._evalWatchHead = next; 129 | if (item == list._evalWatchTail) list._evalWatchTail = prev; 130 | if (prev != null) prev._nextEvalWatch = next; 131 | if (next != null) next._prevEvalWatch = prev; 132 | } 133 | } 134 | } 135 | 136 | class _WatchGroupList { 137 | WatchGroup _watchGroupHead, _watchGroupTail; 138 | 139 | static WatchGroup _add(_WatchGroupList list, WatchGroup item) { 140 | assert(item._nextWatchGroup == null); 141 | assert(item._prevWatchGroup == null); 142 | if (list._watchGroupTail == null) { 143 | list._watchGroupHead = list._watchGroupTail = item; 144 | } else { 145 | item._prevWatchGroup = list._watchGroupTail; 146 | list._watchGroupTail._nextWatchGroup = item; 147 | list._watchGroupTail = item; 148 | } 149 | return item; 150 | } 151 | 152 | static bool _isEmpty(_WatchGroupList list) => list._watchGroupHead == null; 153 | 154 | static void _remove(_WatchGroupList list, WatchGroup item) { 155 | var previous = item._prevWatchGroup; 156 | var next = item._nextWatchGroup; 157 | 158 | if (previous == null) list._watchGroupHead = next; else previous._nextWatchGroup = next; 159 | if (next == null) list._watchGroupTail = previous; else next._prevWatchGroup = previous; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /change_detection/lib/change_detection.dart: -------------------------------------------------------------------------------- 1 | library change_detection; 2 | 3 | typedef void EvalExceptionHandler(error, stack); 4 | 5 | /** 6 | * An interface for [ChangeDetectorGroup] groups related watches together. It 7 | * guarantees that within the group all watches will be reported in the order in 8 | * which they were registered. It also provides an efficient way of removing the 9 | * watch group. 10 | */ 11 | abstract class ChangeDetectorGroup { 12 | /** 13 | * Watch a specific [field] on an [object]. 14 | * 15 | * If the [field] is: 16 | * * _name_ - Name of the property to watch. (If the [object] is a Map then 17 | * treat the name as a key.) 18 | * * _null_ - Watch all the items for arrays and maps otherwise the object 19 | * identity. 20 | * 21 | * Parameters: 22 | * * [object] to watch. 23 | * * [field] to watch on the [object]. 24 | * * [handler] an opaque object passed on to [Record]. 25 | */ 26 | WatchRecord watch(Object object, String field, H handler); 27 | 28 | /// Remove all the watches in an efficient manner. 29 | void remove(); 30 | 31 | /// Create a child [ChangeDetectorGroup] 32 | ChangeDetectorGroup newGroup(); 33 | } 34 | 35 | /** 36 | * An interface for [ChangeDetector]. An application can have multiple instances 37 | * of the [ChangeDetector] to be used for checking different application 38 | * domains. 39 | * 40 | * [ChangeDetector] works by comparing the identity of the objects not by 41 | * calling the `.equals()` method. This is because ChangeDetector needs to have 42 | * predictable performance, and the developer can implement `.equals()` on top 43 | * of identity checks. 44 | * 45 | * [H] A [Record] has associated handler object. The handler object is opaque 46 | * to the [ChangeDetector] but it is meaningful to the code which registered the 47 | * watcher. It can be a data structure, an object, or a function. It is up to 48 | * the developer to attach meaning to it. 49 | */ 50 | abstract class ChangeDetector extends ChangeDetectorGroup { 51 | /** 52 | * This method does the work of collecting the changes and returns them as a 53 | * linked list of [Record]s. The [Record]s are returned in the 54 | * same order as they were registered. 55 | */ 56 | Iterator> collectChanges([EvalExceptionHandler exceptionHandler, 57 | AvgStopwatch stopwatch ]); 58 | } 59 | 60 | abstract class Record { 61 | /** The observed object. */ 62 | Object get object; 63 | 64 | /** 65 | * The field which is being watched: 66 | * * _name_ - Name of the field to watch. 67 | * * _null_ - Watch all the items for arrays and maps otherwise the object 68 | * identity. 69 | */ 70 | String get field; 71 | 72 | /** 73 | * An application provided object which contains the specific logic which 74 | * needs to be applied when the change is detected. The handler is opaque to 75 | * the ChangeDetector and as such can be anything the application desires. 76 | */ 77 | H get handler; 78 | 79 | /** 80 | * * The current value of the [field] on the [object], 81 | * * a [CollectionChangeRecord] if an iterable is observed, 82 | * * a [MapChangeRecord] if a map is observed. 83 | */ 84 | get currentValue; 85 | /** 86 | * * Previous value of the [field] on the [object], 87 | * * [:null:] when an iterable or a map are observed. 88 | */ 89 | get previousValue; 90 | } 91 | 92 | /** 93 | * [WatchRecord] API which allows changing what object is being watched and 94 | * manually triggering the checking. 95 | */ 96 | abstract class WatchRecord extends Record { 97 | /// Set a new object for checking 98 | set object(value); 99 | 100 | /// Returns [:true:] when changes have been detected 101 | bool check(); 102 | 103 | void remove(); 104 | } 105 | 106 | /** 107 | * If the [ChangeDetector] is watching a [Map] then the [currentValue] of 108 | * [Record] will contain an instance of [MapChangeRecord]. A [MapChangeRecord] 109 | * contains the changes to the map since the last execution. The changes are 110 | * reported as a list of [MapKeyValue]s which contain the key as well as its 111 | * current and previous value. 112 | */ 113 | abstract class MapChangeRecord { 114 | /// The underlying map object 115 | Map get map; 116 | 117 | void forEachItem(void f(MapKeyValue item)); 118 | void forEachPreviousItem(void f(MapKeyValue previousItem)); 119 | void forEachChange(void f(MapKeyValue change)); 120 | void forEachAddition(void f(MapKeyValue addition)); 121 | void forEachRemoval(void f(MapKeyValue removal)); 122 | } 123 | 124 | /** 125 | * Each item in map is wrapped in [MapKeyValue], which can track 126 | * the [item]s [currentValue] and [previousValue] location. 127 | */ 128 | abstract class MapKeyValue { 129 | /// The item. 130 | K get key; 131 | 132 | /// Previous item location in the list or [null] if addition. 133 | V get previousValue; 134 | 135 | /// Current item location in the list or [null] if removal. 136 | V get currentValue; 137 | } 138 | 139 | /** 140 | * If the [ChangeDetector] is watching an [Iterable] then the [currentValue] of 141 | * [Record] will contain an instance of [CollectionChangeRecord]. The 142 | * [CollectionChangeRecord] contains the changes to the collection since the 143 | * last execution. The changes are reported as a list of [CollectionChangeItem]s 144 | * which contain the item as well as its current and previous index. 145 | */ 146 | abstract class CollectionChangeRecord { 147 | /** The underlying iterable object */ 148 | Iterable get iterable; 149 | int get length; 150 | 151 | void forEachItem(void f(CollectionChangeItem item)); 152 | void forEachPreviousItem(void f(CollectionChangeItem previousItem)); 153 | void forEachAddition(void f(CollectionChangeItem addition)); 154 | void forEachMove(void f(CollectionChangeItem move)); 155 | void forEachRemoval(void f(CollectionChangeItem removal)); 156 | } 157 | 158 | /** 159 | * Each changed item in the collection is wrapped in a [CollectionChangeItem], 160 | * which tracks the [item]s [currentKey] and [previousKey] location. 161 | */ 162 | abstract class CollectionChangeItem { 163 | /** Previous item location in the list or [:null:] if addition. */ 164 | int get previousIndex; 165 | 166 | /** Current item location in the list or [:null:] if removal. */ 167 | int get currentIndex; 168 | 169 | /** The item. */ 170 | V get item; 171 | } 172 | 173 | typedef dynamic FieldGetter(object); 174 | typedef void FieldSetter(object, value); 175 | 176 | abstract class FieldGetterFactory { 177 | FieldGetter getter(Object object, String name); 178 | } 179 | 180 | class AvgStopwatch extends Stopwatch { 181 | int _count = 0; 182 | 183 | int get count => _count; 184 | 185 | void reset() { 186 | _count = 0; 187 | super.reset(); 188 | } 189 | 190 | int increment(int count) => _count += count; 191 | 192 | double get ratePerMs => elapsedMicroseconds == 0 193 | ? 0.0 194 | : _count / elapsedMicroseconds * 1000; 195 | } 196 | -------------------------------------------------------------------------------- /test/test_runner.dart: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dart 2 | 3 | import 'dart:io'; 4 | import 'dart:async'; 5 | import 'package:guinness/guinness.dart'; 6 | import 'package:path/path.dart' as path; 7 | import 'package:dart2es6/transpiler.dart'; 8 | 9 | const String DUMMY_CLASS_NAME = "TEST_CLASS_NAME"; 10 | const String DUMMY_METHOD_NAME = "TEST_METHOD_NAME"; 11 | 12 | List testFiles = [ 13 | "unittest" 14 | ]; 15 | 16 | Future test(String p) { 17 | var curDir = path.dirname(path.fromUri(Platform.script)); 18 | Map> testCaseNames; 19 | var dart2es6Path = path.join(path.dirname(curDir), 'dart2es6'); 20 | var preprocessorOutput = path.join(curDir, 'out', 'preprocessor', p + '.dart'); 21 | var transpilerOutput = path.join(curDir, 'out', 'transpiler', p + '.js'); 22 | var traceurOutput = path.join(curDir, 'out', 'traceur', p + '.js'); 23 | 24 | var traceurRuntime = new File(path.join(curDir, "assets", "traceur.js")).readAsStringSync(); 25 | 26 | // The entire Js file gets copied for each test so only one traceur call is needed 27 | // Dart is done the same way to match 28 | Future _getJsOutput(String className, String methodName) { 29 | var p = path.join(curDir, 'temp-$className-$methodName.js'); 30 | var code = new File(traceurOutput).readAsStringSync() 31 | .replaceAll(DUMMY_CLASS_NAME, className) 32 | .replaceAll(DUMMY_METHOD_NAME, methodName); 33 | var file = new File(p); 34 | var sink = file.openWrite(mode: FileMode.WRITE) 35 | ..write(traceurRuntime) // needed to run traceur output 36 | ..write(code); 37 | return sink.close().then((_) { 38 | return Process.run("node", [p]).then((result) { 39 | try { 40 | _checkResults(result); 41 | } catch (e) { 42 | throw "Error thrown while attempting to execute:\n$p\n\n$e"; 43 | } 44 | file.delete(); 45 | return result.stdout; 46 | }); 47 | }); 48 | } 49 | 50 | Future _getDartOutput(String className, String methodName) { 51 | var p = path.join(curDir, 'temp-$className-$methodName.dart'); 52 | var file = new File(p); 53 | var sink = file.openWrite(mode: FileMode.WRITE) 54 | ..write(new File(preprocessorOutput).readAsStringSync()) 55 | ..write("\nmain() => print(new $className().$methodName());"); 56 | return sink.close().then((_){ 57 | return Process.run("dart", ['-c', p]).then((result) { 58 | try { 59 | _checkResults(result); 60 | } catch (e) { 61 | throw "Error thrown while attempting to execute:\n$p\n\n$e"; 62 | } 63 | file.delete(); 64 | return result.stdout; 65 | }); 66 | }); 67 | } 68 | 69 | /** 70 | * Runs method in dart & JS, compares results with guinness 71 | */ 72 | void _testClass(String className, List methodNames) { 73 | describe(_convertName(className), () { 74 | methodNames.forEach((methodName) { 75 | it(_convertName(methodName), () { 76 | Future dart = _getDartOutput(className, methodName); 77 | Future js = _getJsOutput(className, methodName); 78 | return Future.wait([js, dart]).then((results) { 79 | expect(results[0]).toEqual(results[1]); 80 | }); 81 | }); 82 | }); 83 | }); 84 | } 85 | 86 | new File(path.join(curDir, 'samples', p + '.dart')).readAsString().then((f) { 87 | var sink = new File(preprocessorOutput).openWrite(); 88 | testCaseNames = _processTestFile(f, sink); 89 | sink.close(); 90 | }).then((_) { 91 | return Process.run("dart", ['-c', dart2es6Path, '-o', transpilerOutput, preprocessorOutput]) 92 | .then((ProcessResult results) { 93 | if (results.stdout.isNotEmpty) print(results.stdout); 94 | _checkResults(results); 95 | new File(transpilerOutput).writeAsStringSync( 96 | "\nconsole.log(new $DUMMY_CLASS_NAME().$DUMMY_METHOD_NAME());\n", mode: FileMode.APPEND); 97 | }); 98 | }).then((_) { 99 | // needs `npm install -g traceur` 100 | assert(new File(transpilerOutput) != null); 101 | return Process.run("traceur", ['--out', traceurOutput, transpilerOutput]) 102 | .then((ProcessResult results) => _checkResults(results)); 103 | }).then((_) { 104 | testCaseNames.forEach(_testClass); 105 | }); 106 | } 107 | 108 | String _convertName(String name) { 109 | return name 110 | .replaceAllMapped(new RegExp(r"([A-Z])"), (m) => " ${m.group(1)}") 111 | .replaceAll(new RegExp(r"\d$"), "") 112 | .toLowerCase(); 113 | } 114 | 115 | void _checkResults(ProcessResult results) { 116 | if (results.exitCode != 0) { 117 | exitCode = results.exitCode; 118 | throw results.stderr; 119 | } 120 | } 121 | 122 | /** 123 | * TODO: 124 | * 1. Add a tree shaker to remove helper classes that are not used. Helper classes do not have a 125 | * @describe, and if the test that uses them gets x'd, the helper should be removed too to not 126 | * throw an error. 127 | * 2. Bug: If a method has @iit, the class must have @ddescribe or else all other classes's methods 128 | * still get run. aka @iit is currently only local to the class that it's in 129 | * 3. Bug: Having a comment after [@describe, @it, ...] or after the class open brace breaks the 130 | * regex and results in a non-match. Fix regex to allow these comments 131 | * 4. Feature: Add a way to annotate a test to expect it to error/fail 132 | */ 133 | /// writes dart file with only selected test cases to sink, tree shakes helpers 134 | /// returns names of tests 135 | Map> _processTestFile(String file, StringSink sink) { 136 | var classRegExpStr = 137 | r'\n(class\s+(\w+?)\s+{' 138 | r'[\s\S]*?' 139 | r'\n}\n)'; // assumes class ends with the first non-indented closing curly brace 140 | file = file.replaceAll(new RegExp('@xdescribe' + classRegExpStr), '') 141 | .replaceAll("import '../annotations.dart';", ""); 142 | var ddescribeRegExp = new RegExp('@ddescribe' + classRegExpStr); 143 | var describeRegExp = new RegExp('@describe' + classRegExpStr); 144 | var classRegExp = ddescribeRegExp.hasMatch(file) ? ddescribeRegExp : describeRegExp; 145 | 146 | var methodRegExpStr = 147 | r'\n .*?(\w+)\(.*?\) ?(' 148 | r'{[\s\S]*?' // block function body 149 | r'\n }\n' // assumes method ends with first closing curly brace with indented two spaces 150 | r'|' 151 | r'=>[\s\S]*?;\n)'; // expression function body 152 | var iitRegExp = new RegExp('@iit' + methodRegExpStr); 153 | var itRegExp = new RegExp('@it' + methodRegExpStr); 154 | 155 | Map> testNames = {}; 156 | 157 | String _processClass(Match classMatch) { 158 | var className = classMatch.group(2); 159 | var methodNames = []; 160 | addMethodNameToList(match) => methodNames.add(match.group(1)); 161 | 162 | var classStr = classMatch.group(1) 163 | .replaceAll(new RegExp('@xit' + methodRegExpStr), ''); 164 | 165 | var iitMatches = iitRegExp.allMatches(classStr); 166 | if (iitMatches.isNotEmpty) { 167 | classStr = classStr.replaceAll(itRegExp, '').replaceAll('@iit\n', ''); 168 | iitMatches.forEach(addMethodNameToList); 169 | } else { 170 | var matches = itRegExp.allMatches(classStr); 171 | matches.forEach(addMethodNameToList); 172 | classStr = classStr.replaceAll('@it\n', ''); 173 | }; 174 | 175 | assert(methodNames.isNotEmpty); 176 | testNames[className] = methodNames; 177 | return classStr; 178 | } 179 | 180 | file = file.replaceAllMapped(classRegExp, _processClass); 181 | sink.write(file); 182 | return testNames; 183 | } 184 | 185 | main() { 186 | Future.forEach(testFiles, (f) => test(f)); 187 | } 188 | -------------------------------------------------------------------------------- /lib/visitor.dart: -------------------------------------------------------------------------------- 1 | library dart2es6.visitor; 2 | 3 | import 'package:analyzer/analyzer.dart'; 4 | import 'package:analyzer/src/generated/element.dart' show DartType; 5 | import 'package:path/path.dart' as Path; 6 | import 'transpiler.dart'; 7 | import 'visitor_null.dart'; 8 | import 'writer.dart'; 9 | import 'dart:io'; 10 | 11 | part 'visitor_block.dart'; 12 | part 'visitor_test.dart'; 13 | part "replacements.dart"; 14 | 15 | 16 | /** 17 | * Transpilation starts with this visitor. Processes top level declarations and directives, 18 | * assigning tasks to other visitors. 19 | */ 20 | class MainVisitor extends NullVisitor { 21 | 22 | final String path; 23 | 24 | MainVisitor(this.path); 25 | 26 | visitClassDeclaration(ClassDeclaration node) { 27 | return node.accept(new ClassVisitor()); 28 | } 29 | 30 | visitCompilationUnit(CompilationUnit node) { 31 | IndentedStringBuffer output = new IndentedStringBuffer(); 32 | node.directives.where((d) => d is! PartDirective).forEach((directive) { 33 | output.writeln(directive.accept(this)); 34 | }); 35 | 36 | node.declarations.forEach((declaration) { 37 | var d = declaration.accept(this); 38 | output.write(d); 39 | output.write('\n'); 40 | }); 41 | 42 | node.directives.where((d) => d is PartDirective).forEach((PartDirective part) { 43 | output.writeln(part.accept(this)); 44 | }); 45 | return output; 46 | } 47 | 48 | visitExportDirective(ExportDirective node) { 49 | return visitNameSpaceDirective(node); 50 | } 51 | 52 | visitFunctionDeclaration(FunctionDeclaration node) { 53 | return node.accept(new BlockVisitor()); 54 | } 55 | 56 | visitFunctionTypeAlias(FunctionTypeAlias node) { 57 | return "// $node"; 58 | } 59 | 60 | visitHideCombinator(HideCombinator node) { 61 | assert("Hide not supported" == ""); 62 | return ""; 63 | } 64 | 65 | visitImportDirective(ImportDirective node) { 66 | return visitNameSpaceDirective(node); 67 | } 68 | 69 | visitNameSpaceDirective(NamespaceDirective node) { 70 | var uri = node.uri.stringValue; 71 | if (uri.startsWith("package:") || uri.startsWith("dart:")) { 72 | print("Tried importing '$uri', skipping."); 73 | return ""; 74 | }; 75 | uri = uri.replaceAll(".dart", ""); 76 | uri = "./" + uri; 77 | 78 | if (node is ImportDirective && node.asToken != null) { 79 | assert (node.combinators.isEmpty); 80 | return "module ${node.prefix.name} from '$uri';"; 81 | } 82 | 83 | var output = new IndentedStringBuffer(); 84 | output.write(node is ImportDirective ? "import " : "export "); 85 | 86 | if (node.combinators.isEmpty) { 87 | output.write(node is ImportDirective ? "\$" : "*"); 88 | } else { 89 | output.write("{\n"); 90 | var show = node.combinators.map((c) => c.accept(this)).join(",\n"); 91 | output..write(new IndentedStringBuffer(show).indent()) 92 | ..write("}"); 93 | } 94 | output.write(" from '$uri';"); 95 | return output; 96 | } 97 | 98 | visitLibraryDirective(LibraryDirective node) => ""; 99 | 100 | visitPartDirective(PartDirective node) { 101 | var uri = node.uri.stringValue; 102 | var p = Path.join(Path.dirname(path), uri); 103 | return new Transpiler.fromPath(p).transpile(); 104 | } 105 | 106 | visitPartOfDirective(PartOfDirective node) { 107 | return "/* $node */"; 108 | } 109 | 110 | visitShowCombinator(ShowCombinator node) { 111 | return node.shownNames.map((n) => n.name).join(",\n"); 112 | } 113 | 114 | visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) { 115 | var visitor = new BlockVisitor(); 116 | var vars = node.variables.variables.map((v) => v.accept(visitor)).join(', '); 117 | return '$vars;'; // TODO: types, const, final 118 | } 119 | } 120 | 121 | 122 | // Most of these fields are not actually used 123 | class Field { 124 | final String name; 125 | final Expression value; 126 | final String type; 127 | final bool isFinal; 128 | final bool isStatic; 129 | final bool isConst; 130 | Field(this.name, this.value, this.type, this.isFinal, this.isStatic, this.isConst); 131 | toString() => name; 132 | } 133 | 134 | class Method { 135 | final String name; 136 | Method(this.name); 137 | toString() => name; 138 | } 139 | 140 | /** 141 | * A separate visitor for each class, keeping track of fields 142 | */ 143 | class ClassVisitor extends NullVisitor { 144 | 145 | String name; 146 | List fields = []; 147 | List methods = []; 148 | 149 | ClassVisitor(); 150 | 151 | visitClassDeclaration(ClassDeclaration node) { 152 | assert(name == null); 153 | name = node.name.toString(); 154 | node.members.where((m) => m is FieldDeclaration).forEach((FieldDeclaration member) { 155 | var isStatic = member.isStatic; 156 | var type = member.fields.type == null ? null : member.fields.type.name.name; 157 | var isConst = member.fields.isConst; 158 | var isFinal = member.fields.isFinal; 159 | member.fields.variables.forEach((VariableDeclaration declaration) { 160 | var field = new Field(declaration.name.toString(), declaration.initializer, 161 | type, isFinal, isStatic, isConst); 162 | fields.add(field); 163 | }); 164 | }); 165 | node.members.where((m) => m is MethodDeclaration).forEach((MethodDeclaration member) { 166 | var method = new Method(member.name.toString()); 167 | methods.add(method); 168 | }); 169 | 170 | var output = new IndentedStringBuffer(); 171 | if (node.documentationComment != null) { 172 | output.writeln(node.documentationComment.accept(new BlockVisitor())); 173 | } 174 | if (name[0] != "_") output.write("export "); // private 175 | output.write("class $name "); 176 | if (node.extendsClause != null) output.write("${node.extendsClause.accept(this)} "); 177 | output.write("{\n"); 178 | 179 | var input = new IndentedStringBuffer(); 180 | input.write(node.members.where((m) => m is! FieldDeclaration).map((ClassMember member) { 181 | return member.accept(this); 182 | }).join('\n\n')); 183 | input.indent(); 184 | output.write(input); 185 | 186 | output.write('}\n'); 187 | fields.where((f) => f.isStatic).forEach((field) { 188 | var value = field.value == null ? "null" : field.value.accept(new BlockVisitor()); 189 | output.write("$name.${field.name} = $value;\n"); 190 | }); 191 | return output; 192 | } 193 | 194 | visitExtendsClause(ExtendsClause node) => "extends ${node.superclass.name.name}"; 195 | 196 | visitMethodDeclaration(MethodDeclaration node) { 197 | return node.accept(new BlockVisitor(this)); 198 | } 199 | 200 | visitConstructorDeclaration(ConstructorDeclaration node) { 201 | return node.accept(new ConstructorVisitor(this)); 202 | } 203 | } 204 | 205 | 206 | class ConstructorVisitor extends BlockVisitor { 207 | 208 | Set initializedFieldNames = new Set(); 209 | ConstructorVisitor(cls): super(cls); 210 | 211 | String initializeFields() { 212 | var value; 213 | return cls.fields.where((f) => !f.isStatic).map((f) { 214 | if (f.value != null) { 215 | value = f.value.accept(this); 216 | } else { 217 | if (initializedFieldNames.contains(f.name)) { 218 | value = f.name; 219 | } else { 220 | value = "null"; 221 | } 222 | } 223 | return "this.${f.name} = $value;"; 224 | }).join('\n'); 225 | } 226 | 227 | visitConstructorDeclaration(ConstructorDeclaration node) { 228 | IndentedStringBuffer output = new IndentedStringBuffer() 229 | ..write('constructor') 230 | ..write(node.parameters.accept(this)) 231 | ..write(' {\n') 232 | ..write(node.body.accept(this).indent()) 233 | ..write('}'); 234 | return output; 235 | } 236 | 237 | visitBlockFunctionBody(BlockFunctionBody node) { 238 | IndentedStringBuffer output = new IndentedStringBuffer(initializeFields()) 239 | ..write('\n') 240 | ..write(super.visitBlockFunctionBody(node)); 241 | return output; 242 | } 243 | 244 | visitEmptyFunctionBody(EmptyFunctionBody node) { 245 | return new IndentedStringBuffer(initializeFields()); 246 | } 247 | 248 | visitFieldFormalParameter(FieldFormalParameter node) { 249 | assert(node.parameters == null); 250 | var name = node.identifier.toString(); 251 | initializedFieldNames.add(name); 252 | return name; 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /lib/visitor_null.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/analyzer.dart'; 2 | import 'writer.dart'; 3 | import 'dart:math' show min; 4 | 5 | 6 | /** 7 | * All visitors extend this so when a visit method for a node type has not been implemented, 8 | * an error message can be produced. 9 | */ 10 | class NullVisitor implements AstVisitor { 11 | 12 | _complain(node) { 13 | String str = node.toString(); 14 | print("${this.runtimeType.toString().padLeft(17)} " + 15 | "<${node.runtimeType.toString()}>".padRight(33) + 16 | "${str.substring(0, min(str.length, 46))}"); 17 | return new IndentedStringBuffer("<${node.runtimeType}>"); 18 | } 19 | 20 | visitAdjacentStrings(AdjacentStrings node) => _complain(node); 21 | 22 | visitAnnotation(Annotation node) => _complain(node); 23 | 24 | visitArgumentList(ArgumentList node) => _complain(node); 25 | 26 | visitAsExpression(AsExpression node) => _complain(node); 27 | 28 | visitAssertStatement(AssertStatement assertStatement) => _complain(assertStatement); 29 | 30 | visitAssignmentExpression(AssignmentExpression node) => _complain(node); 31 | 32 | visitAwaitExpression(AwaitExpression node) => _complain(node); 33 | 34 | visitBinaryExpression(BinaryExpression node) => _complain(node); 35 | 36 | visitBlock(Block node) => _complain(node); 37 | 38 | visitBlockFunctionBody(BlockFunctionBody node) => _complain(node); 39 | 40 | visitBooleanLiteral(BooleanLiteral node) => _complain(node); 41 | 42 | visitBreakStatement(BreakStatement node) => _complain(node); 43 | 44 | visitCascadeExpression(CascadeExpression node) => _complain(node); 45 | 46 | visitCatchClause(CatchClause node) => _complain(node); 47 | 48 | visitClassDeclaration(ClassDeclaration node) => _complain(node); 49 | 50 | visitClassTypeAlias(ClassTypeAlias node) => _complain(node); 51 | 52 | visitComment(Comment node) => _complain(node); 53 | 54 | visitCommentReference(CommentReference node) => _complain(node); 55 | 56 | visitCompilationUnit(CompilationUnit node) => _complain(node); 57 | 58 | visitConditionalExpression(ConditionalExpression node) => _complain(node); 59 | 60 | visitConstructorDeclaration(ConstructorDeclaration node) => _complain(node); 61 | 62 | visitConstructorFieldInitializer(ConstructorFieldInitializer node) => _complain(node); 63 | 64 | visitConstructorName(ConstructorName node) => _complain(node); 65 | 66 | visitContinueStatement(ContinueStatement node) => _complain(node); 67 | 68 | visitDeclaredIdentifier(DeclaredIdentifier node) => _complain(node); 69 | 70 | visitDefaultFormalParameter(DefaultFormalParameter node) => _complain(node); 71 | 72 | visitDoStatement(DoStatement node) => _complain(node); 73 | 74 | visitDoubleLiteral(DoubleLiteral node) => _complain(node); 75 | 76 | visitEmptyFunctionBody(EmptyFunctionBody node) => _complain(node); 77 | 78 | visitEmptyStatement(EmptyStatement node) => _complain(node); 79 | 80 | visitExportDirective(ExportDirective node) => _complain(node); 81 | 82 | visitExpressionFunctionBody(ExpressionFunctionBody node) => _complain(node); 83 | 84 | visitExpressionStatement(ExpressionStatement node) => _complain(node); 85 | 86 | visitExtendsClause(ExtendsClause node) => _complain(node); 87 | 88 | visitFieldDeclaration(FieldDeclaration node) => _complain(node); 89 | 90 | visitFieldFormalParameter(FieldFormalParameter node) => _complain(node); 91 | 92 | visitForEachStatement(ForEachStatement node) => _complain(node); 93 | 94 | visitFormalParameterList(FormalParameterList node) => _complain(node); 95 | 96 | visitForStatement(ForStatement node) => _complain(node); 97 | 98 | visitFunctionDeclaration(FunctionDeclaration node) => _complain(node); 99 | 100 | visitFunctionDeclarationStatement(FunctionDeclarationStatement node) => _complain(node); 101 | 102 | visitFunctionExpression(FunctionExpression node) => _complain(node); 103 | 104 | visitFunctionExpressionInvocation(FunctionExpressionInvocation node) => _complain(node); 105 | 106 | visitFunctionTypeAlias(FunctionTypeAlias functionTypeAlias) => _complain(functionTypeAlias); 107 | 108 | visitFunctionTypedFormalParameter(FunctionTypedFormalParameter node) => _complain(node); 109 | 110 | visitHideCombinator(HideCombinator node) => _complain(node); 111 | 112 | visitIfStatement(IfStatement node) => _complain(node); 113 | 114 | visitImplementsClause(ImplementsClause node) => _complain(node); 115 | 116 | visitImportDirective(ImportDirective node) => _complain(node); 117 | 118 | visitIndexExpression(IndexExpression node) => _complain(node); 119 | 120 | visitInstanceCreationExpression(InstanceCreationExpression node) => _complain(node); 121 | 122 | visitIntegerLiteral(IntegerLiteral node) => _complain(node); 123 | 124 | visitInterpolationExpression(InterpolationExpression node) => _complain(node); 125 | 126 | visitInterpolationString(InterpolationString node) => _complain(node); 127 | 128 | visitIsExpression(IsExpression node) => _complain(node); 129 | 130 | visitLabel(Label node) => _complain(node); 131 | 132 | visitLabeledStatement(LabeledStatement node) => _complain(node); 133 | 134 | visitLibraryDirective(LibraryDirective node) => _complain(node); 135 | 136 | visitLibraryIdentifier(LibraryIdentifier node) => _complain(node); 137 | 138 | visitListLiteral(ListLiteral node) => _complain(node); 139 | 140 | visitMapLiteral(MapLiteral node) => _complain(node); 141 | 142 | visitMapLiteralEntry(MapLiteralEntry node) => _complain(node); 143 | 144 | visitMethodDeclaration(MethodDeclaration node) => _complain(node); 145 | 146 | visitMethodInvocation(MethodInvocation node) => _complain(node); 147 | 148 | visitNamedExpression(NamedExpression node) => _complain(node); 149 | 150 | visitNativeClause(NativeClause node) => _complain(node); 151 | 152 | visitNativeFunctionBody(NativeFunctionBody node) => _complain(node); 153 | 154 | visitNullLiteral(NullLiteral node) => _complain(node); 155 | 156 | visitParenthesizedExpression(ParenthesizedExpression node) => _complain(node); 157 | 158 | visitPartDirective(PartDirective node) => _complain(node); 159 | 160 | visitPartOfDirective(PartOfDirective node) => _complain(node); 161 | 162 | visitPostfixExpression(PostfixExpression node) => _complain(node); 163 | 164 | visitPrefixedIdentifier(PrefixedIdentifier node) => _complain(node); 165 | 166 | visitPrefixExpression(PrefixExpression node) => _complain(node); 167 | 168 | visitPropertyAccess(PropertyAccess node) => _complain(node); 169 | 170 | visitRedirectingConstructorInvocation(RedirectingConstructorInvocation node) => _complain(node); 171 | 172 | visitRethrowExpression(RethrowExpression node) => _complain(node); 173 | 174 | visitReturnStatement(ReturnStatement node) => _complain(node); 175 | 176 | visitScriptTag(ScriptTag node) => _complain(node); 177 | 178 | visitShowCombinator(ShowCombinator node) => _complain(node); 179 | 180 | visitSimpleFormalParameter(SimpleFormalParameter node) => _complain(node); 181 | 182 | visitSimpleIdentifier(SimpleIdentifier node) => _complain(node); 183 | 184 | visitSimpleStringLiteral(SimpleStringLiteral node) => _complain(node); 185 | 186 | visitStringInterpolation(StringInterpolation node) => _complain(node); 187 | 188 | visitSuperConstructorInvocation(SuperConstructorInvocation node) => _complain(node); 189 | 190 | visitSuperExpression(SuperExpression node) => _complain(node); 191 | 192 | visitSwitchCase(SwitchCase node) => _complain(node); 193 | 194 | visitSwitchDefault(SwitchDefault node) => _complain(node); 195 | 196 | visitSwitchStatement(SwitchStatement node) => _complain(node); 197 | 198 | visitSymbolLiteral(SymbolLiteral node) => _complain(node); 199 | 200 | visitThisExpression(ThisExpression node) => _complain(node); 201 | 202 | visitThrowExpression(ThrowExpression node) => _complain(node); 203 | 204 | visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) => _complain(node); 205 | 206 | visitTryStatement(TryStatement node) => _complain(node); 207 | 208 | visitTypeArgumentList(TypeArgumentList node) => _complain(node); 209 | 210 | visitTypeName(TypeName node) => _complain(node); 211 | 212 | visitTypeParameter(TypeParameter node) => _complain(node); 213 | 214 | visitTypeParameterList(TypeParameterList node) => _complain(node); 215 | 216 | visitVariableDeclaration(VariableDeclaration node) => _complain(node); 217 | 218 | visitVariableDeclarationList(VariableDeclarationList node) => _complain(node); 219 | 220 | visitVariableDeclarationStatement(VariableDeclarationStatement node) => _complain(node); 221 | 222 | visitWhileStatement(WhileStatement node) => _complain(node); 223 | 224 | visitWithClause(WithClause node) => _complain(node); 225 | 226 | visitYieldStatement(YieldStatement node) => _complain(node); 227 | } 228 | -------------------------------------------------------------------------------- /test/samples/watchgroup.dart: -------------------------------------------------------------------------------- 1 | class WatchGroup implements _EvalWatchList, _WatchGroupList { 2 | /** A unique ID for the WatchGroup */ 3 | final String id; 4 | /** 5 | * A marker to be inserted when a group has no watches. We need the marker to 6 | * hold our position information in the linked list of all [Watch]es. 7 | */ 8 | final _EvalWatchRecord _marker = new _EvalWatchRecord.marker(); 9 | 10 | /** All Expressions are evaluated against a context object. */ 11 | final Object context; 12 | 13 | /** [ChangeDetector] used for field watching */ 14 | final ChangeDetectorGroup<_Handler> _changeDetector; 15 | /** A cache for sharing sub expression watching. Watching `a` and `a.b` will 16 | * watch `a` only once. */ 17 | final Map> _cache; 18 | final RootWatchGroup _rootGroup; 19 | 20 | /// STATS: Number of field watchers which are in use. 21 | int _fieldCost = 0; 22 | int _collectionCost = 0; 23 | int _evalCost = 0; 24 | 25 | /// STATS: Number of field watchers which are in use including child [WatchGroup]s. 26 | int get fieldCost => _fieldCost; 27 | int get totalFieldCost { 28 | var cost = _fieldCost; 29 | WatchGroup group = _watchGroupHead; 30 | while (group != null) { 31 | cost += group.totalFieldCost; 32 | group = group._nextWatchGroup; 33 | } 34 | return cost; 35 | } 36 | 37 | /// STATS: Number of collection watchers which are in use including child [WatchGroup]s. 38 | int get collectionCost => _collectionCost; 39 | int get totalCollectionCost { 40 | var cost = _collectionCost; 41 | WatchGroup group = _watchGroupHead; 42 | while (group != null) { 43 | cost += group.totalCollectionCost; 44 | group = group._nextWatchGroup; 45 | } 46 | return cost; 47 | } 48 | 49 | /// STATS: Number of invocation watchers (closures/methods) which are in use. 50 | int get evalCost => _evalCost; 51 | 52 | /// STATS: Number of invocation watchers which are in use including child [WatchGroup]s. 53 | int get totalEvalCost { 54 | var cost = _evalCost; 55 | WatchGroup group = _watchGroupHead; 56 | while (group != null) { 57 | cost += group.evalCost; 58 | group = group._nextWatchGroup; 59 | } 60 | return cost; 61 | } 62 | 63 | int _nextChildId = 0; 64 | _EvalWatchRecord _evalWatchHead, _evalWatchTail; 65 | /// Pointer for creating tree of [WatchGroup]s. 66 | WatchGroup _parentWatchGroup; 67 | WatchGroup _watchGroupHead, _watchGroupTail; 68 | WatchGroup _prevWatchGroup, _nextWatchGroup; 69 | 70 | WatchGroup._child(_parentWatchGroup2, this._changeDetector, this.context, 71 | this._cache, this._rootGroup) 72 | : _parentWatchGroup = _parentWatchGroup2, 73 | id = '${_parentWatchGroup.id}.${_parentWatchGroup._nextChildId++}' 74 | { 75 | _marker.watchGrp = this; 76 | _evalWatchTail = _evalWatchHead = _marker; 77 | } 78 | 79 | WatchGroup._root(this._changeDetector, this.context) 80 | : id = '', 81 | _rootGroup = null, 82 | _parentWatchGroup = null, 83 | _cache = new HashMap>() 84 | { 85 | _marker.watchGrp = this; 86 | _evalWatchTail = _evalWatchHead = _marker; 87 | } 88 | 89 | get isAttached { 90 | var group = this; 91 | var root = _rootGroup; 92 | while (group != null) { 93 | if (group == root){ 94 | return true; 95 | } 96 | group = group._parentWatchGroup; 97 | } 98 | return false; 99 | } 100 | 101 | Watch watch(AST expression, ReactionFn reactionFn) { 102 | WatchRecord<_Handler> watchRecord = _cache[expression.expression]; 103 | if (watchRecord == null) { 104 | _cache[expression.expression] = watchRecord = expression.setupWatch(this); 105 | } 106 | return watchRecord.handler.addReactionFn(reactionFn); 107 | } 108 | 109 | /** 110 | * Watch a [name] field on [lhs] represented by [expression]. 111 | * 112 | * - [name] the field to watch. 113 | * - [lhs] left-hand-side of the field. 114 | */ 115 | WatchRecord<_Handler> addFieldWatch(AST lhs, String name, String expression) { 116 | var fieldHandler = new _FieldHandler(this, expression); 117 | 118 | // Create a Record for the current field and assign the change record 119 | // to the handler. 120 | var watchRecord = _changeDetector.watch(null, name, fieldHandler); 121 | _fieldCost++; 122 | fieldHandler.watchRecord = watchRecord; 123 | 124 | WatchRecord<_Handler> lhsWR = _cache[lhs.expression]; 125 | if (lhsWR == null) { 126 | lhsWR = _cache[lhs.expression] = lhs.setupWatch(this); 127 | } 128 | 129 | // We set a field forwarding handler on LHS. This will allow the change 130 | // objects to propagate to the current WatchRecord. 131 | lhsWR.handler.addForwardHandler(fieldHandler); 132 | 133 | // propagate the value from the LHS to here 134 | fieldHandler.acceptValue(lhsWR.currentValue); 135 | return watchRecord; 136 | } 137 | 138 | WatchRecord<_Handler> addCollectionWatch(AST ast) { 139 | var collectionHandler = new _CollectionHandler(this, ast.expression); 140 | var watchRecord = _changeDetector.watch(null, null, collectionHandler); 141 | _collectionCost++; 142 | collectionHandler.watchRecord = watchRecord; 143 | WatchRecord<_Handler> astWR = _cache[ast.expression]; 144 | if (astWR == null) { 145 | astWR = _cache[ast.expression] = ast.setupWatch(this); 146 | } 147 | 148 | // We set a field forwarding handler on LHS. This will allow the change 149 | // objects to propagate to the current WatchRecord. 150 | astWR.handler.addForwardHandler(collectionHandler); 151 | 152 | // propagate the value from the LHS to here 153 | collectionHandler.acceptValue(astWR.currentValue); 154 | return watchRecord; 155 | } 156 | 157 | /** 158 | * Watch a [fn] function represented by an [expression]. 159 | * 160 | * - [fn] function to evaluate. 161 | * - [argsAST] list of [AST]es which represent arguments passed to function. 162 | * - [expression] normalized expression used for caching. 163 | * - [isPure] A pure function is one which holds no internal state. This implies that the 164 | * function is idempotent. 165 | */ 166 | _EvalWatchRecord addFunctionWatch(Function fn, List argsAST, 167 | Map namedArgsAST, 168 | String expression, bool isPure) => 169 | _addEvalWatch(null, fn, null, argsAST, namedArgsAST, expression, isPure); 170 | 171 | /** 172 | * Watch a method [name]ed represented by an [expression]. 173 | * 174 | * - [lhs] left-hand-side of the method. 175 | * - [name] name of the method. 176 | * - [argsAST] list of [AST]es which represent arguments passed to method. 177 | * - [expression] normalized expression used for caching. 178 | */ 179 | _EvalWatchRecord addMethodWatch(AST lhs, String name, List argsAST, 180 | Map namedArgsAST, 181 | String expression) => 182 | _addEvalWatch(lhs, null, name, argsAST, namedArgsAST, expression, false); 183 | 184 | 185 | 186 | _EvalWatchRecord _addEvalWatch(AST lhsAST, Function fn, String name, 187 | List argsAST, 188 | Map namedArgsAST, 189 | String expression, bool isPure) { 190 | _InvokeHandler invokeHandler = new _InvokeHandler(this, expression); 191 | var evalWatchRecord = new _EvalWatchRecord( 192 | _rootGroup._fieldGetterFactory, this, invokeHandler, fn, name, 193 | argsAST.length, isPure); 194 | invokeHandler.watchRecord = evalWatchRecord; 195 | 196 | if (lhsAST != null) { 197 | var lhsWR = _cache[lhsAST.expression]; 198 | if (lhsWR == null) { 199 | lhsWR = _cache[lhsAST.expression] = lhsAST.setupWatch(this); 200 | } 201 | lhsWR.handler.addForwardHandler(invokeHandler); 202 | invokeHandler.acceptValue(lhsWR.currentValue); 203 | } 204 | 205 | // Convert the args from AST to WatchRecords 206 | for (var i = 0; i < argsAST.length; i++) { 207 | var ast = argsAST[i]; 208 | WatchRecord<_Handler> record = _cache[ast.expression]; 209 | if (record == null) { 210 | record = _cache[ast.expression] = ast.setupWatch(this); 211 | } 212 | _ArgHandler handler = new _PositionalArgHandler(this, evalWatchRecord, i); 213 | _ArgHandlerList._add(invokeHandler, handler); 214 | record.handler.addForwardHandler(handler); 215 | handler.acceptValue(record.currentValue); 216 | } 217 | 218 | namedArgsAST.forEach((Symbol name, AST ast) { 219 | WatchRecord<_Handler> record = _cache[ast.expression]; 220 | if (record == null) { 221 | record = _cache[ast.expression] = ast.setupWatch(this); 222 | } 223 | _ArgHandler handler = new _NamedArgHandler(this, evalWatchRecord, name); 224 | _ArgHandlerList._add(invokeHandler, handler); 225 | record.handler.addForwardHandler(handler); 226 | handler.acceptValue(record.currentValue); 227 | }); 228 | 229 | // Must be done last 230 | _EvalWatchList._add(this, evalWatchRecord); 231 | _evalCost++; 232 | if (_rootGroup.isInsideInvokeDirty) { 233 | // This check means that we are inside invoke reaction function. 234 | // Registering a new EvalWatch at this point will not run the 235 | // .check() on it which means it will not be processed, but its 236 | // reaction function will be run with null. So we process it manually. 237 | evalWatchRecord.check(); 238 | } 239 | return evalWatchRecord; 240 | } 241 | 242 | WatchGroup get _childWatchGroupTail { 243 | var tail = this, nextTail; 244 | while ((nextTail = tail._watchGroupTail) != null) { 245 | tail = nextTail; 246 | } 247 | return tail; 248 | } 249 | 250 | /** 251 | * Create a new child [WatchGroup]. 252 | * 253 | * - [context] if present the the child [WatchGroup] expressions will evaluate 254 | * against the new [context]. If not present than child expressions will 255 | * evaluate on same context allowing the reuse of the expression cache. 256 | */ 257 | WatchGroup newGroup([Object context]) { 258 | _EvalWatchRecord prev = _childWatchGroupTail._evalWatchTail; 259 | _EvalWatchRecord next = prev._nextEvalWatch; 260 | var childGroup = new WatchGroup._child( 261 | this, 262 | _changeDetector.newGroup(), 263 | context == null ? this.context : context, 264 | new HashMap>(), 265 | _rootGroup == null ? this : _rootGroup); 266 | _WatchGroupList._add(this, childGroup); 267 | var marker = childGroup._marker; 268 | 269 | marker._prevEvalWatch = prev; 270 | marker._nextEvalWatch = next; 271 | prev._nextEvalWatch = marker; 272 | if (next != null) next._prevEvalWatch = marker; 273 | 274 | return childGroup; 275 | } 276 | 277 | /** 278 | * Remove/destroy [WatchGroup] and all of its [Watches]. 279 | */ 280 | void remove() { 281 | // TODO:(misko) This code is not right. 282 | // 1) It fails to release [ChangeDetector] [WatchRecord]s. 283 | 284 | _WatchGroupList._remove(_parentWatchGroup, this); 285 | _nextWatchGroup = _prevWatchGroup = null; 286 | _changeDetector.remove(); 287 | _rootGroup._removeCount++; 288 | _parentWatchGroup = null; 289 | 290 | // Unlink the _watchRecord 291 | _EvalWatchRecord firstEvalWatch = _evalWatchHead; 292 | _EvalWatchRecord lastEvalWatch = _childWatchGroupTail._evalWatchTail; 293 | _EvalWatchRecord previous = firstEvalWatch._prevEvalWatch; 294 | _EvalWatchRecord next = lastEvalWatch._nextEvalWatch; 295 | if (previous != null) previous._nextEvalWatch = next; 296 | if (next != null) next._prevEvalWatch = previous; 297 | _evalWatchHead._prevEvalWatch = null; 298 | _evalWatchTail._nextEvalWatch = null; 299 | _evalWatchHead = _evalWatchTail = null; 300 | } 301 | 302 | toString() { 303 | var lines = []; 304 | if (this == _rootGroup) { 305 | var allWatches = []; 306 | var watch = _evalWatchHead; 307 | var prev = null; 308 | while (watch != null) { 309 | allWatches.add(watch.toString()); 310 | assert(watch._prevEvalWatch == prev); 311 | prev = watch; 312 | watch = watch._nextEvalWatch; 313 | } 314 | lines.add('WATCHES: ${allWatches.join(', ')}'); 315 | } 316 | 317 | var watches = []; 318 | var watch = _evalWatchHead; 319 | while (watch != _evalWatchTail) { 320 | watches.add(watch.toString()); 321 | watch = watch._nextEvalWatch; 322 | } 323 | watches.add(watch.toString()); 324 | 325 | lines.add('WatchGroup[$id](watches: ${watches.join(', ')})'); 326 | var childGroup = _watchGroupHead; 327 | while (childGroup != null) { 328 | lines.add(' ' + childGroup.toString().replaceAll('\n', '\n ')); 329 | childGroup = childGroup._nextWatchGroup; 330 | } 331 | return lines.join('\n'); 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /lib/visitor_block.dart: -------------------------------------------------------------------------------- 1 | part of dart2es6.visitor; 2 | 3 | class BlockVisitor extends NullVisitor { 4 | 5 | final ClassVisitor cls; 6 | String errorVariableName; 7 | 8 | BlockVisitor([this.cls = null]); 9 | 10 | Field _getField(name) { 11 | if (cls == null) return null; 12 | if (name is SimpleIdentifier) name = name.name; 13 | var fields = cls.fields.where((f) => f.name == name); 14 | if (fields.isEmpty) return null; 15 | assert (fields.length == 1); 16 | return fields.first; 17 | } 18 | 19 | _checkDeclarationShadowing(String name) { 20 | var shadows = cls == null ? [] : cls.fields.where((f) => f.name == name); 21 | if (shadows.isNotEmpty) { 22 | throw "Variable shadows field: ${name}" + (cls == null ? "" : " in ${cls.name}"); 23 | } 24 | shadows = GLOBAL_REPLACE.keys.where((f) => f == name); 25 | if (shadows.isNotEmpty) throw "Variable shadows global/builtin: ${name}"; 26 | } 27 | 28 | visitAdjacentStrings(AdjacentStrings node) { 29 | return node.strings.map((s) => s.accept(this)).join(' + '); 30 | } 31 | 32 | visitArgumentList(ArgumentList node) { 33 | var buffer = new IndentedStringBuffer('('); 34 | buffer.write(node.arguments.map((a) => a.accept(this)).join(', ')); 35 | buffer.write(')'); 36 | return buffer; 37 | } 38 | 39 | visitAsExpression(AsExpression node) => node.expression.accept(this); 40 | 41 | visitAssertStatement(AssertStatement node) { 42 | // TODO: Keep asserts or not? 43 | return "/* console.assert(${node.condition.accept(this)}); */"; 44 | } 45 | 46 | visitAssignmentExpression(AssignmentExpression node) { 47 | assert(['=', '+=', '-=', '*=', '/=', '%=', '&=', '^=', '|=', '<<=', '>>='] 48 | .contains(node.operator.toString())); 49 | return "${node.leftHandSide.accept(this)} ${node.operator} ${node.rightHandSide.accept(this)}"; 50 | } 51 | 52 | visitBinaryExpression(BinaryExpression node) { 53 | var op = node.operator.toString(); 54 | assert(["+","-","*","/","%","==","!=","<","<=",">",">=","&&","||","&","|","^"].contains(op)); 55 | if (op == "==" || op == "!=") op += "="; 56 | return "${node.leftOperand.accept(this)} $op ${node.rightOperand.accept(this)}"; 57 | } 58 | 59 | visitBlock(Block node) { 60 | IndentedStringBuffer output = new IndentedStringBuffer(); 61 | output.write(node.statements.map((s) => s.accept(this)).join('\n')); 62 | return output; 63 | } 64 | 65 | visitBlockFunctionBody(BlockFunctionBody node) { 66 | return node.block.accept(this); 67 | } 68 | 69 | visitBooleanLiteral(BooleanLiteral node) => node.literal.toString(); 70 | 71 | visitBreakStatement(BreakStatement node) => "break;"; 72 | 73 | visitConditionalExpression(ConditionalExpression node) { 74 | return "${node.condition.accept(this)} ? ${node.thenExpression.accept(this)}" 75 | " : ${node.elseExpression.accept(this)}"; 76 | } 77 | 78 | visitConstructorName(ConstructorName node) { 79 | // TODO: implement different constructors 80 | // assert(node.name == null); TODO: uncomment 81 | return node.type.name.toString(); 82 | } 83 | 84 | visitComment(Comment node) { 85 | return node.tokens.map((t) => t.toString()).join().split("\n").map((line) { 86 | var trimmed = line.trim(); 87 | if (trimmed[0] == '*') trimmed = " $trimmed"; 88 | return trimmed; 89 | }).join('\n'); 90 | } 91 | 92 | visitDefaultFormalParameter(DefaultFormalParameter node) { 93 | // TODO: check for shadowing in methods 94 | return "${node.identifier.name} = " + 95 | (node.defaultValue == null ? "null" : node.defaultValue.accept(this).toString()); 96 | } 97 | 98 | visitDoStatement(DoStatement node) { 99 | var output = new IndentedStringBuffer() 100 | ..write("do {\n") 101 | ..write(new IndentedStringBuffer(node.body.accept(this)).indent()) 102 | ..write("}\nwhile(") 103 | ..write(node.condition.accept(this)) 104 | ..write(");"); 105 | return output; 106 | } 107 | 108 | visitDoubleLiteral(DoubleLiteral node) { 109 | return node.literal.toString(); 110 | } 111 | 112 | visitEmptyFunctionBody(EmptyFunctionBody node) { 113 | return 'throw new Error("abstract function not implemented");'; 114 | } 115 | 116 | visitExpressionFunctionBody(ExpressionFunctionBody node) { 117 | return "return ${node.expression.accept(this)};"; 118 | } 119 | 120 | visitExpressionStatement(ExpressionStatement node) { 121 | assert(node.semicolon != null); 122 | return "${node.expression.accept(this)};"; 123 | } 124 | 125 | visitFormalParameterList(FormalParameterList node) { 126 | var buffer = new IndentedStringBuffer('('); 127 | buffer.write(node.parameters 128 | .where((p) => p.kind == ParameterKind.REQUIRED || p.kind == ParameterKind.POSITIONAL) 129 | .map((p) => p.accept(this)) 130 | .join(', ')); 131 | // TODO: named arguments 132 | buffer.write(')'); 133 | return buffer; 134 | } 135 | 136 | visitForStatement(ForStatement node) { 137 | assert(node.initialization == null || node.variables == null); 138 | IndentedStringBuffer output = new IndentedStringBuffer() 139 | ..write("for (") 140 | ..write(node.initialization == null ? "": node.initialization.accept(this)) 141 | ..write(node.variables == null ? "" : node.variables.accept(this)) 142 | ..write("; ") 143 | ..write(node.condition.accept(this)) 144 | ..write("; ") 145 | ..write(node.updaters == null ? "" : node.updaters.map((e) => e.accept(this)).join(', ')) 146 | ..write(") {\n") 147 | ..write(new IndentedStringBuffer(node.body.accept(this)).indent()) 148 | ..write("}"); 149 | return output; 150 | } 151 | 152 | visitForEachStatement(ForEachStatement node) { 153 | assert(node.awaitKeyword == null); 154 | var identifier; 155 | if (node.loopVariable != null) { 156 | identifier = "var " + node.loopVariable.identifier.name; 157 | } else { 158 | identifier = node.identifier.name; 159 | } 160 | var output = new IndentedStringBuffer() 161 | ..write("for ($identifier of ") 162 | ..write(node.iterator.accept(this)) 163 | ..write(") {\n") 164 | ..write(new IndentedStringBuffer(node.body.accept(this)).indent()) 165 | ..write("}"); 166 | return output; 167 | } 168 | 169 | visitFunctionDeclaration(FunctionDeclaration node) { 170 | assert(_doesNotShadow(node.name.name)); 171 | assert(node.isGetter == false); 172 | assert(node.isSetter == false); 173 | IndentedStringBuffer output = new IndentedStringBuffer() 174 | ..write("function ") 175 | ..write(node.name.name) 176 | ..write(node.functionExpression.parameters.accept(this)) 177 | ..write(" {\n") 178 | ..write(new IndentedStringBuffer(node.functionExpression.body.accept(this)).indent()) 179 | ..write("}"); 180 | return output; 181 | } 182 | 183 | visitFunctionDeclarationStatement(FunctionDeclarationStatement node) { 184 | return node.functionDeclaration.accept(this); 185 | } 186 | 187 | visitFunctionExpression(FunctionExpression node) { 188 | IndentedStringBuffer output = new IndentedStringBuffer() 189 | ..write(node.parameters.accept(this)) 190 | ..write(" => {\n") 191 | ..write(new IndentedStringBuffer(node.body.accept(this)).indent()) 192 | ..write("}"); 193 | return output; 194 | } 195 | 196 | visitFunctionExpressionInvocation(FunctionExpressionInvocation node) { 197 | var output = new IndentedStringBuffer() 198 | ..write("(function") 199 | ..write(node.argumentList.accept(this)) 200 | ..write(" {\n") 201 | ..write(new IndentedStringBuffer(node.function.accept(new BlockVisitor(cls))).indent()) 202 | ..write("})()"); 203 | return output; 204 | } 205 | 206 | visitFunctionTypedFormalParameter(FunctionTypedFormalParameter node) { 207 | return node.identifier.name; 208 | } 209 | 210 | visitIfStatement(IfStatement node) { 211 | IndentedStringBuffer output = new IndentedStringBuffer() 212 | ..write("if (") 213 | ..write(node.condition.accept(this)) 214 | ..write(") {\n") 215 | ..write(new IndentedStringBuffer(node.thenStatement.accept(this)).indent()) 216 | ..write("}"); 217 | if (node.elseStatement != null) { 218 | output.write(" else "); 219 | if (node.elseStatement is IfStatement) { 220 | output.write(node.elseStatement.accept(this)); 221 | } else { 222 | output..write("{\n") 223 | ..write(new IndentedStringBuffer(node.elseStatement.accept(this)).indent()) 224 | ..write("}"); 225 | } 226 | } 227 | return output; 228 | } 229 | 230 | visitIndexExpression(IndexExpression node) { 231 | assert(node.isCascaded == false); 232 | return "${node.target.accept(this)}[${node.index.accept(this)}]"; 233 | } 234 | 235 | visitInstanceCreationExpression(InstanceCreationExpression node) { 236 | var typeName = node.constructorName.type.name.name; 237 | if (["Map", "HashMap"].contains(typeName)) { 238 | assert(node.argumentList.accept(this).toString() == "()"); 239 | return "{}"; 240 | } else if (typeName == "List") { 241 | if (node.argumentList.accept(this).toString() != "()") { 242 | print("Arguments to List constructor ignored: $node"); 243 | } // TODO: List.generate, List(size). 244 | return "[]"; 245 | } 246 | return "new ${node.constructorName.accept(this)}${node.argumentList.accept(this)}"; 247 | } 248 | 249 | visitIntegerLiteral(IntegerLiteral node) { 250 | return node.literal.toString(); 251 | } 252 | 253 | visitInterpolationExpression(InterpolationExpression node) { 254 | return "\${${node.expression.accept(this)}}"; 255 | } 256 | 257 | visitInterpolationString(InterpolationString node) { 258 | return node.contents.toString(); 259 | } 260 | 261 | visitIsExpression(IsExpression node) { 262 | var types = { 263 | "num" : "number", 264 | "String" : "string", 265 | "Map" : "object", 266 | "List" : "object", 267 | "Function" : "function", 268 | }; 269 | var type = node.type.name.name; 270 | var jsType = types[type]; 271 | if (jsType == null) { 272 | return "${node.expression.accept(this)} instanceof $type"; // TODO: make this better 273 | } 274 | return "typeof ${node.expression.accept(this)} == '$jsType'"; 275 | } 276 | 277 | visitListLiteral(ListLiteral node) { 278 | return "[${node.elements.map((e) => e.accept(this)).join(', ')}]"; 279 | } 280 | 281 | visitMapLiteral(MapLiteral node) { 282 | return "{" + node.entries.map((n) => n.accept(this)).join(", ") + "}"; 283 | } 284 | 285 | visitMapLiteralEntry(MapLiteralEntry node) { 286 | return "${node.key.accept(this)} : ${node.value.accept(this)}"; 287 | } 288 | 289 | visitMethodDeclaration(MethodDeclaration node) { 290 | IndentedStringBuffer output = new IndentedStringBuffer(); 291 | if (node.documentationComment != null) { 292 | output.writeln(node.documentationComment.accept(this)); 293 | } 294 | if (node.isGetter) output.write('get '); 295 | if (node.isSetter) output.write('set '); 296 | output.write(node.name); 297 | if (node.parameters != null) { 298 | output.write(node.parameters.accept(this)); 299 | output.write(' '); 300 | } else { 301 | output.write('() '); 302 | } 303 | output.write("{\n"); 304 | var body = node.body.accept(this); 305 | if (body is! IndentedStringBuffer) body = new IndentedStringBuffer(body); 306 | output.write(body.indent()); 307 | output.write('}'); 308 | return output; 309 | } 310 | 311 | visitMethodInvocation(MethodInvocation node) { 312 | assert(!node.isCascaded); 313 | var target = ""; 314 | String method = node.methodName.accept(this); 315 | if (node.target != null) { 316 | assert(node.period != null); 317 | target = '${node.target.accept(this)}.'; 318 | method = _replacedField(node.target.bestType, method); 319 | } else if (cls != null && cls.methods.where((f) => f.name == node.methodName.name).isNotEmpty) { 320 | target = "this."; 321 | } 322 | return "$target$method${node.argumentList.accept(this)}"; 323 | } 324 | 325 | visitNullLiteral(NullLiteral node) => "null"; 326 | 327 | visitParenthesizedExpression(ParenthesizedExpression node) { 328 | return "(${node.expression.accept(this)})"; 329 | } 330 | 331 | visitPostfixExpression(PostfixExpression node) { 332 | assert(["++", "--"].contains(node.operator.toString())); 333 | return "${node.operand.accept(this)}${node.operator}"; 334 | } 335 | 336 | visitPrefixExpression(PrefixExpression node) { 337 | assert(["!", "++", "--", "-"].contains(node.operator.toString())); 338 | return "${node.operator}${node.operand.accept(this)}"; 339 | } 340 | 341 | visitPrefixedIdentifier(PrefixedIdentifier node) { 342 | return "${node.prefix.accept(this)}.${node.identifier.name}"; 343 | } 344 | 345 | visitPropertyAccess(PropertyAccess node) { 346 | assert(!node.isCascaded); 347 | return "${node.target.accept(this)}.${node.propertyName.name}"; 348 | } 349 | 350 | visitRethrowExpression(RethrowExpression node) { 351 | assert(errorVariableName != null); 352 | return "throw $errorVariableName"; 353 | } 354 | 355 | visitReturnStatement(ReturnStatement node) { 356 | if (node.expression == null) return "return;"; 357 | return "return ${node.expression.accept(this)};"; 358 | } 359 | 360 | visitSimpleFormalParameter(SimpleFormalParameter node) { 361 | return node.identifier.name; // TODO: check for shadowing in methods 362 | } 363 | 364 | visitSimpleIdentifier(SimpleIdentifier node) { 365 | var name = node.name; 366 | // unresolved check, broken by shadowing, but should work usually 367 | var field = _getField(name); 368 | if (field != null) { 369 | assert(!GLOBAL_REPLACE.containsKey(name)); 370 | if (field.isStatic) { 371 | return "${cls.name}.$name"; 372 | } else { 373 | return "this.$name"; 374 | } 375 | } 376 | if (GLOBAL_REPLACE.containsKey(name)) return GLOBAL_REPLACE[name]; 377 | return name; 378 | } 379 | 380 | visitSimpleStringLiteral(SimpleStringLiteral node) { 381 | assert(!node.isMultiline && !node.isRaw); 382 | return node.literal.toString(); 383 | } 384 | 385 | visitStringInterpolation(StringInterpolation node) { 386 | // TODO: Multiline 387 | return '${node.elements.map((e) => e.accept(this)).join()}'; 388 | } 389 | 390 | visitSuperExpression(SuperExpression node) => "super"; 391 | 392 | visitSwitchCase(SwitchCase node) { 393 | var statements = node.statements.map((s) => s.accept(this)).join('\n'); 394 | var output = new IndentedStringBuffer() 395 | ..write("case (") 396 | ..write(node.expression.accept(this)) 397 | ..write("):\n") 398 | ..write(new IndentedStringBuffer(statements).indent()); 399 | return output; 400 | } 401 | 402 | visitSwitchDefault(SwitchDefault node) { 403 | var statements = node.statements.map((s) => s.accept(this)).join("\n"); 404 | return "default:\n" + new IndentedStringBuffer(statements).indent().toString(); 405 | } 406 | 407 | visitSwitchStatement(SwitchStatement node) { 408 | var members = node.members.map((m) => m.accept(this)).join('\n'); 409 | var output = new IndentedStringBuffer() 410 | ..write("switch (") 411 | ..write(node.expression.accept(this)) 412 | ..write(") {\n") 413 | ..write(new IndentedStringBuffer(members).indent()) 414 | ..write("}"); 415 | return output; 416 | } 417 | 418 | visitThisExpression(ThisExpression node) => "this"; 419 | 420 | visitThrowExpression(ThrowExpression node) => "throw ${node.expression.accept(this)}"; 421 | 422 | visitTryStatement(TryStatement node) { 423 | var output = new IndentedStringBuffer() 424 | ..write("try {\n") 425 | ..write(new IndentedStringBuffer(node.body.accept(this)).indent()) 426 | ..write("}"); 427 | if (node.catchClauses.isNotEmpty) { 428 | assert(node.catchClauses.length == 1); 429 | var catchClause = node.catchClauses.first; 430 | assert(catchClause.onKeyword == null); 431 | var catchVisitor = new BlockVisitor(cls) 432 | ..errorVariableName = catchClause.exceptionParameter.name; 433 | var exceptionVar = catchClause.exceptionParameter.name; 434 | var stackParam = catchClause.stackTraceParameter; 435 | var catchOutput = new IndentedStringBuffer(); 436 | if (stackParam != null) { 437 | catchOutput.writeln("var ${stackParam.name} = ${exceptionVar}.stack;"); 438 | } 439 | catchOutput.write(catchClause.body.accept(catchVisitor)); 440 | output 441 | ..write(" catch (${exceptionVar}) {\n") 442 | ..write(catchOutput.indent()) 443 | ..write("}"); 444 | } 445 | if (node.finallyBlock != null) { 446 | output..write(" finally {\n") 447 | ..write(new IndentedStringBuffer(node.finallyBlock.accept(this)).indent()) 448 | ..write("}"); 449 | } 450 | return output; 451 | } 452 | 453 | visitVariableDeclaration(VariableDeclaration node) { 454 | _checkDeclarationShadowing(node.name.toString()); 455 | var initializer = node.initializer == null ? "null" : node.initializer.accept(this); 456 | return "${node.name.toString()} = $initializer"; 457 | } 458 | 459 | visitVariableDeclarationList(VariableDeclarationList node) { 460 | var vars = node.variables.map((v) => v.accept(this)).join(', '); 461 | return 'var $vars'; // TODO: types, const, final 462 | } 463 | 464 | visitVariableDeclarationStatement(VariableDeclarationStatement node) { 465 | return "${node.variables.accept(this)};"; 466 | } 467 | 468 | visitWhileStatement(WhileStatement node) { 469 | IndentedStringBuffer output = new IndentedStringBuffer() 470 | ..write("while (") 471 | ..write(node.condition.accept(this)) 472 | ..write(") {\n") 473 | ..write(new IndentedStringBuffer(node.body.accept(this)).indent()) 474 | ..write("}"); 475 | return output; 476 | } 477 | } 478 | -------------------------------------------------------------------------------- /change_detection/lib/watch_group.dart: -------------------------------------------------------------------------------- 1 | library angular.watch_group; 2 | 3 | import 'change_detection.dart'; 4 | import 'dart:collection'; 5 | 6 | part 'linked_list.dart'; 7 | part 'ast.dart'; 8 | part 'prototype_map.dart'; 9 | 10 | /** 11 | * A function that is notified of changes to the model. 12 | * 13 | * ReactionFn is a function implemented by the developer that executes when a change is detected 14 | * in a watched expression. 15 | * 16 | * * [value]: The current value of the watched expression. 17 | * * [previousValue]: The previous value of the watched expression. 18 | * 19 | * If the expression is watching a collection (a list or a map), then [value] is wrapped in 20 | * a [CollectionChangeItem] that lists all the changes. 21 | */ 22 | typedef void ReactionFn(value, previousValue); 23 | typedef void ChangeLog(String expression, current, previous); 24 | 25 | /** 26 | * Extend this class if you wish to pretend to be a function, but you don't know 27 | * number of arguments with which the function will get called with. 28 | */ 29 | abstract class FunctionApply { 30 | dynamic call() { throw new StateError('Use apply()'); } 31 | dynamic apply(List arguments); 32 | } 33 | 34 | /** 35 | * [WatchGroup] is a logical grouping of a set of watches. [WatchGroup]s are 36 | * organized into a hierarchical tree parent-children configuration. 37 | * [WatchGroup] builds upon [ChangeDetector] and adds expression (field chains 38 | * as in `a.b.c`) support as well as support function/closure/method (function 39 | * invocation as in `a.b()`) watching. 40 | */ 41 | class WatchGroup implements _EvalWatchList, _WatchGroupList { 42 | /** A unique ID for the WatchGroup */ 43 | final String id; 44 | /** 45 | * A marker to be inserted when a group has no watches. We need the marker to 46 | * hold our position information in the linked list of all [Watch]es. 47 | */ 48 | final _EvalWatchRecord _marker = new _EvalWatchRecord.marker(); 49 | 50 | /** All Expressions are evaluated against a context object. */ 51 | final Object context; 52 | 53 | /** [ChangeDetector] used for field watching */ 54 | final ChangeDetectorGroup<_Handler> _changeDetector; 55 | /** A cache for sharing sub expression watching. Watching `a` and `a.b` will 56 | * watch `a` only once. */ 57 | final Map> _cache; 58 | final RootWatchGroup _rootGroup; 59 | 60 | /// STATS: Number of field watchers which are in use. 61 | int _fieldCost = 0; 62 | int _collectionCost = 0; 63 | int _evalCost = 0; 64 | 65 | /// STATS: Number of field watchers which are in use including child [WatchGroup]s. 66 | int get fieldCost => _fieldCost; 67 | int get totalFieldCost { 68 | var cost = _fieldCost; 69 | WatchGroup group = _watchGroupHead; 70 | while (group != null) { 71 | cost += group.totalFieldCost; 72 | group = group._nextWatchGroup; 73 | } 74 | return cost; 75 | } 76 | 77 | /// STATS: Number of collection watchers which are in use including child [WatchGroup]s. 78 | int get collectionCost => _collectionCost; 79 | int get totalCollectionCost { 80 | var cost = _collectionCost; 81 | WatchGroup group = _watchGroupHead; 82 | while (group != null) { 83 | cost += group.totalCollectionCost; 84 | group = group._nextWatchGroup; 85 | } 86 | return cost; 87 | } 88 | 89 | /// STATS: Number of invocation watchers (closures/methods) which are in use. 90 | int get evalCost => _evalCost; 91 | 92 | /// STATS: Number of invocation watchers which are in use including child [WatchGroup]s. 93 | int get totalEvalCost { 94 | var cost = _evalCost; 95 | WatchGroup group = _watchGroupHead; 96 | while (group != null) { 97 | cost += group.evalCost; 98 | group = group._nextWatchGroup; 99 | } 100 | return cost; 101 | } 102 | 103 | int _nextChildId = 0; 104 | _EvalWatchRecord _evalWatchHead, _evalWatchTail; 105 | /// Pointer for creating tree of [WatchGroup]s. 106 | WatchGroup _parentWatchGroup; 107 | WatchGroup _watchGroupHead, _watchGroupTail; 108 | WatchGroup _prevWatchGroup, _nextWatchGroup; 109 | 110 | WatchGroup._child(_parentWatchGroup, this._changeDetector, this.context, 111 | this._cache, this._rootGroup) 112 | : _parentWatchGroup = _parentWatchGroup, 113 | id = '${_parentWatchGroup.id}.${_parentWatchGroup._nextChildId++}' 114 | { 115 | _marker.watchGrp = this; 116 | _evalWatchTail = _evalWatchHead = _marker; 117 | } 118 | 119 | WatchGroup._root(this._changeDetector, this.context) 120 | : id = '', 121 | _rootGroup = null, 122 | _parentWatchGroup = null, 123 | _cache = new HashMap>() 124 | { 125 | _marker.watchGrp = this; 126 | _evalWatchTail = _evalWatchHead = _marker; 127 | } 128 | 129 | get isAttached { 130 | var group = this; 131 | var root = _rootGroup; 132 | while (group != null) { 133 | if (group == root){ 134 | return true; 135 | } 136 | group = group._parentWatchGroup; 137 | } 138 | return false; 139 | } 140 | 141 | Watch watch(AST expression, ReactionFn reactionFn) { 142 | WatchRecord<_Handler> watchRecord = _cache[expression.expression]; 143 | if (watchRecord == null) { 144 | _cache[expression.expression] = watchRecord = expression.setupWatch(this); 145 | } 146 | return watchRecord.handler.addReactionFn(reactionFn); 147 | } 148 | 149 | /** 150 | * Watch a [name] field on [lhs] represented by [expression]. 151 | * 152 | * - [name] the field to watch. 153 | * - [lhs] left-hand-side of the field. 154 | */ 155 | WatchRecord<_Handler> addFieldWatch(AST lhs, String name, String expression) { 156 | var fieldHandler = new _FieldHandler(this, expression); 157 | 158 | // Create a Record for the current field and assign the change record 159 | // to the handler. 160 | var watchRecord = _changeDetector.watch(null, name, fieldHandler); 161 | _fieldCost++; 162 | fieldHandler.watchRecord = watchRecord; 163 | 164 | WatchRecord<_Handler> lhsWR = _cache[lhs.expression]; 165 | if (lhsWR == null) { 166 | lhsWR = _cache[lhs.expression] = lhs.setupWatch(this); 167 | } 168 | 169 | // We set a field forwarding handler on LHS. This will allow the change 170 | // objects to propagate to the current WatchRecord. 171 | lhsWR.handler.addForwardHandler(fieldHandler); 172 | 173 | // propagate the value from the LHS to here 174 | fieldHandler.acceptValue(lhsWR.currentValue); 175 | return watchRecord; 176 | } 177 | 178 | WatchRecord<_Handler> addCollectionWatch(AST ast) { 179 | var collectionHandler = new _CollectionHandler(this, ast.expression); 180 | var watchRecord = _changeDetector.watch(null, null, collectionHandler); 181 | _collectionCost++; 182 | collectionHandler.watchRecord = watchRecord; 183 | WatchRecord<_Handler> astWR = _cache[ast.expression]; 184 | if (astWR == null) { 185 | astWR = _cache[ast.expression] = ast.setupWatch(this); 186 | } 187 | 188 | // We set a field forwarding handler on LHS. This will allow the change 189 | // objects to propagate to the current WatchRecord. 190 | astWR.handler.addForwardHandler(collectionHandler); 191 | 192 | // propagate the value from the LHS to here 193 | collectionHandler.acceptValue(astWR.currentValue); 194 | return watchRecord; 195 | } 196 | 197 | /** 198 | * Watch a [fn] function represented by an [expression]. 199 | * 200 | * - [fn] function to evaluate. 201 | * - [argsAST] list of [AST]es which represent arguments passed to function. 202 | * - [expression] normalized expression used for caching. 203 | * - [isPure] A pure function is one which holds no internal state. This implies that the 204 | * function is idempotent. 205 | */ 206 | _EvalWatchRecord addFunctionWatch(Function fn, List argsAST, 207 | Map namedArgsAST, 208 | String expression, bool isPure) => 209 | _addEvalWatch(null, fn, null, argsAST, namedArgsAST, expression, isPure); 210 | 211 | /** 212 | * Watch a method [name]ed represented by an [expression]. 213 | * 214 | * - [lhs] left-hand-side of the method. 215 | * - [name] name of the method. 216 | * - [argsAST] list of [AST]es which represent arguments passed to method. 217 | * - [expression] normalized expression used for caching. 218 | */ 219 | _EvalWatchRecord addMethodWatch(AST lhs, String name, List argsAST, 220 | Map namedArgsAST, 221 | String expression) => 222 | _addEvalWatch(lhs, null, name, argsAST, namedArgsAST, expression, false); 223 | 224 | 225 | 226 | _EvalWatchRecord _addEvalWatch(AST lhsAST, Function fn, String name, 227 | List argsAST, 228 | Map namedArgsAST, 229 | String expression, bool isPure) { 230 | _InvokeHandler invokeHandler = new _InvokeHandler(this, expression); 231 | var evalWatchRecord = new _EvalWatchRecord( 232 | _rootGroup._fieldGetterFactory, this, invokeHandler, fn, name, 233 | argsAST.length, isPure); 234 | invokeHandler.watchRecord = evalWatchRecord; 235 | 236 | if (lhsAST != null) { 237 | var lhsWR = _cache[lhsAST.expression]; 238 | if (lhsWR == null) { 239 | lhsWR = _cache[lhsAST.expression] = lhsAST.setupWatch(this); 240 | } 241 | lhsWR.handler.addForwardHandler(invokeHandler); 242 | invokeHandler.acceptValue(lhsWR.currentValue); 243 | } 244 | 245 | // Convert the args from AST to WatchRecords 246 | for (var i = 0; i < argsAST.length; i++) { 247 | var ast = argsAST[i]; 248 | WatchRecord<_Handler> record = _cache[ast.expression]; 249 | if (record == null) { 250 | record = _cache[ast.expression] = ast.setupWatch(this); 251 | } 252 | _ArgHandler handler = new _PositionalArgHandler(this, evalWatchRecord, i); 253 | _ArgHandlerList._add(invokeHandler, handler); 254 | record.handler.addForwardHandler(handler); 255 | handler.acceptValue(record.currentValue); 256 | } 257 | 258 | namedArgsAST.forEach((Symbol name, AST ast) { 259 | WatchRecord<_Handler> record = _cache[ast.expression]; 260 | if (record == null) { 261 | record = _cache[ast.expression] = ast.setupWatch(this); 262 | } 263 | _ArgHandler handler = new _NamedArgHandler(this, evalWatchRecord, name); 264 | _ArgHandlerList._add(invokeHandler, handler); 265 | record.handler.addForwardHandler(handler); 266 | handler.acceptValue(record.currentValue); 267 | }); 268 | 269 | // Must be done last 270 | _EvalWatchList._add(this, evalWatchRecord); 271 | _evalCost++; 272 | if (_rootGroup.isInsideInvokeDirty) { 273 | // This check means that we are inside invoke reaction function. 274 | // Registering a new EvalWatch at this point will not run the 275 | // .check() on it which means it will not be processed, but its 276 | // reaction function will be run with null. So we process it manually. 277 | evalWatchRecord.check(); 278 | } 279 | return evalWatchRecord; 280 | } 281 | 282 | WatchGroup get _childWatchGroupTail { 283 | var tail = this, nextTail; 284 | while ((nextTail = tail._watchGroupTail) != null) { 285 | tail = nextTail; 286 | } 287 | return tail; 288 | } 289 | 290 | /** 291 | * Create a new child [WatchGroup]. 292 | * 293 | * - [context] if present the the child [WatchGroup] expressions will evaluate 294 | * against the new [context]. If not present than child expressions will 295 | * evaluate on same context allowing the reuse of the expression cache. 296 | */ 297 | WatchGroup newGroup([Object context]) { 298 | _EvalWatchRecord prev = _childWatchGroupTail._evalWatchTail; 299 | _EvalWatchRecord next = prev._nextEvalWatch; 300 | var childGroup = new WatchGroup._child( 301 | this, 302 | _changeDetector.newGroup(), 303 | context == null ? this.context : context, 304 | new HashMap>(), 305 | _rootGroup == null ? this : _rootGroup); 306 | _WatchGroupList._add(this, childGroup); 307 | var marker = childGroup._marker; 308 | 309 | marker._prevEvalWatch = prev; 310 | marker._nextEvalWatch = next; 311 | prev._nextEvalWatch = marker; 312 | if (next != null) next._prevEvalWatch = marker; 313 | 314 | return childGroup; 315 | } 316 | 317 | /** 318 | * Remove/destroy [WatchGroup] and all of its [Watches]. 319 | */ 320 | void remove() { 321 | // TODO:(misko) This code is not right. 322 | // 1) It fails to release [ChangeDetector] [WatchRecord]s. 323 | 324 | _WatchGroupList._remove(_parentWatchGroup, this); 325 | _nextWatchGroup = _prevWatchGroup = null; 326 | _changeDetector.remove(); 327 | _rootGroup._removeCount++; 328 | _parentWatchGroup = null; 329 | 330 | // Unlink the _watchRecord 331 | _EvalWatchRecord firstEvalWatch = _evalWatchHead; 332 | _EvalWatchRecord lastEvalWatch = _childWatchGroupTail._evalWatchTail; 333 | _EvalWatchRecord previous = firstEvalWatch._prevEvalWatch; 334 | _EvalWatchRecord next = lastEvalWatch._nextEvalWatch; 335 | if (previous != null) previous._nextEvalWatch = next; 336 | if (next != null) next._prevEvalWatch = previous; 337 | _evalWatchHead._prevEvalWatch = null; 338 | _evalWatchTail._nextEvalWatch = null; 339 | _evalWatchHead = _evalWatchTail = null; 340 | } 341 | 342 | toString() { 343 | var lines = []; 344 | if (this == _rootGroup) { 345 | var allWatches = []; 346 | var watch = _evalWatchHead; 347 | var prev = null; 348 | while (watch != null) { 349 | allWatches.add(watch.toString()); 350 | assert(watch._prevEvalWatch == prev); 351 | prev = watch; 352 | watch = watch._nextEvalWatch; 353 | } 354 | lines.add('WATCHES: ${allWatches.join(', ')}'); 355 | } 356 | 357 | var watches = []; 358 | var watch = _evalWatchHead; 359 | while (watch != _evalWatchTail) { 360 | watches.add(watch.toString()); 361 | watch = watch._nextEvalWatch; 362 | } 363 | watches.add(watch.toString()); 364 | 365 | lines.add('WatchGroup[$id](watches: ${watches.join(', ')})'); 366 | var childGroup = _watchGroupHead; 367 | while (childGroup != null) { 368 | lines.add(' ' + childGroup.toString().replaceAll('\n', '\n ')); 369 | childGroup = childGroup._nextWatchGroup; 370 | } 371 | return lines.join('\n'); 372 | } 373 | } 374 | 375 | /** 376 | * [RootWatchGroup] 377 | */ 378 | class RootWatchGroup extends WatchGroup { 379 | final FieldGetterFactory _fieldGetterFactory; 380 | Watch _dirtyWatchHead, _dirtyWatchTail; 381 | 382 | /** 383 | * Every time a [WatchGroup] is destroyed we increment the counter. During 384 | * [detectChanges] we reset the count. Before calling the reaction function, 385 | * we check [_removeCount] and if it is unchanged we can safely call the 386 | * reaction function. If it is changed we only call the reaction function 387 | * if the [WatchGroup] is still attached. 388 | */ 389 | int _removeCount = 0; 390 | 391 | 392 | RootWatchGroup(this._fieldGetterFactory, 393 | ChangeDetector changeDetector, 394 | Object context) 395 | : super._root(changeDetector, context); 396 | 397 | RootWatchGroup get _rootGroup => this; 398 | 399 | /** 400 | * Detect changes and process the [ReactionFn]s. 401 | * 402 | * Algorithm: 403 | * 1) process the [ChangeDetector#collectChanges]. 404 | * 2) process function/closure/method changes 405 | * 3) call an [ReactionFn]s 406 | * 407 | * Each step is called in sequence. ([ReactionFn]s are not called until all 408 | * previous steps are completed). 409 | */ 410 | int detectChanges({ EvalExceptionHandler exceptionHandler, 411 | ChangeLog changeLog, 412 | AvgStopwatch fieldStopwatch, 413 | AvgStopwatch evalStopwatch, 414 | AvgStopwatch processStopwatch}) { 415 | // Process the Records from the change detector 416 | Iterator> changedRecordIterator = 417 | (_changeDetector as ChangeDetector<_Handler>).collectChanges( 418 | exceptionHandler, 419 | fieldStopwatch); 420 | if (processStopwatch != null) processStopwatch.start(); 421 | while (changedRecordIterator.moveNext()) { 422 | var record = changedRecordIterator.current; 423 | if (changeLog != null) changeLog(record.handler.expression, 424 | record.currentValue, 425 | record.previousValue); 426 | record.handler.onChange(record); 427 | } 428 | if (processStopwatch != null) processStopwatch.stop(); 429 | 430 | if (evalStopwatch != null) evalStopwatch.start(); 431 | // Process our own function evaluations 432 | _EvalWatchRecord evalRecord = _evalWatchHead; 433 | int evalCount = 0; 434 | while (evalRecord != null) { 435 | try { 436 | if (evalStopwatch != null) evalCount++; 437 | if (evalRecord.check() && changeLog != null) { 438 | changeLog(evalRecord.handler.expression, 439 | evalRecord.currentValue, 440 | evalRecord.previousValue); 441 | } 442 | } catch (e, s) { 443 | if (exceptionHandler == null) rethrow; else exceptionHandler(e, s); 444 | } 445 | evalRecord = evalRecord._nextEvalWatch; 446 | } 447 | if (evalStopwatch != null) { 448 | evalStopwatch.stop(); 449 | evalStopwatch.increment(evalCount); 450 | } 451 | 452 | // Because the handler can forward changes between each other synchronously 453 | // We need to call reaction functions asynchronously. This processes the 454 | // asynchronous reaction function queue. 455 | int count = 0; 456 | if (processStopwatch != null) processStopwatch.start(); 457 | Watch dirtyWatch = _dirtyWatchHead; 458 | _dirtyWatchHead = null; 459 | RootWatchGroup root = _rootGroup; 460 | try { 461 | while (dirtyWatch != null) { 462 | count++; 463 | try { 464 | if (root._removeCount == 0 || dirtyWatch._watchGroup.isAttached) { 465 | dirtyWatch.invoke(); 466 | } 467 | } catch (e, s) { 468 | if (exceptionHandler == null) rethrow; else exceptionHandler(e, s); 469 | } 470 | var nextDirtyWatch = dirtyWatch._nextDirtyWatch; 471 | dirtyWatch._nextDirtyWatch = null; 472 | dirtyWatch = nextDirtyWatch; 473 | } 474 | } finally { 475 | _dirtyWatchTail = null; 476 | root._removeCount = 0; 477 | } 478 | if (processStopwatch != null) { 479 | processStopwatch.stop(); 480 | processStopwatch.increment(count); 481 | } 482 | return count; 483 | } 484 | 485 | bool get isInsideInvokeDirty => 486 | _dirtyWatchHead == null && _dirtyWatchTail != null; 487 | 488 | /** 489 | * Add Watch into the asynchronous queue for later processing. 490 | */ 491 | Watch _addDirtyWatch(Watch watch) { 492 | if (!watch._dirty) { 493 | watch._dirty = true; 494 | if (_dirtyWatchTail == null) { 495 | _dirtyWatchHead = _dirtyWatchTail = watch; 496 | } else { 497 | _dirtyWatchTail._nextDirtyWatch = watch; 498 | _dirtyWatchTail = watch; 499 | } 500 | watch._nextDirtyWatch = null; 501 | } 502 | return watch; 503 | } 504 | } 505 | 506 | /** 507 | * [Watch] corresponds to an individual [watch] registration on the watchGrp. 508 | */ 509 | class Watch { 510 | Watch _previousWatch, _nextWatch; 511 | 512 | final Record<_Handler> _record; 513 | final ReactionFn reactionFn; 514 | final WatchGroup _watchGroup; 515 | 516 | bool _dirty = false; 517 | bool _deleted = false; 518 | Watch _nextDirtyWatch; 519 | 520 | Watch(this._watchGroup, this._record, this.reactionFn); 521 | 522 | get expression => _record.handler.expression; 523 | void invoke() { 524 | if (_deleted || !_dirty) return; 525 | _dirty = false; 526 | reactionFn(_record.currentValue, _record.previousValue); 527 | } 528 | 529 | void remove() { 530 | if (_deleted) throw new StateError('Already deleted!'); 531 | _deleted = true; 532 | var handler = _record.handler; 533 | _WatchList._remove(handler, this); 534 | handler.release(); 535 | } 536 | } 537 | 538 | /** 539 | * This class processes changes from the change detector. The changes are 540 | * forwarded onto the next [_Handler] or queued up in case of reaction function. 541 | * 542 | * Given these two expression: 'a.b.c' => rfn1 and 'a.b' => rfn2 543 | * The resulting data structure is: 544 | * 545 | * _Handler +--> _Handler +--> _Handler 546 | * - delegateHandler -+ - delegateHandler -+ - delegateHandler = null 547 | * - expression: 'a' - expression: 'a.b' - expression: 'a.b.c' 548 | * - watchObject: context - watchObject: context.a - watchObject: context.a.b 549 | * - watchRecord: 'a' - watchRecord 'b' - watchRecord 'c' 550 | * - reactionFn: null - reactionFn: rfn1 - reactionFn: rfn2 551 | * 552 | * Notice how the [_Handler]s coalesce their watching. Also notice that any 553 | * changes detected at one handler are propagated to the next handler. 554 | */ 555 | abstract class _Handler implements _LinkedList, _LinkedListItem, _WatchList { 556 | // Used for forwarding changes to delegates 557 | _Handler _head, _tail; 558 | _Handler _next, _previous; 559 | Watch _watchHead, _watchTail; 560 | 561 | final String expression; 562 | final WatchGroup watchGrp; 563 | 564 | WatchRecord<_Handler> watchRecord; 565 | _Handler forwardingHandler; 566 | 567 | _Handler(this.watchGrp, this.expression) { 568 | assert(watchGrp != null); 569 | assert(expression != null); 570 | } 571 | 572 | Watch addReactionFn(ReactionFn reactionFn) { 573 | assert(_next != this); // verify we are not detached 574 | return watchGrp._rootGroup._addDirtyWatch(_WatchList._add(this, 575 | new Watch(watchGrp, watchRecord, reactionFn))); 576 | } 577 | 578 | void addForwardHandler(_Handler forwardToHandler) { 579 | assert(forwardToHandler.forwardingHandler == null); 580 | _LinkedList._add(this, forwardToHandler); 581 | forwardToHandler.forwardingHandler = this; 582 | } 583 | 584 | /// Return true if release has happened 585 | bool release() { 586 | if (_WatchList._isEmpty(this) && _LinkedList._isEmpty(this)) { 587 | _releaseWatch(); 588 | // Remove ourselves from cache, or else new registrations will go to us, 589 | // but we are dead 590 | watchGrp._cache.remove(expression); 591 | 592 | if (forwardingHandler != null) { 593 | // TODO(misko): why do we need this check? 594 | _LinkedList._remove(forwardingHandler, this); 595 | forwardingHandler.release(); 596 | } 597 | 598 | // We can remove ourselves 599 | assert((_next = _previous = this) == this); // mark ourselves as detached 600 | return true; 601 | } else { 602 | return false; 603 | } 604 | } 605 | 606 | void _releaseWatch() { 607 | watchRecord.remove(); 608 | watchGrp._fieldCost--; 609 | } 610 | acceptValue(object) => null; 611 | 612 | void onChange(Record<_Handler> record) { 613 | assert(_next != this); // verify we are not detached 614 | // If we have reaction functions than queue them up for asynchronous 615 | // processing. 616 | Watch watch = _watchHead; 617 | while (watch != null) { 618 | watchGrp._rootGroup._addDirtyWatch(watch); 619 | watch = watch._nextWatch; 620 | } 621 | // If we have a delegateHandler then forward the new value to it. 622 | _Handler delegateHandler = _head; 623 | while (delegateHandler != null) { 624 | delegateHandler.acceptValue(record.currentValue); 625 | delegateHandler = delegateHandler._next; 626 | } 627 | } 628 | } 629 | 630 | class _ConstantHandler extends _Handler { 631 | _ConstantHandler(WatchGroup watchGroup, String expression, constantValue) 632 | : super(watchGroup, expression) 633 | { 634 | watchRecord = new _EvalWatchRecord.constant(this, constantValue); 635 | } 636 | release() => null; 637 | } 638 | 639 | class _FieldHandler extends _Handler { 640 | _FieldHandler(watchGrp, expression): super(watchGrp, expression); 641 | 642 | /** 643 | * This function forwards the watched object to the next [_Handler] 644 | * synchronously. 645 | */ 646 | void acceptValue(object) { 647 | watchRecord.object = object; 648 | if (watchRecord.check()) onChange(watchRecord); 649 | } 650 | } 651 | 652 | class _CollectionHandler extends _Handler { 653 | _CollectionHandler(WatchGroup watchGrp, String expression) 654 | : super(watchGrp, expression); 655 | /** 656 | * This function forwards the watched object to the next [_Handler] synchronously. 657 | */ 658 | void acceptValue(object) { 659 | watchRecord.object = object; 660 | if (watchRecord.check()) onChange(watchRecord); 661 | } 662 | 663 | void _releaseWatch() { 664 | watchRecord.remove(); 665 | watchGrp._collectionCost--; 666 | } 667 | } 668 | 669 | abstract class _ArgHandler extends _Handler { 670 | _ArgHandler _previousArgHandler, _nextArgHandler; 671 | 672 | // TODO(misko): Why do we override parent? 673 | final _EvalWatchRecord watchRecord; 674 | _ArgHandler(WatchGroup watchGrp, String expression, this.watchRecord) 675 | : super(watchGrp, expression); 676 | 677 | _releaseWatch() => null; 678 | } 679 | 680 | class _PositionalArgHandler extends _ArgHandler { 681 | static final List _ARGS = new List.generate(20, (index) => 'arg[$index]'); 682 | final int index; 683 | _PositionalArgHandler(WatchGroup watchGrp, _EvalWatchRecord record, int index) 684 | : this.index = index, 685 | super(watchGrp, _ARGS[index], record); 686 | 687 | void acceptValue(object) { 688 | watchRecord.dirtyArgs = true; 689 | watchRecord.args[index] = object; 690 | } 691 | } 692 | 693 | class _NamedArgHandler extends _ArgHandler { 694 | static final Map _NAMED_ARG = new HashMap(); 695 | static String _GET_NAMED_ARG(Symbol symbol) { 696 | String n = _NAMED_ARG[symbol]; 697 | if (n == null) n = _NAMED_ARG[symbol] = 'namedArg[$n]'; 698 | return n; 699 | } 700 | final Symbol name; 701 | 702 | _NamedArgHandler(WatchGroup watchGrp, _EvalWatchRecord record, Symbol name) 703 | : this.name = name, 704 | super(watchGrp, _GET_NAMED_ARG(name), record); 705 | 706 | void acceptValue(object) { 707 | if (watchRecord.namedArgs == null) { 708 | watchRecord.namedArgs = new HashMap(); 709 | } 710 | watchRecord.dirtyArgs = true; 711 | watchRecord.namedArgs[name] = object; 712 | } 713 | } 714 | 715 | class _InvokeHandler extends _Handler implements _ArgHandlerList { 716 | _ArgHandler _argHandlerHead, _argHandlerTail; 717 | 718 | _InvokeHandler(WatchGroup watchGrp, String expression) 719 | : super(watchGrp, expression); 720 | 721 | void acceptValue(object) { 722 | watchRecord.object = object; 723 | } 724 | 725 | void _releaseWatch() { 726 | (watchRecord as _EvalWatchRecord).remove(); 727 | } 728 | 729 | bool release() { 730 | if (super.release()) { 731 | _ArgHandler current = _argHandlerHead; 732 | while (current != null) { 733 | current.release(); 734 | current = current._nextArgHandler; 735 | } 736 | return true; 737 | } else { 738 | return false; 739 | } 740 | } 741 | } 742 | 743 | 744 | class _EvalWatchRecord implements WatchRecord<_Handler> { 745 | static const int _MODE_INVALID_ = -2; 746 | static const int _MODE_DELETED_ = -1; 747 | static const int _MODE_MARKER_ = 0; 748 | static const int _MODE_PURE_FUNCTION_ = 1; 749 | static const int _MODE_FUNCTION_ = 2; 750 | static const int _MODE_PURE_FUNCTION_APPLY_ = 3; 751 | static const int _MODE_NULL_ = 4; 752 | static const int _MODE_FIELD_OR_METHOD_CLOSURE_ = 5; 753 | static const int _MODE_METHOD_ = 6; 754 | static const int _MODE_FIELD_CLOSURE_ = 7; 755 | static const int _MODE_MAP_CLOSURE_ = 8; 756 | WatchGroup watchGrp; 757 | final _Handler handler; 758 | final List args; 759 | Map namedArgs = null; 760 | final String name; 761 | int mode; 762 | Function fn; 763 | FieldGetterFactory _fieldGetterFactory; 764 | bool dirtyArgs = true; 765 | 766 | dynamic currentValue, previousValue, _object; 767 | _EvalWatchRecord _prevEvalWatch, _nextEvalWatch; 768 | 769 | _EvalWatchRecord(this._fieldGetterFactory, this.watchGrp, this.handler, 770 | this.fn, this.name, int arity, bool pure) 771 | : args = new List(arity) 772 | { 773 | if (fn is FunctionApply) { 774 | mode = pure ? _MODE_PURE_FUNCTION_APPLY_: _MODE_INVALID_; 775 | } else if (fn is Function) { 776 | mode = pure ? _MODE_PURE_FUNCTION_ : _MODE_FUNCTION_; 777 | } else { 778 | mode = _MODE_NULL_; 779 | } 780 | } 781 | 782 | _EvalWatchRecord.marker() 783 | : mode = _MODE_MARKER_, 784 | _fieldGetterFactory = null, 785 | watchGrp = null, 786 | handler = null, 787 | args = null, 788 | fn = null, 789 | name = null; 790 | 791 | _EvalWatchRecord.constant(_Handler handler, dynamic constantValue) 792 | : mode = _MODE_MARKER_, 793 | _fieldGetterFactory = null, 794 | handler = handler, 795 | currentValue = constantValue, 796 | watchGrp = null, 797 | args = null, 798 | fn = null, 799 | name = null; 800 | 801 | get field => '()'; 802 | 803 | get object => _object; 804 | 805 | set object(value) { 806 | assert(mode != _MODE_DELETED_); 807 | assert(mode != _MODE_MARKER_); 808 | assert(mode != _MODE_FUNCTION_); 809 | assert(mode != _MODE_PURE_FUNCTION_); 810 | assert(mode != _MODE_PURE_FUNCTION_APPLY_); 811 | _object = value; 812 | 813 | if (value == null) { 814 | mode = _MODE_NULL_; 815 | } else { 816 | if (value is Map) { 817 | mode = _MODE_MAP_CLOSURE_; 818 | } else { 819 | mode = _MODE_FIELD_OR_METHOD_CLOSURE_; 820 | fn = _fieldGetterFactory.getter(value, name); 821 | } 822 | } 823 | } 824 | 825 | bool check() { 826 | var value; 827 | switch (mode) { 828 | case _MODE_MARKER_: 829 | case _MODE_NULL_: 830 | return false; 831 | case _MODE_PURE_FUNCTION_: 832 | if (!dirtyArgs) return false; 833 | value = Function.apply(fn, args, namedArgs); 834 | dirtyArgs = false; 835 | break; 836 | case _MODE_FUNCTION_: 837 | value = Function.apply(fn, args, namedArgs); 838 | dirtyArgs = false; 839 | break; 840 | case _MODE_PURE_FUNCTION_APPLY_: 841 | if (!dirtyArgs) return false; 842 | value = (fn as FunctionApply).apply(args); 843 | dirtyArgs = false; 844 | break; 845 | case _MODE_FIELD_OR_METHOD_CLOSURE_: 846 | var closure = fn(_object); 847 | // NOTE: When Dart looks up a method "foo" on object "x", it returns a 848 | // new closure for each lookup. They compare equal via "==" but are no 849 | // identical(). There's no point getting a new value each time and 850 | // decide it's the same so we'll skip further checking after the first 851 | // time. 852 | if (closure is Function && !identical(closure, fn(_object))) { 853 | fn = closure; 854 | mode = _MODE_METHOD_; 855 | } else { 856 | mode = _MODE_FIELD_CLOSURE_; 857 | } 858 | value = (closure == null) ? null : Function.apply(closure, args, namedArgs); 859 | break; 860 | case _MODE_METHOD_: 861 | value = Function.apply(fn, args, namedArgs); 862 | break; 863 | case _MODE_FIELD_CLOSURE_: 864 | var closure = fn(_object); 865 | value = (closure == null) ? null : Function.apply(closure, args, namedArgs); 866 | break; 867 | case _MODE_MAP_CLOSURE_: 868 | var closure = object[name]; 869 | value = (closure == null) ? null : Function.apply(closure, args, namedArgs); 870 | break; 871 | default: 872 | assert(false); 873 | } 874 | 875 | var current = currentValue; 876 | if (!identical(current, value)) { 877 | if (value is String && current is String && value == current) { 878 | // it is really the same, recover and save so next time identity is same 879 | current = value; 880 | } else { 881 | previousValue = current; 882 | currentValue = value; 883 | handler.onChange(this); 884 | return true; 885 | } 886 | } 887 | return false; 888 | } 889 | 890 | get nextChange => null; 891 | 892 | void remove() { 893 | assert(mode != _MODE_DELETED_); 894 | assert((mode = _MODE_DELETED_) == _MODE_DELETED_); // Mark as deleted. 895 | watchGrp._evalCost--; 896 | _EvalWatchList._remove(watchGrp, this); 897 | } 898 | 899 | String toString() { 900 | if (mode == _MODE_MARKER_) return 'MARKER[$currentValue]'; 901 | return '${watchGrp.id}:${handler.expression}'; 902 | } 903 | } 904 | -------------------------------------------------------------------------------- /change_detection/test/watch_group_spec.dart: -------------------------------------------------------------------------------- 1 | library watch_group_spec; 2 | 3 | import '../_specs.dart'; 4 | import 'dart:collection'; 5 | import 'package:angular/change_detection/ast_parser.dart'; 6 | import 'package:angular/change_detection/watch_group.dart'; 7 | import 'package:angular/change_detection/dirty_checking_change_detector.dart'; 8 | import 'package:angular/change_detection/dirty_checking_change_detector_dynamic.dart'; 9 | import 'dirty_checking_change_detector_spec.dart' hide main; 10 | import 'package:angular/core/parser/parser_dynamic.dart' show DynamicClosureMap; 11 | 12 | class TestData { 13 | sub1(a, {b: 0}) => a - b; 14 | sub2({a: 0, b: 0}) => a - b; 15 | } 16 | 17 | void main() { 18 | describe('WatchGroup', () { 19 | var context; 20 | var watchGrp; 21 | DirtyCheckingChangeDetector changeDetector; 22 | Logger logger; 23 | Parser parser; 24 | ASTParser astParser; 25 | 26 | beforeEach((Logger _logger, Parser _parser, ASTParser _astParser) { 27 | context = {}; 28 | var getterFactory = new DynamicFieldGetterFactory(); 29 | changeDetector = new DirtyCheckingChangeDetector(getterFactory); 30 | watchGrp = new RootWatchGroup(getterFactory, changeDetector, context); 31 | logger = _logger; 32 | parser = _parser; 33 | astParser = _astParser; 34 | }); 35 | 36 | AST parse(String expression) => astParser(expression); 37 | 38 | eval(String expression, [evalContext]) { 39 | AST ast = parse(expression); 40 | 41 | if (evalContext == null) evalContext = context; 42 | WatchGroup group = watchGrp.newGroup(evalContext); 43 | 44 | List log = []; 45 | Watch watch = group.watch(ast, (v, p) => log.add(v)); 46 | 47 | watchGrp.detectChanges(); 48 | group.remove(); 49 | 50 | if (log.isEmpty) { 51 | throw new StateError('Expression <$expression> was not evaluated'); 52 | } else if (log.length > 1) { 53 | throw new StateError('Expression <$expression> produced too many values: $log'); 54 | } else { 55 | return log.first; 56 | } 57 | } 58 | 59 | expectOrder(list) { 60 | logger.clear(); 61 | watchGrp.detectChanges(); // Clear the initial queue 62 | logger.clear(); 63 | watchGrp.detectChanges(); 64 | expect(logger).toEqual(list); 65 | } 66 | 67 | beforeEach((Logger _logger) { 68 | context = {}; 69 | var getterFactory = new DynamicFieldGetterFactory(); 70 | changeDetector = new DirtyCheckingChangeDetector(getterFactory); 71 | watchGrp = new RootWatchGroup(getterFactory, changeDetector, context); 72 | logger = _logger; 73 | }); 74 | 75 | it('should have a toString for debugging', () { 76 | watchGrp.watch(parse('a'), (v, p) {}); 77 | watchGrp.newGroup({}); 78 | expect("$watchGrp").toEqual( 79 | 'WATCHES: MARKER[null], MARKER[null]\n' 80 | 'WatchGroup[](watches: MARKER[null])\n' 81 | ' WatchGroup[.0](watches: MARKER[null])' 82 | ); 83 | }); 84 | 85 | describe('watch lifecycle', () { 86 | it('should prevent reaction fn on removed', () { 87 | context['a'] = 'hello'; 88 | var watch ; 89 | watchGrp.watch(parse('a'), (v, p) { 90 | logger('removed'); 91 | watch.remove(); 92 | }); 93 | watch = watchGrp.watch(parse('a'), (v, p) => logger(v)); 94 | watchGrp.detectChanges(); 95 | expect(logger).toEqual(['removed']); 96 | }); 97 | }); 98 | 99 | describe('property chaining', () { 100 | it('should read property', () { 101 | context['a'] = 'hello'; 102 | 103 | // should fire on initial adding 104 | expect(watchGrp.fieldCost).toEqual(0); 105 | var watch = watchGrp.watch(parse('a'), (v, p) => logger(v)); 106 | expect(watch.expression).toEqual('a'); 107 | expect(watchGrp.fieldCost).toEqual(1); 108 | watchGrp.detectChanges(); 109 | expect(logger).toEqual(['hello']); 110 | 111 | // make sore no new changes are logged on extra detectChanges 112 | watchGrp.detectChanges(); 113 | expect(logger).toEqual(['hello']); 114 | 115 | // Should detect value change 116 | context['a'] = 'bye'; 117 | watchGrp.detectChanges(); 118 | expect(logger).toEqual(['hello', 'bye']); 119 | 120 | // should cleanup after itself 121 | watch.remove(); 122 | expect(watchGrp.fieldCost).toEqual(0); 123 | context['a'] = 'cant see me'; 124 | watchGrp.detectChanges(); 125 | expect(logger).toEqual(['hello', 'bye']); 126 | }); 127 | 128 | describe('sequence mutations and ref changes', () { 129 | it('should handle a simultaneous map mutation and reference change', () { 130 | context['a'] = context['b'] = {1: 10, 2: 20}; 131 | var watchA = watchGrp.watch(new CollectionAST(parse('a')), (v, p) => logger(v)); 132 | var watchB = watchGrp.watch(new CollectionAST(parse('b')), (v, p) => logger(v)); 133 | 134 | watchGrp.detectChanges(); 135 | expect(logger.length).toEqual(2); 136 | expect(logger[0], toEqualMapRecord( 137 | map: ['1', '2'], 138 | previous: ['1', '2'])); 139 | expect(logger[1], toEqualMapRecord( 140 | map: ['1', '2'], 141 | previous: ['1', '2'])); 142 | logger.clear(); 143 | 144 | // context['a'] is set to a copy with an addition. 145 | context['a'] = new Map.from(context['a'])..[3] = 30; 146 | // context['b'] still has the original collection. We'll mutate it. 147 | context['b'].remove(1); 148 | 149 | watchGrp.detectChanges(); 150 | expect(logger.length).toEqual(2); 151 | expect(logger[0], toEqualMapRecord( 152 | map: ['1', '2', '3[null -> 30]'], 153 | previous: ['1', '2'], 154 | additions: ['3[null -> 30]'])); 155 | expect(logger[1], toEqualMapRecord( 156 | map: ['2'], 157 | previous: ['1[10 -> null]', '2'], 158 | removals: ['1[10 -> null]'])); 159 | logger.clear(); 160 | }); 161 | 162 | it('should handle a simultaneous list mutation and reference change', () { 163 | context['a'] = context['b'] = [0, 1]; 164 | var watchA = watchGrp.watch(new CollectionAST(parse('a')), (v, p) => logger(v)); 165 | var watchB = watchGrp.watch(new CollectionAST(parse('b')), (v, p) => logger(v)); 166 | 167 | watchGrp.detectChanges(); 168 | expect(logger.length).toEqual(2); 169 | expect(logger[0], toEqualCollectionRecord( 170 | collection: ['0', '1'], 171 | previous: ['0', '1'], 172 | additions: [], moves: [], removals: [])); 173 | expect(logger[1], toEqualCollectionRecord( 174 | collection: ['0', '1'], 175 | previous: ['0', '1'], 176 | additions: [], moves: [], removals: [])); 177 | logger.clear(); 178 | 179 | // context['a'] is set to a copy with an addition. 180 | context['a'] = context['a'].toList()..add(2); 181 | // context['b'] still has the original collection. We'll mutate it. 182 | context['b'].remove(0); 183 | 184 | watchGrp.detectChanges(); 185 | expect(logger.length).toEqual(2); 186 | expect(logger[0], toEqualCollectionRecord( 187 | collection: ['0', '1', '2[null -> 2]'], 188 | previous: ['0', '1'], 189 | additions: ['2[null -> 2]'], 190 | moves: [], 191 | removals: [])); 192 | expect(logger[1], toEqualCollectionRecord( 193 | collection: ['1[1 -> 0]'], 194 | previous: ['0[0 -> null]', '1[1 -> 0]'], 195 | additions: [], 196 | moves: ['1[1 -> 0]'], 197 | removals: ['0[0 -> null]'])); 198 | logger.clear(); 199 | }); 200 | 201 | it('should work correctly with UnmodifiableListView', () { 202 | context['a'] = new UnmodifiableListView([0, 1]); 203 | var watch = watchGrp.watch(new CollectionAST(parse('a')), (v, p) => logger(v)); 204 | 205 | watchGrp.detectChanges(); 206 | expect(logger.length).toEqual(1); 207 | expect(logger[0], toEqualCollectionRecord( 208 | collection: ['0', '1'], 209 | previous: ['0', '1'])); 210 | logger.clear(); 211 | 212 | context['a'] = new UnmodifiableListView([1, 0]); 213 | 214 | watchGrp.detectChanges(); 215 | expect(logger.length).toEqual(1); 216 | expect(logger[0], toEqualCollectionRecord( 217 | collection: ['1[1 -> 0]', '0[0 -> 1]'], 218 | previous: ['0[0 -> 1]', '1[1 -> 0]'], 219 | moves: ['1[1 -> 0]', '0[0 -> 1]'])); 220 | logger.clear(); 221 | }); 222 | 223 | }); 224 | 225 | it('should read property chain', () { 226 | context['a'] = {'b': 'hello'}; 227 | 228 | // should fire on initial adding 229 | expect(watchGrp.fieldCost).toEqual(0); 230 | expect(changeDetector.count).toEqual(0); 231 | var watch = watchGrp.watch(parse('a.b'), (v, p) => logger(v)); 232 | expect(watch.expression).toEqual('a.b'); 233 | expect(watchGrp.fieldCost).toEqual(2); 234 | expect(changeDetector.count).toEqual(2); 235 | watchGrp.detectChanges(); 236 | expect(logger).toEqual(['hello']); 237 | 238 | // make sore no new changes are logged on extra detectChanges 239 | watchGrp.detectChanges(); 240 | expect(logger).toEqual(['hello']); 241 | 242 | // make sure no changes or logged when intermediary object changes 243 | context['a'] = {'b': 'hello'}; 244 | watchGrp.detectChanges(); 245 | expect(logger).toEqual(['hello']); 246 | 247 | // Should detect value change 248 | context['a'] = {'b': 'hello2'}; 249 | watchGrp.detectChanges(); 250 | expect(logger).toEqual(['hello', 'hello2']); 251 | 252 | // Should detect value change 253 | context['a']['b'] = 'bye'; 254 | watchGrp.detectChanges(); 255 | expect(logger).toEqual(['hello', 'hello2', 'bye']); 256 | 257 | // should cleanup after itself 258 | watch.remove(); 259 | expect(watchGrp.fieldCost).toEqual(0); 260 | context['a']['b'] = 'cant see me'; 261 | watchGrp.detectChanges(); 262 | expect(logger).toEqual(['hello', 'hello2', 'bye']); 263 | }); 264 | 265 | it('should reuse handlers', () { 266 | var user1 = {'first': 'misko', 'last': 'hevery'}; 267 | var user2 = {'first': 'misko', 'last': 'Hevery'}; 268 | 269 | context['user'] = user1; 270 | 271 | // should fire on initial adding 272 | expect(watchGrp.fieldCost).toEqual(0); 273 | var watch = watchGrp.watch(parse('user'), (v, p) => logger(v)); 274 | var watchFirst = watchGrp.watch(parse('user.first'), (v, p) => logger(v)); 275 | var watchLast = watchGrp.watch(parse('user.last'), (v, p) => logger(v)); 276 | expect(watchGrp.fieldCost).toEqual(3); 277 | 278 | watchGrp.detectChanges(); 279 | expect(logger).toEqual([user1, 'misko', 'hevery']); 280 | logger.clear(); 281 | 282 | context['user'] = user2; 283 | watchGrp.detectChanges(); 284 | expect(logger).toEqual([user2, 'Hevery']); 285 | 286 | 287 | watch.remove(); 288 | expect(watchGrp.fieldCost).toEqual(3); 289 | 290 | watchFirst.remove(); 291 | expect(watchGrp.fieldCost).toEqual(2); 292 | 293 | watchLast.remove(); 294 | expect(watchGrp.fieldCost).toEqual(0); 295 | 296 | expect(() => watch.remove()).toThrow('Already deleted!'); 297 | }); 298 | 299 | it('should eval pure FunctionApply', () { 300 | context['a'] = {'val': 1}; 301 | 302 | FunctionApply fn = new LoggingFunctionApply(logger); 303 | var watch = watchGrp.watch( 304 | new PureFunctionAST('add', fn, [parse('a.val')]), 305 | (v, p) => logger(v) 306 | ); 307 | 308 | // a; a.val; b; b.val; 309 | expect(watchGrp.fieldCost).toEqual(2); 310 | // add 311 | expect(watchGrp.evalCost).toEqual(1); 312 | 313 | watchGrp.detectChanges(); 314 | expect(logger).toEqual([[1], null]); 315 | logger.clear(); 316 | 317 | context['a'] = {'val': 2}; 318 | watchGrp.detectChanges(); 319 | expect(logger).toEqual([[2]]); 320 | }); 321 | 322 | 323 | it('should eval pure function', () { 324 | context['a'] = {'val': 1}; 325 | context['b'] = {'val': 2}; 326 | 327 | var watch = watchGrp.watch( 328 | new PureFunctionAST('add', 329 | (a, b) { logger('+'); return a+b; }, 330 | [parse('a.val'), parse('b.val')] 331 | ), 332 | (v, p) => logger(v) 333 | ); 334 | 335 | // a; a.val; b; b.val; 336 | expect(watchGrp.fieldCost).toEqual(4); 337 | // add 338 | expect(watchGrp.evalCost).toEqual(1); 339 | 340 | watchGrp.detectChanges(); 341 | expect(logger).toEqual(['+', 3]); 342 | 343 | // extra checks should not trigger functions 344 | watchGrp.detectChanges(); 345 | watchGrp.detectChanges(); 346 | expect(logger).toEqual(['+', 3]); 347 | 348 | // multiple arg changes should only trigger function once. 349 | context['a']['val'] = 3; 350 | context['b']['val'] = 4; 351 | 352 | watchGrp.detectChanges(); 353 | expect(logger).toEqual(['+', 3, '+', 7]); 354 | 355 | watch.remove(); 356 | expect(watchGrp.fieldCost).toEqual(0); 357 | expect(watchGrp.evalCost).toEqual(0); 358 | 359 | context['a']['val'] = 0; 360 | context['b']['val'] = 0; 361 | 362 | watchGrp.detectChanges(); 363 | expect(logger).toEqual(['+', 3, '+', 7]); 364 | }); 365 | 366 | 367 | it('should eval closure', () { 368 | context['a'] = {'val': 1}; 369 | context['b'] = {'val': 2}; 370 | var innerState = 1; 371 | 372 | var watch = watchGrp.watch( 373 | new ClosureAST('sum', 374 | (a, b) { logger('+'); return innerState+a+b; }, 375 | [parse('a.val'), parse('b.val')] 376 | ), 377 | (v, p) => logger(v) 378 | ); 379 | 380 | // a; a.val; b; b.val; 381 | expect(watchGrp.fieldCost).toEqual(4); 382 | // add 383 | expect(watchGrp.evalCost).toEqual(1); 384 | 385 | watchGrp.detectChanges(); 386 | expect(logger).toEqual(['+', 4]); 387 | 388 | // extra checks should trigger closures 389 | watchGrp.detectChanges(); 390 | watchGrp.detectChanges(); 391 | expect(logger).toEqual(['+', 4, '+', '+']); 392 | logger.clear(); 393 | 394 | // multiple arg changes should only trigger function once. 395 | context['a']['val'] = 3; 396 | context['b']['val'] = 4; 397 | 398 | watchGrp.detectChanges(); 399 | expect(logger).toEqual(['+', 8]); 400 | logger.clear(); 401 | 402 | // inner state change should only trigger function once. 403 | innerState = 2; 404 | 405 | watchGrp.detectChanges(); 406 | expect(logger).toEqual(['+', 9]); 407 | logger.clear(); 408 | 409 | watch.remove(); 410 | expect(watchGrp.fieldCost).toEqual(0); 411 | expect(watchGrp.evalCost).toEqual(0); 412 | 413 | context['a']['val'] = 0; 414 | context['b']['val'] = 0; 415 | 416 | watchGrp.detectChanges(); 417 | expect(logger).toEqual([]); 418 | }); 419 | 420 | 421 | it('should eval chained pure function', () { 422 | context['a'] = {'val': 1}; 423 | context['b'] = {'val': 2}; 424 | context['c'] = {'val': 3}; 425 | 426 | var a_plus_b = new PureFunctionAST('add1', 427 | (a, b) { logger('$a+$b'); return a + b; }, 428 | [parse('a.val'), parse('b.val')]); 429 | 430 | var a_plus_b_plus_c = new PureFunctionAST('add2', 431 | (b, c) { logger('$b+$c'); return b + c; }, 432 | [a_plus_b, parse('c.val')]); 433 | 434 | var watch = watchGrp.watch(a_plus_b_plus_c, (v, p) => logger(v)); 435 | 436 | // a; a.val; b; b.val; c; c.val; 437 | expect(watchGrp.fieldCost).toEqual(6); 438 | // add 439 | expect(watchGrp.evalCost).toEqual(2); 440 | 441 | watchGrp.detectChanges(); 442 | expect(logger).toEqual(['1+2', '3+3', 6]); 443 | logger.clear(); 444 | 445 | // extra checks should not trigger functions 446 | watchGrp.detectChanges(); 447 | watchGrp.detectChanges(); 448 | expect(logger).toEqual([]); 449 | logger.clear(); 450 | 451 | // multiple arg changes should only trigger function once. 452 | context['a']['val'] = 3; 453 | context['b']['val'] = 4; 454 | context['c']['val'] = 5; 455 | watchGrp.detectChanges(); 456 | expect(logger).toEqual(['3+4', '7+5', 12]); 457 | logger.clear(); 458 | 459 | context['a']['val'] = 9; 460 | watchGrp.detectChanges(); 461 | expect(logger).toEqual(['9+4', '13+5', 18]); 462 | logger.clear(); 463 | 464 | context['c']['val'] = 9; 465 | watchGrp.detectChanges(); 466 | expect(logger).toEqual(['13+9', 22]); 467 | logger.clear(); 468 | 469 | 470 | watch.remove(); 471 | expect(watchGrp.fieldCost).toEqual(0); 472 | expect(watchGrp.evalCost).toEqual(0); 473 | 474 | context['a']['val'] = 0; 475 | context['b']['val'] = 0; 476 | 477 | watchGrp.detectChanges(); 478 | expect(logger).toEqual([]); 479 | }); 480 | 481 | 482 | it('should eval closure', () { 483 | var obj; 484 | obj = { 485 | 'methodA': (arg1) { 486 | logger('methodA($arg1) => ${obj['valA']}'); 487 | return obj['valA']; 488 | }, 489 | 'valA': 'A' 490 | }; 491 | context['obj'] = obj; 492 | context['arg0'] = 1; 493 | 494 | var watch = watchGrp.watch( 495 | new MethodAST(parse('obj'), 'methodA', [parse('arg0')]), 496 | (v, p) => logger(v) 497 | ); 498 | 499 | // obj, arg0; 500 | expect(watchGrp.fieldCost).toEqual(2); 501 | // methodA() 502 | expect(watchGrp.evalCost).toEqual(1); 503 | 504 | watchGrp.detectChanges(); 505 | expect(logger).toEqual(['methodA(1) => A', 'A']); 506 | logger.clear(); 507 | 508 | watchGrp.detectChanges(); 509 | watchGrp.detectChanges(); 510 | expect(logger).toEqual(['methodA(1) => A', 'methodA(1) => A']); 511 | logger.clear(); 512 | 513 | obj['valA'] = 'B'; 514 | context['arg0'] = 2; 515 | 516 | watchGrp.detectChanges(); 517 | expect(logger).toEqual(['methodA(2) => B', 'B']); 518 | logger.clear(); 519 | 520 | watch.remove(); 521 | expect(watchGrp.fieldCost).toEqual(0); 522 | expect(watchGrp.evalCost).toEqual(0); 523 | 524 | obj['valA'] = 'C'; 525 | context['arg0'] = 3; 526 | 527 | watchGrp.detectChanges(); 528 | expect(logger).toEqual([]); 529 | }); 530 | 531 | it('should ignore NaN != NaN', () { 532 | watchGrp.watch(new ClosureAST('NaN', () => double.NAN, []), (_, __) => logger('NaN')); 533 | 534 | watchGrp.detectChanges(); 535 | expect(logger).toEqual(['NaN']); 536 | 537 | logger.clear(); 538 | watchGrp.detectChanges(); 539 | expect(logger).toEqual([]); 540 | }) ; 541 | 542 | it('should test string by value', () { 543 | watchGrp.watch(new ClosureAST('String', () => 'value', []), (v, _) => logger(v)); 544 | 545 | watchGrp.detectChanges(); 546 | expect(logger).toEqual(['value']); 547 | 548 | logger.clear(); 549 | watchGrp.detectChanges(); 550 | expect(logger).toEqual([]); 551 | }); 552 | 553 | it('should eval method', () { 554 | var obj = new MyClass(logger); 555 | obj.valA = 'A'; 556 | context['obj'] = obj; 557 | context['arg0'] = 1; 558 | 559 | var watch = watchGrp.watch( 560 | new MethodAST(parse('obj'), 'methodA', [parse('arg0')]), 561 | (v, p) => logger(v) 562 | ); 563 | 564 | // obj, arg0; 565 | expect(watchGrp.fieldCost).toEqual(2); 566 | // methodA() 567 | expect(watchGrp.evalCost).toEqual(1); 568 | 569 | watchGrp.detectChanges(); 570 | expect(logger).toEqual(['methodA(1) => A', 'A']); 571 | logger.clear(); 572 | 573 | watchGrp.detectChanges(); 574 | watchGrp.detectChanges(); 575 | expect(logger).toEqual(['methodA(1) => A', 'methodA(1) => A']); 576 | logger.clear(); 577 | 578 | obj.valA = 'B'; 579 | context['arg0'] = 2; 580 | 581 | watchGrp.detectChanges(); 582 | expect(logger).toEqual(['methodA(2) => B', 'B']); 583 | logger.clear(); 584 | 585 | watch.remove(); 586 | expect(watchGrp.fieldCost).toEqual(0); 587 | expect(watchGrp.evalCost).toEqual(0); 588 | 589 | obj.valA = 'C'; 590 | context['arg0'] = 3; 591 | 592 | watchGrp.detectChanges(); 593 | expect(logger).toEqual([]); 594 | }); 595 | 596 | it('should eval method chain', () { 597 | var obj1 = new MyClass(logger); 598 | var obj2 = new MyClass(logger); 599 | obj1.valA = obj2; 600 | obj2.valA = 'A'; 601 | context['obj'] = obj1; 602 | context['arg0'] = 0; 603 | context['arg1'] = 1; 604 | 605 | // obj.methodA(arg0) 606 | var ast = new MethodAST(parse('obj'), 'methodA', [parse('arg0')]); 607 | ast = new MethodAST(ast, 'methodA', [parse('arg1')]); 608 | var watch = watchGrp.watch(ast, (v, p) => logger(v)); 609 | 610 | // obj, arg0, arg1; 611 | expect(watchGrp.fieldCost).toEqual(3); 612 | // methodA(), methodA() 613 | expect(watchGrp.evalCost).toEqual(2); 614 | 615 | watchGrp.detectChanges(); 616 | expect(logger).toEqual(['methodA(0) => MyClass', 'methodA(1) => A', 'A']); 617 | logger.clear(); 618 | 619 | watchGrp.detectChanges(); 620 | watchGrp.detectChanges(); 621 | expect(logger).toEqual(['methodA(0) => MyClass', 'methodA(1) => A', 622 | 'methodA(0) => MyClass', 'methodA(1) => A']); 623 | logger.clear(); 624 | 625 | obj2.valA = 'B'; 626 | context['arg0'] = 10; 627 | context['arg1'] = 11; 628 | 629 | watchGrp.detectChanges(); 630 | expect(logger).toEqual(['methodA(10) => MyClass', 'methodA(11) => B', 'B']); 631 | logger.clear(); 632 | 633 | watch.remove(); 634 | expect(watchGrp.fieldCost).toEqual(0); 635 | expect(watchGrp.evalCost).toEqual(0); 636 | 637 | obj2.valA = 'C'; 638 | context['arg0'] = 20; 639 | context['arg1'] = 21; 640 | 641 | watchGrp.detectChanges(); 642 | expect(logger).toEqual([]); 643 | }); 644 | 645 | it('should not return null when evaling method first time', () { 646 | context['text'] ='abc'; 647 | var ast = new MethodAST(parse('text'), 'toUpperCase', []); 648 | var watch = watchGrp.watch(ast, (v, p) => logger(v)); 649 | 650 | watchGrp.detectChanges(); 651 | expect(logger).toEqual(['ABC']); 652 | }); 653 | 654 | it('should not eval a function if registered during reaction', () { 655 | context['text'] ='abc'; 656 | var ast = new MethodAST(parse('text'), 'toLowerCase', []); 657 | var watch = watchGrp.watch(ast, (v, p) { 658 | var ast = new MethodAST(parse('text'), 'toUpperCase', []); 659 | watchGrp.watch(ast, (v, p) { 660 | logger(v); 661 | }); 662 | }); 663 | 664 | watchGrp.detectChanges(); 665 | watchGrp.detectChanges(); 666 | expect(logger).toEqual(['ABC']); 667 | }); 668 | 669 | 670 | it('should eval function eagerly when registered during reaction', () { 671 | var fn = (arg) { logger('fn($arg)'); return arg; }; 672 | context['obj'] = {'fn': fn}; 673 | context['arg1'] = 'OUT'; 674 | context['arg2'] = 'IN'; 675 | var ast = new MethodAST(parse('obj'), 'fn', [parse('arg1')]); 676 | var watch = watchGrp.watch(ast, (v, p) { 677 | var ast = new MethodAST(parse('obj'), 'fn', [parse('arg2')]); 678 | watchGrp.watch(ast, (v, p) { 679 | logger('reaction: $v'); 680 | }); 681 | }); 682 | 683 | expect(logger).toEqual([]); 684 | watchGrp.detectChanges(); 685 | expect(logger).toEqual(['fn(OUT)', 'fn(IN)', 'reaction: IN']); 686 | logger.clear(); 687 | watchGrp.detectChanges(); 688 | expect(logger).toEqual(['fn(OUT)', 'fn(IN)']); 689 | }); 690 | 691 | 692 | it('should read constant', () { 693 | // should fire on initial adding 694 | expect(watchGrp.fieldCost).toEqual(0); 695 | var watch = watchGrp.watch(new ConstantAST(123), (v, p) => logger(v)); 696 | expect(watch.expression).toEqual('123'); 697 | expect(watchGrp.fieldCost).toEqual(0); 698 | watchGrp.detectChanges(); 699 | expect(logger).toEqual([123]); 700 | 701 | // make sore no new changes are logged on extra detectChanges 702 | watchGrp.detectChanges(); 703 | expect(logger).toEqual([123]); 704 | }); 705 | 706 | it('should wrap iterable in ObservableList', () { 707 | context['list'] = []; 708 | var watch = watchGrp.watch(new CollectionAST(parse('list')), (v, p) => logger(v)); 709 | 710 | expect(watchGrp.fieldCost).toEqual(1); 711 | expect(watchGrp.collectionCost).toEqual(1); 712 | expect(watchGrp.evalCost).toEqual(0); 713 | 714 | watchGrp.detectChanges(); 715 | expect(logger.length).toEqual(1); 716 | expect(logger[0], toEqualCollectionRecord( 717 | collection: [], 718 | additions: [], 719 | moves: [], 720 | removals: [])); 721 | logger.clear(); 722 | 723 | context['list'] = [1]; 724 | watchGrp.detectChanges(); 725 | expect(logger.length).toEqual(1); 726 | expect(logger[0], toEqualCollectionRecord( 727 | collection: ['1[null -> 0]'], 728 | additions: ['1[null -> 0]'], 729 | moves: [], 730 | removals: [])); 731 | logger.clear(); 732 | 733 | watch.remove(); 734 | expect(watchGrp.fieldCost).toEqual(0); 735 | expect(watchGrp.collectionCost).toEqual(0); 736 | expect(watchGrp.evalCost).toEqual(0); 737 | }); 738 | 739 | it('should watch literal arrays made of expressions', () { 740 | context['a'] = 1; 741 | var ast = new CollectionAST( 742 | new PureFunctionAST('[a]', new ArrayFn(), [parse('a')]) 743 | ); 744 | var watch = watchGrp.watch(ast, (v, p) => logger(v)); 745 | watchGrp.detectChanges(); 746 | expect(logger[0], toEqualCollectionRecord( 747 | collection: ['1[null -> 0]'], 748 | additions: ['1[null -> 0]'], 749 | moves: [], 750 | removals: [])); 751 | logger.clear(); 752 | 753 | context['a'] = 2; 754 | watchGrp.detectChanges(); 755 | expect(logger[0], toEqualCollectionRecord( 756 | collection: ['2[null -> 0]'], 757 | previous: ['1[0 -> null]'], 758 | additions: ['2[null -> 0]'], 759 | moves: [], 760 | removals: ['1[0 -> null]'])); 761 | logger.clear(); 762 | }); 763 | 764 | it('should watch pure function whose result goes to pure function', () { 765 | context['a'] = 1; 766 | var ast = new PureFunctionAST( 767 | '-', 768 | (v) => -v, 769 | [new PureFunctionAST('++', (v) => v + 1, [parse('a')])] 770 | ); 771 | var watch = watchGrp.watch(ast, (v, p) => logger(v)); 772 | 773 | expect(watchGrp.detectChanges()).not.toBe(null); 774 | expect(logger).toEqual([-2]); 775 | logger.clear(); 776 | 777 | context['a'] = 2; 778 | expect(watchGrp.detectChanges()).not.toBe(null); 779 | expect(logger).toEqual([-3]); 780 | }); 781 | }); 782 | 783 | describe('evaluation', () { 784 | it('should support simple literals', () { 785 | expect(eval('42')).toBe(42); 786 | expect(eval('87')).toBe(87); 787 | }); 788 | 789 | it('should support context access', () { 790 | context['x'] = 42; 791 | expect(eval('x')).toBe(42); 792 | context['y'] = 87; 793 | expect(eval('y')).toBe(87); 794 | }); 795 | 796 | it('should support custom context', () { 797 | expect(eval('x', {'x': 42})).toBe(42); 798 | expect(eval('x', {'x': 87})).toBe(87); 799 | }); 800 | 801 | it('should support named arguments for scope calls', () { 802 | var data = new TestData(); 803 | expect(eval("sub1(1)", data)).toEqual(1); 804 | expect(eval("sub1(3, b: 2)", data)).toEqual(1); 805 | 806 | expect(eval("sub2()", data)).toEqual(0); 807 | expect(eval("sub2(a: 3)", data)).toEqual(3); 808 | expect(eval("sub2(a: 3, b: 2)", data)).toEqual(1); 809 | expect(eval("sub2(b: 4)", data)).toEqual(-4); 810 | }); 811 | 812 | it('should support named arguments for scope calls (map)', () { 813 | context["sub1"] = (a, {b: 0}) => a - b; 814 | expect(eval("sub1(1)")).toEqual(1); 815 | expect(eval("sub1(3, b: 2)")).toEqual(1); 816 | 817 | context["sub2"] = ({a: 0, b: 0}) => a - b; 818 | expect(eval("sub2()")).toEqual(0); 819 | expect(eval("sub2(a: 3)")).toEqual(3); 820 | expect(eval("sub2(a: 3, b: 2)")).toEqual(1); 821 | expect(eval("sub2(b: 4)")).toEqual(-4); 822 | }); 823 | 824 | it('should support named arguments for member calls', () { 825 | context['o'] = new TestData(); 826 | expect(eval("o.sub1(1)")).toEqual(1); 827 | expect(eval("o.sub1(3, b: 2)")).toEqual(1); 828 | 829 | expect(eval("o.sub2()")).toEqual(0); 830 | expect(eval("o.sub2(a: 3)")).toEqual(3); 831 | expect(eval("o.sub2(a: 3, b: 2)")).toEqual(1); 832 | expect(eval("o.sub2(b: 4)")).toEqual(-4); 833 | }); 834 | 835 | it('should support named arguments for member calls (map)', () { 836 | context['o'] = { 837 | 'sub1': (a, {b: 0}) => a - b, 838 | 'sub2': ({a: 0, b: 0}) => a - b 839 | }; 840 | expect(eval("o.sub1(1)")).toEqual(1); 841 | expect(eval("o.sub1(3, b: 2)")).toEqual(1); 842 | 843 | expect(eval("o.sub2()")).toEqual(0); 844 | expect(eval("o.sub2(a: 3)")).toEqual(3); 845 | expect(eval("o.sub2(a: 3, b: 2)")).toEqual(1); 846 | expect(eval("o.sub2(b: 4)")).toEqual(-4); 847 | }); 848 | }); 849 | 850 | describe('child group', () { 851 | it('should remove all field watches in group and group\'s children', () { 852 | watchGrp.watch(parse('a'), (v, p) => logger('0a')); 853 | var child1a = watchGrp.newGroup(new PrototypeMap(context)); 854 | var child1b = watchGrp.newGroup(new PrototypeMap(context)); 855 | var child2 = child1a.newGroup(new PrototypeMap(context)); 856 | child1a.watch(parse('a'), (v, p) => logger('1a')); 857 | child1b.watch(parse('a'), (v, p) => logger('1b')); 858 | watchGrp.watch(parse('a'), (v, p) => logger('0A')); 859 | child1a.watch(parse('a'), (v, p) => logger('1A')); 860 | child2.watch(parse('a'), (v, p) => logger('2A')); 861 | 862 | // flush initial reaction functions 863 | expect(watchGrp.detectChanges()).toEqual(6); 864 | // expect(logger).toEqual(['0a', '0A', '1a', '1A', '2A', '1b']); 865 | expect(logger).toEqual(['0a', '1a', '1b', '0A', '1A', '2A']); // we go by registration order 866 | expect(watchGrp.fieldCost).toEqual(1); 867 | expect(watchGrp.totalFieldCost).toEqual(4); 868 | logger.clear(); 869 | 870 | context['a'] = 1; 871 | expect(watchGrp.detectChanges()).toEqual(6); 872 | expect(logger).toEqual(['0a', '0A', '1a', '1A', '2A', '1b']); // we go by group order 873 | logger.clear(); 874 | 875 | context['a'] = 2; 876 | child1a.remove(); // should also remove child2 877 | expect(watchGrp.detectChanges()).toEqual(3); 878 | expect(logger).toEqual(['0a', '0A', '1b']); 879 | expect(watchGrp.fieldCost).toEqual(1); 880 | expect(watchGrp.totalFieldCost).toEqual(2); 881 | }); 882 | 883 | it('should remove all method watches in group and group\'s children', () { 884 | context['my'] = new MyClass(logger); 885 | AST countMethod = new MethodAST(parse('my'), 'count', []); 886 | watchGrp.watch(countMethod, (v, p) => logger('0a')); 887 | expectOrder(['0a']); 888 | 889 | var child1a = watchGrp.newGroup(new PrototypeMap(context)); 890 | var child1b = watchGrp.newGroup(new PrototypeMap(context)); 891 | var child2 = child1a.newGroup(new PrototypeMap(context)); 892 | var child3 = child2.newGroup(new PrototypeMap(context)); 893 | child1a.watch(countMethod, (v, p) => logger('1a')); 894 | expectOrder(['0a', '1a']); 895 | child1b.watch(countMethod, (v, p) => logger('1b')); 896 | expectOrder(['0a', '1a', '1b']); 897 | watchGrp.watch(countMethod, (v, p) => logger('0A')); 898 | expectOrder(['0a', '0A', '1a', '1b']); 899 | child1a.watch(countMethod, (v, p) => logger('1A')); 900 | expectOrder(['0a', '0A', '1a', '1A', '1b']); 901 | child2.watch(countMethod, (v, p) => logger('2A')); 902 | expectOrder(['0a', '0A', '1a', '1A', '2A', '1b']); 903 | child3.watch(countMethod, (v, p) => logger('3')); 904 | expectOrder(['0a', '0A', '1a', '1A', '2A', '3', '1b']); 905 | 906 | // flush initial reaction functions 907 | expect(watchGrp.detectChanges()).toEqual(7); 908 | expectOrder(['0a', '0A', '1a', '1A', '2A', '3', '1b']); 909 | 910 | child1a.remove(); // should also remove child2 and child 3 911 | expect(watchGrp.detectChanges()).toEqual(3); 912 | expectOrder(['0a', '0A', '1b']); 913 | }); 914 | 915 | it('should add watches within its own group', () { 916 | context['my'] = new MyClass(logger); 917 | AST countMethod = new MethodAST(parse('my'), 'count', []); 918 | var ra = watchGrp.watch(countMethod, (v, p) => logger('a')); 919 | var child = watchGrp.newGroup(new PrototypeMap(context)); 920 | var cb = child.watch(countMethod, (v, p) => logger('b')); 921 | 922 | expectOrder(['a', 'b']); 923 | expectOrder(['a', 'b']); 924 | 925 | ra.remove(); 926 | expectOrder(['b']); 927 | 928 | cb.remove(); 929 | expectOrder([]); 930 | 931 | // TODO: add them back in wrong order, assert events in right order 932 | cb = child.watch(countMethod, (v, p) => logger('b')); 933 | ra = watchGrp.watch(countMethod, (v, p) => logger('a'));; 934 | expectOrder(['a', 'b']); 935 | }); 936 | 937 | 938 | it('should not call reaction function on removed group', () { 939 | var log = []; 940 | context['name'] = 'misko'; 941 | var child = watchGrp.newGroup(context); 942 | watchGrp.watch(parse('name'), (v, _) { 943 | log.add('root $v'); 944 | if (v == 'destroy') { 945 | child.remove(); 946 | } 947 | }); 948 | child.watch(parse('name'), (v, _) => log.add('child $v')); 949 | watchGrp.detectChanges(); 950 | expect(log).toEqual(['root misko', 'child misko']); 951 | log.clear(); 952 | 953 | context['name'] = 'destroy'; 954 | watchGrp.detectChanges(); 955 | expect(log).toEqual(['root destroy']); 956 | }); 957 | 958 | 959 | 960 | it('should watch children', () { 961 | var childContext = new PrototypeMap(context); 962 | context['a'] = 'OK'; 963 | context['b'] = 'BAD'; 964 | childContext['b'] = 'OK'; 965 | watchGrp.watch(parse('a'), (v, p) => logger(v)); 966 | watchGrp.newGroup(childContext).watch(parse('b'), (v, p) => logger(v)); 967 | 968 | watchGrp.detectChanges(); 969 | expect(logger).toEqual(['OK', 'OK']); 970 | logger.clear(); 971 | 972 | context['a'] = 'A'; 973 | childContext['b'] = 'B'; 974 | 975 | watchGrp.detectChanges(); 976 | expect(logger).toEqual(['A', 'B']); 977 | logger.clear(); 978 | }); 979 | }); 980 | 981 | }); 982 | } 983 | 984 | class MyClass { 985 | final Logger logger; 986 | var valA; 987 | int _count = 0; 988 | 989 | MyClass(this.logger); 990 | 991 | methodA(arg1) { 992 | logger('methodA($arg1) => $valA'); 993 | return valA; 994 | } 995 | 996 | count() => _count++; 997 | 998 | String toString() => 'MyClass'; 999 | } 1000 | 1001 | class LoggingFunctionApply extends FunctionApply { 1002 | Logger logger; 1003 | LoggingFunctionApply(this.logger); 1004 | apply(List args) => logger(args); 1005 | } 1006 | -------------------------------------------------------------------------------- /change_detection/test/dirty_checking_change_detector_spec.dart: -------------------------------------------------------------------------------- 1 | library dirty_chekcing_change_detector_spec; 2 | 3 | import 'package:guinness/guinness.dart'; 4 | import 'package:unittest/unittest.dart' show Matcher, Description; 5 | import '../lib/change_detection.dart'; 6 | import '../lib/dirty_checking_change_detector.dart'; 7 | import '../lib/dirty_checking_change_detector_static.dart'; 8 | import '../lib/dirty_checking_change_detector_dynamic.dart'; 9 | import 'dart:collection'; 10 | import 'dart:math'; 11 | 12 | void testWithGetterFactory(FieldGetterFactory getterFactory) { 13 | describe('DirtyCheckingChangeDetector with ${getterFactory.runtimeType}', () { 14 | DirtyCheckingChangeDetector detector; 15 | 16 | beforeEach(() { 17 | detector = new DirtyCheckingChangeDetector(getterFactory); 18 | }); 19 | 20 | describe('object field', () { 21 | it('should detect nothing', () { 22 | var changes = detector.collectChanges(); 23 | expect(changes.moveNext()).toEqual(false); 24 | }); 25 | 26 | it('should detect field changes', () { 27 | var user = new _User('', ''); 28 | Iterator changeIterator; 29 | 30 | detector.watch(user, 'first', null); 31 | detector.watch(user, 'last', null); 32 | detector.collectChanges(); // throw away first set 33 | 34 | changeIterator = detector.collectChanges(); 35 | expect(changeIterator.moveNext()).toEqual(false); 36 | user.first = 'misko'; 37 | user.last = 'hevery'; 38 | 39 | changeIterator = detector.collectChanges(); 40 | expect(changeIterator.moveNext()).toEqual(true); 41 | expect(changeIterator.current.currentValue).toEqual('misko'); 42 | expect(changeIterator.current.previousValue).toEqual(''); 43 | expect(changeIterator.moveNext()).toEqual(true); 44 | expect(changeIterator.current.currentValue).toEqual('hevery'); 45 | expect(changeIterator.current.previousValue).toEqual(''); 46 | expect(changeIterator.moveNext()).toEqual(false); 47 | 48 | // force different instance 49 | user.first = 'mis'; 50 | user.first += 'ko'; 51 | 52 | changeIterator = detector.collectChanges(); 53 | expect(changeIterator.moveNext()).toEqual(false); 54 | 55 | user.last = 'Hevery'; 56 | changeIterator = detector.collectChanges(); 57 | expect(changeIterator.moveNext()).toEqual(true); 58 | expect(changeIterator.current.currentValue).toEqual('Hevery'); 59 | expect(changeIterator.current.previousValue).toEqual('hevery'); 60 | expect(changeIterator.moveNext()).toEqual(false); 61 | }); 62 | 63 | it('should ignore NaN != NaN', () { 64 | var user = new _User(); 65 | user.age = double.NAN; 66 | detector.watch(user, 'age', null); 67 | detector.collectChanges(); // throw away first set 68 | 69 | var changeIterator = detector.collectChanges(); 70 | expect(changeIterator.moveNext()).toEqual(false); 71 | 72 | user.age = 123; 73 | changeIterator = detector.collectChanges(); 74 | expect(changeIterator.moveNext()).toEqual(true); 75 | expect(changeIterator.current.currentValue).toEqual(123); 76 | expect(changeIterator.current.previousValue.isNaN).toEqual(true); 77 | expect(changeIterator.moveNext()).toEqual(false); 78 | }); 79 | 80 | it('should treat map field dereference as []', () { 81 | var obj = {'name':'misko'}; 82 | detector.watch(obj, 'name', null); 83 | detector.collectChanges(); // throw away first set 84 | 85 | obj['name'] = 'Misko'; 86 | var changeIterator = detector.collectChanges(); 87 | expect(changeIterator.moveNext()).toEqual(true); 88 | expect(changeIterator.current.currentValue).toEqual('Misko'); 89 | expect(changeIterator.current.previousValue).toEqual('misko'); 90 | }); 91 | }); 92 | 93 | describe('insertions / removals', () { 94 | it('should insert at the end of list', () { 95 | var obj = {}; 96 | var a = detector.watch(obj, 'a', 'a'); 97 | var b = detector.watch(obj, 'b', 'b'); 98 | 99 | obj['a'] = obj['b'] = 1; 100 | var changeIterator = detector.collectChanges(); 101 | expect(changeIterator.moveNext()).toEqual(true); 102 | expect(changeIterator.current.handler).toEqual('a'); 103 | expect(changeIterator.moveNext()).toEqual(true); 104 | expect(changeIterator.current.handler).toEqual('b'); 105 | expect(changeIterator.moveNext()).toEqual(false); 106 | 107 | obj['a'] = obj['b'] = 2; 108 | a.remove(); 109 | changeIterator = detector.collectChanges(); 110 | expect(changeIterator.moveNext()).toEqual(true); 111 | expect(changeIterator.current.handler).toEqual('b'); 112 | expect(changeIterator.moveNext()).toEqual(false); 113 | 114 | obj['a'] = obj['b'] = 3; 115 | b.remove(); 116 | changeIterator = detector.collectChanges(); 117 | expect(changeIterator.moveNext()).toEqual(false); 118 | }); 119 | 120 | it('should remove all watches in group and group\'s children', () { 121 | var obj = {}; 122 | detector.watch(obj, 'a', '0a'); 123 | var child1a = detector.newGroup(); 124 | var child1b = detector.newGroup(); 125 | var child2 = child1a.newGroup(); 126 | child1a.watch(obj,'a', '1a'); 127 | child1b.watch(obj,'a', '1b'); 128 | detector.watch(obj, 'a', '0A'); 129 | child1a.watch(obj,'a', '1A'); 130 | child2.watch(obj,'a', '2A'); 131 | 132 | var iterator; 133 | obj['a'] = 1; 134 | expect(detector.collectChanges(), 135 | toEqualChanges(['0a', '0A', '1a', '1A', '2A', '1b'])); 136 | 137 | obj['a'] = 2; 138 | child1a.remove(); // should also remove child2 139 | expect(detector.collectChanges(), toEqualChanges(['0a', '0A', '1b'])); 140 | }); 141 | 142 | it('should add watches within its own group', () { 143 | var obj = {}; 144 | var ra = detector.watch(obj, 'a', 'a'); 145 | var child = detector.newGroup(); 146 | var cb = child.watch(obj,'b', 'b'); 147 | var iterotar; 148 | 149 | obj['a'] = obj['b'] = 1; 150 | expect(detector.collectChanges(), toEqualChanges(['a', 'b'])); 151 | 152 | obj['a'] = obj['b'] = 2; 153 | ra.remove(); 154 | expect(detector.collectChanges(), toEqualChanges(['b'])); 155 | 156 | obj['a'] = obj['b'] = 3; 157 | cb.remove(); 158 | expect(detector.collectChanges(), toEqualChanges([])); 159 | 160 | // TODO: add them back in wrong order, assert events in right order 161 | cb = child.watch(obj,'b', 'b'); 162 | ra = detector.watch(obj, 'a', 'a'); 163 | obj['a'] = obj['b'] = 4; 164 | expect(detector.collectChanges(), toEqualChanges(['a', 'b'])); 165 | }); 166 | 167 | it('should properly add children', () { 168 | var a = detector.newGroup(); 169 | var aChild = a.newGroup(); 170 | var b = detector.newGroup(); 171 | expect(detector.collectChanges).not.toThrow(); 172 | }); 173 | 174 | it('should properly disconnect group in case watch is removed in disconected group', () { 175 | var map = {}; 176 | var detector0 = new DirtyCheckingChangeDetector(getterFactory); 177 | var detector1 = detector0.newGroup(); 178 | var detector2 = detector1.newGroup(); 179 | var watch2 = detector2.watch(map, 'f1', null); 180 | var detector3 = detector0.newGroup(); 181 | detector1.remove(); 182 | watch2.remove(); // removing a dead record 183 | detector3.watch(map, 'f2', null); 184 | }); 185 | 186 | it('should find random bugs', () { 187 | List detectors; 188 | List records; 189 | List steps; 190 | var field = 'someField'; 191 | step(text) { 192 | //print(text); 193 | steps.add(text); 194 | } 195 | Map map = {}; 196 | var random = new Random(); 197 | try { 198 | for (var i = 0; i < 100000; i++) { 199 | if (i % 50 == 0) { 200 | records = []; 201 | steps = []; 202 | detectors = [new DirtyCheckingChangeDetector(getterFactory)]; 203 | } 204 | switch (random.nextInt(4)) { 205 | case 0: // new child detector 206 | if (detectors.length > 10) break; 207 | var index = random.nextInt(detectors.length); 208 | ChangeDetectorGroup detector = detectors[index]; 209 | step('detectors[$index].newGroup()'); 210 | var child = detector.newGroup(); 211 | detectors.add(child); 212 | break; 213 | case 1: // add watch 214 | var index = random.nextInt(detectors.length); 215 | ChangeDetectorGroup detector = detectors[index]; 216 | step('detectors[$index].watch(map, field, null)'); 217 | WatchRecord record = detector.watch(map, field, null); 218 | records.add(record); 219 | break; 220 | case 2: // destroy watch group 221 | if (detectors.length == 1) break; 222 | var index = random.nextInt(detectors.length - 1) + 1; 223 | ChangeDetectorGroup detector = detectors[index]; 224 | step('detectors[$index].remove()'); 225 | detector.remove(); 226 | detectors = detectors 227 | .where((s) => s.isAttached) 228 | .toList(); 229 | break; 230 | case 3: // remove watch on watch group 231 | if (records.length == 0) break; 232 | var index = random.nextInt(records.length); 233 | WatchRecord record = records.removeAt(index); 234 | step('records.removeAt($index).remove()'); 235 | record.remove(); 236 | break; 237 | } 238 | } 239 | } catch(e) { 240 | print(steps); 241 | rethrow; 242 | } 243 | }); 244 | 245 | }); 246 | 247 | describe('list watching', () { 248 | describe('previous state', () { 249 | it('should store on addition', () { 250 | var list = []; 251 | var record = detector.watch(list, null, null); 252 | expect(detector.collectChanges().moveNext()).toEqual(false); 253 | var iterator; 254 | 255 | list.add('a'); 256 | iterator = detector.collectChanges(); 257 | expect(iterator.moveNext()).toEqual(true); 258 | expect(iterator.current.currentValue, toEqualCollectionRecord( 259 | ['a[null -> 0]'], 260 | [], 261 | ['a[null -> 0]'], 262 | [], 263 | [])); 264 | 265 | list.add('b'); 266 | iterator = detector.collectChanges(); 267 | expect(iterator.moveNext()).toEqual(true); 268 | expect(iterator.current.currentValue, toEqualCollectionRecord( 269 | ['a', 'b[null -> 1]'], 270 | ['a'], 271 | ['b[null -> 1]'], 272 | [], 273 | [])); 274 | }); 275 | 276 | it('should handle swapping elements correctly', () { 277 | var list = [1, 2]; 278 | var record = detector.watch(list, null, null); 279 | detector.collectChanges().moveNext(); 280 | var iterator; 281 | 282 | // reverse the list. 283 | list.setAll(0, list.reversed.toList()); 284 | iterator = detector.collectChanges(); 285 | expect(iterator.moveNext()).toEqual(true); 286 | expect(iterator.current.currentValue, toEqualCollectionRecord( 287 | ['2[1 -> 0]', '1[0 -> 1]'], 288 | ['1[0 -> 1]', '2[1 -> 0]'], 289 | [], 290 | ['2[1 -> 0]', '1[0 -> 1]'], 291 | [])); 292 | }); 293 | 294 | it('should handle swapping elements correctly - gh1097', () { 295 | // This test would only have failed in non-checked mode only 296 | var list = ['a', 'b', 'c']; 297 | var record = detector.watch(list, null, null); 298 | var iterator = detector.collectChanges(); 299 | iterator.moveNext(); 300 | 301 | list.clear(); 302 | list.addAll(['b', 'a', 'c']); 303 | iterator = detector.collectChanges(); 304 | iterator.moveNext(); 305 | expect(iterator.current.currentValue, toEqualCollectionRecord( 306 | ['b[1 -> 0]', 'a[0 -> 1]', 'c'], 307 | ['a[0 -> 1]', 'b[1 -> 0]', 'c'], 308 | [], 309 | ['b[1 -> 0]', 'a[0 -> 1]'], 310 | [])); 311 | 312 | list.clear(); 313 | list.addAll(['b', 'c', 'a']); 314 | iterator = detector.collectChanges(); 315 | iterator.moveNext(); 316 | expect(iterator.current.currentValue, toEqualCollectionRecord( 317 | ['b', 'c[2 -> 1]', 'a[1 -> 2]'], 318 | ['b', 'a[1 -> 2]', 'c[2 -> 1]'], 319 | [], 320 | ['c[2 -> 1]', 'a[1 -> 2]'], 321 | [])); 322 | }); 323 | }); 324 | 325 | it('should detect changes in list', () { 326 | var list = []; 327 | var record = detector.watch(list, null, 'handler'); 328 | expect(detector.collectChanges().moveNext()).toEqual(false); 329 | var iterator; 330 | 331 | list.add('a'); 332 | iterator = detector.collectChanges(); 333 | expect(iterator.moveNext()).toEqual(true); 334 | expect(iterator.current.currentValue, toEqualCollectionRecord( 335 | ['a[null -> 0]'], 336 | null, 337 | ['a[null -> 0]'], 338 | [], 339 | [])); 340 | 341 | list.add('b'); 342 | iterator = detector.collectChanges(); 343 | expect(iterator.moveNext()).toEqual(true); 344 | expect(iterator.current.currentValue, toEqualCollectionRecord( 345 | ['a', 'b[null -> 1]'], 346 | ['a'], 347 | ['b[null -> 1]'], 348 | [], 349 | [])); 350 | 351 | list.add('c'); 352 | list.add('d'); 353 | iterator = detector.collectChanges(); 354 | expect(iterator.moveNext()).toEqual(true); 355 | expect(iterator.current.currentValue, toEqualCollectionRecord( 356 | ['a', 'b', 'c[null -> 2]', 'd[null -> 3]'], 357 | ['a', 'b'], 358 | ['c[null -> 2]', 'd[null -> 3]'], 359 | [], 360 | [])); 361 | 362 | list.remove('c'); 363 | expect(list).toEqual(['a', 'b', 'd']); 364 | iterator = detector.collectChanges(); 365 | expect(iterator.moveNext()).toEqual(true); 366 | expect(iterator.current.currentValue, toEqualCollectionRecord( 367 | ['a', 'b', 'd[3 -> 2]'], 368 | ['a', 'b', 'c[2 -> null]', 'd[3 -> 2]'], 369 | [], 370 | ['d[3 -> 2]'], 371 | ['c[2 -> null]'])); 372 | 373 | list.clear(); 374 | list.addAll(['d', 'c', 'b', 'a']); 375 | iterator = detector.collectChanges(); 376 | expect(iterator.moveNext()).toEqual(true); 377 | expect(iterator.current.currentValue, toEqualCollectionRecord( 378 | ['d[2 -> 0]', 'c[null -> 1]', 'b[1 -> 2]', 'a[0 -> 3]'], 379 | ['a[0 -> 3]', 'b[1 -> 2]', 'd[2 -> 0]'], 380 | ['c[null -> 1]'], 381 | ['d[2 -> 0]', 'b[1 -> 2]', 'a[0 -> 3]'], 382 | [])); 383 | }); 384 | 385 | it('should test string by value rather than by reference', () { 386 | var list = ['a', 'boo']; 387 | detector.watch(list, null, null); 388 | detector.collectChanges(); 389 | 390 | list[1] = 'b' + 'oo'; 391 | 392 | expect(detector.collectChanges().moveNext()).toEqual(false); 393 | }); 394 | 395 | it('should ignore [NaN] != [NaN]', () { 396 | var list = [double.NAN]; 397 | var record = detector; 398 | record.watch(list, null, null); 399 | record.collectChanges(); 400 | 401 | expect(detector.collectChanges().moveNext()).toEqual(false); 402 | }); 403 | 404 | xit('should detect [NaN] moves', () { 405 | var list = [double.NAN, double.NAN]; 406 | detector.watch(list, null, null); 407 | detector.collectChanges(); 408 | 409 | list.clear(); 410 | list.addAll(['foo', double.NAN, double.NAN]); 411 | var iterator = detector.collectChanges(); 412 | expect(iterator.moveNext()).toEqual(true); 413 | expect(iterator.current.currentValue, toEqualCollectionRecord( 414 | ['foo[null -> 0]', 'NaN[0 -> 1]', 'NaN[1 -> 2]'], 415 | ['NaN[0 -> 1]', 'NaN[1 -> 2]'], 416 | ['foo[null -> 0]'], 417 | ['NaN[0 -> 1]', 'NaN[1 -> 2]'], 418 | [])); 419 | }); 420 | 421 | it('should remove and add same item', () { 422 | var list = ['a', 'b', 'c']; 423 | var record = detector.watch(list, null, 'handler'); 424 | var iterator; 425 | detector.collectChanges(); 426 | 427 | list.remove('b'); 428 | iterator = detector.collectChanges(); 429 | iterator.moveNext(); 430 | expect(iterator.current.currentValue, toEqualCollectionRecord( 431 | ['a', 'c[2 -> 1]'], 432 | ['a', 'b[1 -> null]', 'c[2 -> 1]'], 433 | [], 434 | ['c[2 -> 1]'], 435 | ['b[1 -> null]'])); 436 | 437 | list.insert(1, 'b'); 438 | expect(list).toEqual(['a', 'b', 'c']); 439 | iterator = detector.collectChanges(); 440 | iterator.moveNext(); 441 | expect(iterator.current.currentValue, toEqualCollectionRecord( 442 | ['a', 'b[null -> 1]', 'c[1 -> 2]'], 443 | ['a', 'c[1 -> 2]'], 444 | ['b[null -> 1]'], 445 | ['c[1 -> 2]'], 446 | [])); 447 | }); 448 | 449 | it('should support duplicates', () { 450 | var list = ['a', 'a', 'a', 'b', 'b']; 451 | var record = detector.watch(list, null, 'handler'); 452 | detector.collectChanges(); 453 | 454 | list.removeAt(0); 455 | var iterator = detector.collectChanges(); 456 | iterator.moveNext(); 457 | expect(iterator.current.currentValue, toEqualCollectionRecord( 458 | ['a', 'a', 'b[3 -> 2]', 'b[4 -> 3]'], 459 | ['a', 'a', 'a[2 -> null]', 'b[3 -> 2]', 'b[4 -> 3]'], 460 | [], 461 | ['b[3 -> 2]', 'b[4 -> 3]'], 462 | ['a[2 -> null]'])); 463 | }); 464 | 465 | 466 | it('should support insertions/moves', () { 467 | var list = ['a', 'a', 'b', 'b']; 468 | var record = detector.watch(list, null, 'handler'); 469 | var iterator; 470 | detector.collectChanges(); 471 | list.insert(0, 'b'); 472 | expect(list).toEqual(['b', 'a', 'a', 'b', 'b']); 473 | iterator = detector.collectChanges(); 474 | iterator.moveNext(); 475 | expect(iterator.current.currentValue, toEqualCollectionRecord( 476 | ['b[2 -> 0]', 'a[0 -> 1]', 'a[1 -> 2]', 'b', 'b[null -> 4]'], 477 | ['a[0 -> 1]', 'a[1 -> 2]', 'b[2 -> 0]', 'b'], 478 | ['b[null -> 4]'], 479 | ['b[2 -> 0]', 'a[0 -> 1]', 'a[1 -> 2]'], 480 | [])); 481 | }); 482 | 483 | it('should support UnmodifiableListView', () { 484 | var hiddenList = [1]; 485 | var list = new UnmodifiableListView(hiddenList); 486 | var record = detector.watch(list, null, 'handler'); 487 | var iterator = detector.collectChanges(); 488 | iterator.moveNext(); 489 | expect(iterator.current.currentValue, toEqualCollectionRecord( 490 | ['1[null -> 0]'], 491 | null, 492 | ['1[null -> 0]'], 493 | [], 494 | [])); 495 | 496 | // assert no changes detected 497 | expect(detector.collectChanges().moveNext()).toEqual(false); 498 | 499 | // change the hiddenList normally this should trigger change detection 500 | // but because we are wrapped in UnmodifiableListView we see nothing. 501 | hiddenList[0] = 2; 502 | expect(detector.collectChanges().moveNext()).toEqual(false); 503 | }); 504 | 505 | it('should bug', () { 506 | var list = [1, 2, 3, 4]; 507 | var record = detector.watch(list, null, 'handler'); 508 | var iterator; 509 | 510 | iterator = detector.collectChanges(); 511 | iterator.moveNext(); 512 | expect(iterator.current.currentValue, toEqualCollectionRecord( 513 | ['1[null -> 0]', '2[null -> 1]', '3[null -> 2]', '4[null -> 3]'], 514 | null, 515 | ['1[null -> 0]', '2[null -> 1]', '3[null -> 2]', '4[null -> 3]'], 516 | [], 517 | [])); 518 | detector.collectChanges(); 519 | 520 | list.removeRange(0, 1); 521 | iterator = detector.collectChanges(); 522 | iterator.moveNext(); 523 | expect(iterator.current.currentValue, toEqualCollectionRecord( 524 | ['2[1 -> 0]', '3[2 -> 1]', '4[3 -> 2]'], 525 | ['1[0 -> null]', '2[1 -> 0]', '3[2 -> 1]', '4[3 -> 2]'], 526 | [], 527 | ['2[1 -> 0]', '3[2 -> 1]', '4[3 -> 2]'], 528 | ['1[0 -> null]'])); 529 | 530 | list.insert(0, 1); 531 | iterator = detector.collectChanges(); 532 | iterator.moveNext(); 533 | expect(iterator.current.currentValue, toEqualCollectionRecord( 534 | ['1[null -> 0]', '2[0 -> 1]', '3[1 -> 2]', '4[2 -> 3]'], 535 | ['2[0 -> 1]', '3[1 -> 2]', '4[2 -> 3]'], 536 | ['1[null -> 0]'], 537 | ['2[0 -> 1]', '3[1 -> 2]', '4[2 -> 3]'], 538 | [])); 539 | }); 540 | 541 | it('should properly support objects with equality', () { 542 | FooBar.fooIds = 0; 543 | var list = [new FooBar('a', 'a'), new FooBar('a', 'a')]; 544 | var record = detector.watch(list, null, 'handler'); 545 | var iterator; 546 | 547 | iterator = detector.collectChanges(); 548 | iterator.moveNext(); 549 | expect(iterator.current.currentValue, toEqualCollectionRecord( 550 | ['(0)a-a[null -> 0]', '(1)a-a[null -> 1]'], 551 | null, 552 | ['(0)a-a[null -> 0]', '(1)a-a[null -> 1]'], 553 | [], 554 | [])); 555 | detector.collectChanges(); 556 | 557 | list.removeRange(0, 1); 558 | iterator = detector.collectChanges(); 559 | iterator.moveNext(); 560 | expect(iterator.current.currentValue, toEqualCollectionRecord( 561 | ['(1)a-a[1 -> 0]'], 562 | ['(0)a-a[0 -> null]', '(1)a-a[1 -> 0]'], 563 | [], 564 | ['(1)a-a[1 -> 0]'], 565 | ['(0)a-a[0 -> null]'])); 566 | 567 | list.insert(0, new FooBar('a', 'a')); 568 | iterator = detector.collectChanges(); 569 | iterator.moveNext(); 570 | expect(iterator.current.currentValue, toEqualCollectionRecord( 571 | ['(2)a-a[null -> 0]', '(1)a-a[0 -> 1]'], 572 | ['(1)a-a[0 -> 1]'], 573 | ['(2)a-a[null -> 0]'], 574 | ['(1)a-a[0 -> 1]'], 575 | [])); 576 | }); 577 | 578 | it('should not report unnecessary moves', () { 579 | var list = ['a', 'b', 'c']; 580 | var record = detector.watch(list, null, null); 581 | var iterator = detector.collectChanges(); 582 | iterator.moveNext(); 583 | 584 | list.clear(); 585 | list.addAll(['b', 'a', 'c']); 586 | iterator = detector.collectChanges(); 587 | iterator.moveNext(); 588 | expect(iterator.current.currentValue, toEqualCollectionRecord( 589 | ['b[1 -> 0]', 'a[0 -> 1]', 'c'], 590 | ['a[0 -> 1]', 'b[1 -> 0]', 'c'], 591 | [], 592 | ['b[1 -> 0]', 'a[0 -> 1]'], 593 | [])); 594 | }); 595 | }); 596 | 597 | describe('map watching', () { 598 | describe('previous state', () { 599 | it('should store on insertion', () { 600 | var map = {}; 601 | var record = detector.watch(map, null, null); 602 | expect(detector.collectChanges().moveNext()).toEqual(false); 603 | var iterator; 604 | 605 | map['a'] = 1; 606 | iterator = detector.collectChanges(); 607 | expect(iterator.moveNext()).toEqual(true); 608 | expect(iterator.current.currentValue, toEqualMapRecord( 609 | ['a[null -> 1]'], 610 | [], 611 | ['a[null -> 1]'], 612 | [], 613 | [])); 614 | 615 | map['b'] = 2; 616 | iterator = detector.collectChanges(); 617 | expect(iterator.moveNext()).toEqual(true); 618 | expect(iterator.current.currentValue, toEqualMapRecord( 619 | ['a', 'b[null -> 2]'], 620 | ['a'], 621 | ['b[null -> 2]'], 622 | [], 623 | [])); 624 | }); 625 | 626 | it('should handle changing key/values correctly', () { 627 | var map = {1: 10, 2: 20}; 628 | var record = detector.watch(map, null, null); 629 | detector.collectChanges().moveNext(); 630 | var iterator; 631 | 632 | map[1] = 20; 633 | map[2] = 10; 634 | iterator = detector.collectChanges(); 635 | expect(iterator.moveNext()).toEqual(true); 636 | expect(iterator.current.currentValue, toEqualMapRecord( 637 | ['1[10 -> 20]', '2[20 -> 10]'], 638 | ['1[10 -> 20]', '2[20 -> 10]'], 639 | [], 640 | ['1[10 -> 20]', '2[20 -> 10]'], 641 | [])); 642 | }); 643 | }); 644 | 645 | it('should do basic map watching', () { 646 | var map = {}; 647 | var record = detector.watch(map, null, 'handler'); 648 | expect(detector.collectChanges().moveNext()).toEqual(false); 649 | 650 | var changeIterator; 651 | map['a'] = 'A'; 652 | changeIterator = detector.collectChanges(); 653 | expect(changeIterator.moveNext()).toEqual(true); 654 | expect(changeIterator.current.currentValue, toEqualMapRecord( 655 | ['a[null -> A]'], 656 | [], 657 | ['a[null -> A]'], 658 | [], 659 | [])); 660 | 661 | map['b'] = 'B'; 662 | changeIterator = detector.collectChanges(); 663 | expect(changeIterator.moveNext()).toEqual(true); 664 | expect(changeIterator.current.currentValue, toEqualMapRecord( 665 | ['a', 'b[null -> B]'], 666 | ['a'], 667 | ['b[null -> B]'], 668 | [], 669 | [])); 670 | 671 | map['b'] = 'BB'; 672 | map['d'] = 'D'; 673 | changeIterator = detector.collectChanges(); 674 | expect(changeIterator.moveNext()).toEqual(true); 675 | expect(changeIterator.current.currentValue, toEqualMapRecord( 676 | ['a', 'b[B -> BB]', 'd[null -> D]'], 677 | ['a', 'b[B -> BB]'], 678 | ['d[null -> D]'], 679 | ['b[B -> BB]'], 680 | [])); 681 | 682 | map.remove('b'); 683 | expect(map).toEqual({'a': 'A', 'd':'D'}); 684 | changeIterator = detector.collectChanges(); 685 | expect(changeIterator.moveNext()).toEqual(true); 686 | expect(changeIterator.current.currentValue, toEqualMapRecord( 687 | ['a', 'd'], 688 | ['a', 'b[BB -> null]', 'd'], 689 | [], 690 | [], 691 | ['b[BB -> null]'])); 692 | 693 | map.clear(); 694 | changeIterator = detector.collectChanges(); 695 | expect(changeIterator.moveNext()).toEqual(true); 696 | expect(changeIterator.current.currentValue, toEqualMapRecord( 697 | [], 698 | ['a[A -> null]', 'd[D -> null]'], 699 | [], 700 | [], 701 | ['a[A -> null]', 'd[D -> null]'])); 702 | }); 703 | 704 | it('should test string keys by value rather than by reference', () { 705 | var map = {'foo': 0}; 706 | detector.watch(map, null, null); 707 | detector.collectChanges(); 708 | 709 | map['f' + 'oo'] = 0; 710 | 711 | expect(detector.collectChanges().moveNext()).toEqual(false); 712 | }); 713 | 714 | it('should test string values by value rather than by reference', () { 715 | var map = {'foo': 'bar'}; 716 | detector.watch(map, null, null); 717 | detector.collectChanges(); 718 | 719 | map['foo'] = 'b' + 'ar'; 720 | 721 | expect(detector.collectChanges().moveNext()).toEqual(false); 722 | }); 723 | 724 | it('should not see a NaN value as a change', () { 725 | var map = {'foo': double.NAN}; 726 | var record = detector; 727 | record.watch(map, null, null); 728 | record.collectChanges(); 729 | 730 | expect(detector.collectChanges().moveNext()).toEqual(false); 731 | }); 732 | }); 733 | 734 | describe('function watching', () { 735 | it('should detect no changes when watching a function', () { 736 | var user = new _User('marko', 'vuksanovic', 15); 737 | Iterator changeIterator; 738 | 739 | detector.watch(user, 'isUnderAge', null); 740 | changeIterator = detector.collectChanges(); 741 | expect(changeIterator.moveNext()).toEqual(true); 742 | expect(changeIterator.moveNext()).toEqual(false); 743 | 744 | user.age = 17; 745 | changeIterator = detector.collectChanges(); 746 | expect(changeIterator.moveNext()).toEqual(false); 747 | 748 | user.age = 30; 749 | changeIterator = detector.collectChanges(); 750 | expect(changeIterator.moveNext()).toEqual(false); 751 | }); 752 | 753 | it('should detect change when watching a property function', () { 754 | var user = new _User('marko', 'vuksanovic', 30); 755 | Iterator changeIterator; 756 | 757 | detector.watch(user, 'isUnderAgeAsVariable', null); 758 | changeIterator = detector.collectChanges(); 759 | expect(changeIterator.moveNext()).toEqual(true); 760 | 761 | changeIterator = detector.collectChanges(); 762 | expect(changeIterator.moveNext()).toEqual(false); 763 | 764 | user.isUnderAgeAsVariable = () => false; 765 | changeIterator = detector.collectChanges(); 766 | expect(changeIterator.moveNext()).toEqual(true); 767 | }); 768 | }); 769 | 770 | describe('DuplicateMap', () { 771 | DuplicateMap map; 772 | beforeEach(() => map = new DuplicateMap()); 773 | 774 | it('should do basic operations', () { 775 | var k1 = 'a'; 776 | var r1 = new ItemRecord(k1); 777 | r1.currentIndex = 1; 778 | map.put(r1); 779 | expect(map.get(k1, 2)).toEqual(null); 780 | expect(map.get(k1, 1)).toEqual(null); 781 | expect(map.get(k1, 0)).toEqual(r1); 782 | expect(map.remove(r1)).toEqual(r1); 783 | expect(map.get(k1, -1)).toEqual(null); 784 | }); 785 | 786 | it('should do basic operations on duplicate keys', () { 787 | var k1 = 'a'; 788 | var r1 = new ItemRecord(k1); 789 | r1.currentIndex = 1; 790 | var r2 = new ItemRecord(k1); 791 | r2.currentIndex = 2; 792 | map.put(r1); 793 | map.put(r2); 794 | expect(map.get(k1, 0)).toEqual(r1); 795 | expect(map.get(k1, 1)).toEqual(r2); 796 | expect(map.get(k1, 2)).toEqual(null); 797 | expect(map.remove(r2)).toEqual(r2); 798 | expect(map.get(k1, 0)).toEqual(r1); 799 | expect(map.remove(r1)).toEqual(r1); 800 | expect(map.get(k1, 0)).toEqual(null); 801 | }); 802 | }); 803 | }); 804 | } 805 | 806 | 807 | void main() { 808 | testWithGetterFactory(new DynamicFieldGetterFactory()); 809 | 810 | testWithGetterFactory(new StaticFieldGetterFactory({ 811 | "first": (o) => o.first, 812 | "age": (o) => o.age, 813 | "last": (o) => o.last, 814 | "toString": (o) => o.toString, 815 | "isUnderAge": (o) => o.isUnderAge, 816 | "isUnderAgeAsVariable": (o) => o.isUnderAgeAsVariable, 817 | })); 818 | } 819 | 820 | 821 | class _User { 822 | String first; 823 | String last; 824 | num age; 825 | var isUnderAgeAsVariable; 826 | List list = ['foo', 'bar', 'baz']; 827 | Map map = {'foo': 'bar', 'baz': 'cux'}; 828 | 829 | _User([this.first, this.last, this.age]) { 830 | isUnderAgeAsVariable = isUnderAge; 831 | } 832 | 833 | bool isUnderAge() { 834 | return age != null ? age < 18 : false; 835 | } 836 | } 837 | 838 | Matcher toEqualCollectionRecord(collection, previous, additions, moves, removals) => 839 | new CollectionRecordMatcher(collection, previous, 840 | additions, moves, removals); 841 | Matcher toEqualMapRecord(map, previous, additions, changes, removals) => 842 | new MapRecordMatcher(map, previous, 843 | additions, changes, removals); 844 | Matcher toEqualChanges(List changes) => new ChangeMatcher(changes); 845 | 846 | class ChangeMatcher extends Matcher { 847 | List expected; 848 | 849 | ChangeMatcher(this.expected); 850 | 851 | Description describe(Description description) { 852 | description.add(expected.toString()); 853 | return description; 854 | } 855 | 856 | Description describeMismatch(Iterator changes, 857 | Description mismatchDescription, 858 | Map matchState, bool verbose) { 859 | List list = []; 860 | while(changes.moveNext()) { 861 | list.add(changes.current.handler); 862 | } 863 | mismatchDescription.add(list.toString()); 864 | return mismatchDescription; 865 | } 866 | 867 | bool matches(Iterator changes, Map matchState) { 868 | int count = 0; 869 | while(changes.moveNext()) { 870 | if (changes.current.handler != expected[count++]) return false; 871 | } 872 | return count == expected.length; 873 | } 874 | } 875 | 876 | abstract class _CollectionMatcher extends Matcher { 877 | List _getList(Function it) { 878 | var result = []; 879 | it((item) { 880 | result.add(item); 881 | }); 882 | return result; 883 | } 884 | 885 | bool _compareLists(String tag, List expected, List actual, List diffs) { 886 | var equals = true; 887 | Iterator iActual = actual.iterator; 888 | iActual.moveNext(); 889 | T actualItem = iActual.current; 890 | if (expected == null) { 891 | expected = []; 892 | } 893 | for (String expectedItem in expected) { 894 | if (actualItem == null) { 895 | equals = false; 896 | diffs.add('$tag too short: $expectedItem'); 897 | } else { 898 | if ("$actualItem" != expectedItem) { 899 | equals = false; 900 | diffs.add('$tag mismatch: $actualItem != $expectedItem'); 901 | } 902 | iActual.moveNext(); 903 | actualItem = iActual.current; 904 | } 905 | } 906 | if (actualItem != null) { 907 | diffs.add('$tag too long: $actualItem'); 908 | equals = false; 909 | } 910 | return equals; 911 | } 912 | } 913 | 914 | class CollectionRecordMatcher extends _CollectionMatcher { 915 | final List collection; 916 | final List previous; 917 | final List additions; 918 | final List moves; 919 | final List removals; 920 | 921 | CollectionRecordMatcher(this.collection, this.previous, 922 | this.additions, this.moves, this.removals); 923 | 924 | Description describeMismatch(changes, Description mismatchDescription, 925 | Map matchState, bool verbose) { 926 | List diffs = matchState['diffs']; 927 | if (diffs == null) return mismatchDescription; 928 | mismatchDescription.add(diffs.join('\n')); 929 | return mismatchDescription; 930 | } 931 | 932 | Description describe(Description description) { 933 | add(name, collection) { 934 | if (collection != null) { 935 | description.add('$name: ${collection.join(', ')}\n '); 936 | } 937 | } 938 | 939 | add('collection', collection); 940 | add('previous', previous); 941 | add('additions', additions); 942 | add('moves', moves); 943 | add('removals', removals); 944 | return description; 945 | } 946 | 947 | bool matches(CollectionChangeRecord changeRecord, Map matchState) { 948 | var diffs = matchState['diffs'] = []; 949 | return checkCollection(changeRecord, diffs) && 950 | checkPrevious(changeRecord, diffs) && 951 | checkAdditions(changeRecord, diffs) && 952 | checkMoves(changeRecord, diffs) && 953 | checkRemovals(changeRecord, diffs); 954 | } 955 | 956 | bool checkCollection(CollectionChangeRecord changeRecord, List diffs) { 957 | List items = _getList((fn) => changeRecord.forEachItem(fn)); 958 | bool equals = _compareLists("collection", collection, items, diffs); 959 | int iterableLength = changeRecord.iterable.toList().length; 960 | if (iterableLength != items.length) { 961 | diffs.add('collection length mismatched: $iterableLength != ${items.length}'); 962 | equals = false; 963 | } 964 | return equals; 965 | } 966 | 967 | bool checkPrevious(CollectionChangeRecord changeRecord, List diffs) { 968 | List items = _getList((fn) => changeRecord.forEachPreviousItem(fn)); 969 | return _compareLists("previous", previous, items, diffs); 970 | } 971 | 972 | bool checkAdditions(CollectionChangeRecord changeRecord, List diffs) { 973 | List items = _getList((fn) => changeRecord.forEachAddition(fn)); 974 | return _compareLists("additions", additions, items, diffs); 975 | } 976 | 977 | bool checkMoves(CollectionChangeRecord changeRecord, List diffs) { 978 | List items = _getList((fn) => changeRecord.forEachMove(fn)); 979 | return _compareLists("moves", moves, items, diffs); 980 | } 981 | 982 | bool checkRemovals(CollectionChangeRecord changeRecord, List diffs) { 983 | List items = _getList((fn) => changeRecord.forEachRemoval(fn)); 984 | return _compareLists("removes", removals, items, diffs); 985 | } 986 | } 987 | 988 | class MapRecordMatcher extends _CollectionMatcher { 989 | final List map; 990 | final List previous; 991 | final List additions; 992 | final List changes; 993 | final List removals; 994 | 995 | MapRecordMatcher(this.map, this.previous, this.additions, this.changes, this.removals); 996 | 997 | Description describeMismatch(changes, Description mismatchDescription, 998 | Map matchState, bool verbose) { 999 | List diffs = matchState['diffs']; 1000 | if (diffs == null) return mismatchDescription; 1001 | mismatchDescription.add(diffs.join('\n')); 1002 | return mismatchDescription; 1003 | } 1004 | 1005 | Description describe(Description description) { 1006 | add(name, map) { 1007 | if (map != null) { 1008 | description.add('$name: ${map.join(', ')}\n '); 1009 | } 1010 | } 1011 | 1012 | add('map', map); 1013 | add('previous', previous); 1014 | add('additions', additions); 1015 | add('changes', changes); 1016 | add('removals', removals); 1017 | return description; 1018 | } 1019 | 1020 | bool matches(MapChangeRecord changeRecord, Map matchState) { 1021 | var diffs = matchState['diffs'] = []; 1022 | return checkMap(changeRecord, diffs) && 1023 | checkPrevious(changeRecord, diffs) && 1024 | checkAdditions(changeRecord, diffs) && 1025 | checkChanges(changeRecord, diffs) && 1026 | checkRemovals(changeRecord, diffs); 1027 | } 1028 | 1029 | bool checkMap(MapChangeRecord changeRecord, List diffs) { 1030 | List items = _getList((fn) => changeRecord.forEachItem(fn)); 1031 | bool equals = _compareLists("map", map, items, diffs); 1032 | int mapLength = changeRecord.map.length; 1033 | if (mapLength != items.length) { 1034 | diffs.add('map length mismatched: $mapLength != ${items.length}'); 1035 | equals = false; 1036 | } 1037 | return equals; 1038 | } 1039 | 1040 | bool checkPrevious(MapChangeRecord changeRecord, List diffs) { 1041 | List items = _getList((fn) => changeRecord.forEachPreviousItem(fn)); 1042 | return _compareLists("previous", previous, items, diffs); 1043 | } 1044 | 1045 | bool checkAdditions(MapChangeRecord changeRecord, List diffs) { 1046 | List items = _getList((fn) => changeRecord.forEachAddition(fn)); 1047 | return _compareLists("additions", additions, items, diffs); 1048 | } 1049 | 1050 | bool checkChanges(MapChangeRecord changeRecord, List diffs) { 1051 | List items = _getList((fn) => changeRecord.forEachChange(fn)); 1052 | return _compareLists("changes", changes, items, diffs); 1053 | } 1054 | 1055 | bool checkRemovals(MapChangeRecord changeRecord, List diffs) { 1056 | List items = _getList((fn) => changeRecord.forEachRemoval(fn)); 1057 | return _compareLists("removals", removals, items, diffs); 1058 | } 1059 | } 1060 | 1061 | class FooBar { 1062 | static int fooIds = 0; 1063 | 1064 | int id; 1065 | String foo, bar; 1066 | 1067 | FooBar(this.foo, this.bar) { 1068 | id = fooIds++; 1069 | } 1070 | 1071 | bool operator==(other) => 1072 | other is FooBar && foo == other.foo && bar == other.bar; 1073 | 1074 | int get hashCode => foo.hashCode ^ bar.hashCode; 1075 | 1076 | String toString() => '($id)$foo-$bar'; 1077 | } 1078 | --------------------------------------------------------------------------------