Skip to content

Commit c4e6167

Browse files
committed
feat: Removing implicit map dereference in expressions
Closes dart-archive#915 Before this PR a.b would have match either a.b or a['b']. The reason is that the context was a Map (before dart-archive#914) and we don't want to write ctrl['foo'] but ctrl.foo. It was also not possible to access Map properties, ie map.length would have returned map['length']. Now "a.b" return a.b and "a['b']" return a['b']
1 parent f836985 commit c4e6167

17 files changed

+402
-382
lines changed

lib/change_detection/dirty_checking_change_detector.dart

+9-14
Original file line numberDiff line numberDiff line change
@@ -460,22 +460,17 @@ class DirtyCheckingRecord<H> implements WatchRecord<H> {
460460
return;
461461
}
462462

463-
if (object is Map) {
464-
_mode = _MODE_MAP_FIELD_;
465-
_getter = null;
466-
} else {
467-
while (object is ContextLocals) {
468-
var ctx = object as ContextLocals;
469-
if (ctx.hasProperty(field)) {
470-
_mode = _MODE_MAP_FIELD_;
471-
_getter = null;
472-
return;
473-
}
474-
object = ctx.parentContext;
463+
while (object is ContextLocals) {
464+
var ctx = object as ContextLocals;
465+
if (ctx.hasProperty(field)) {
466+
_mode = _MODE_MAP_FIELD_;
467+
_getter = null;
468+
return;
475469
}
476-
_mode = _MODE_GETTER_OR_METHOD_CLOSURE_;
477-
_getter = _fieldGetterFactory.getter(object, field);
470+
object = ctx.parentContext;
478471
}
472+
_mode = _MODE_GETTER_OR_METHOD_CLOSURE_;
473+
_getter = _fieldGetterFactory.getter(object, field);
479474
}
480475

481476
bool check() {

lib/core/parser/eval_access.dart

-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ abstract class AccessFast {
4747

4848
dynamic _eval(holder) {
4949
if (holder == null) return null;
50-
if (holder is Map) return holder[name];
5150
return getter(holder);
5251
}
5352

@@ -56,7 +55,6 @@ abstract class AccessFast {
5655
_assignToNonExisting(scope, value);
5756
return value;
5857
} else {
59-
if (holder is Map) return holder[name] = value;
6058
return setter(holder, value);
6159
}
6260
}

lib/core/parser/parser_dynamic.dart

+7-26
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,14 @@ class DynamicClosureMap implements ClosureMap {
88
final Map<String, Symbol> symbols = {};
99
Getter lookupGetter(String name) {
1010
var symbol = new Symbol(name);
11-
return (o) {
12-
if (o is Map) {
13-
return o[name];
14-
} else {
15-
return reflect(o).getField(symbol).reflectee;
16-
}
17-
};
11+
return (o) => reflect(o).getField(symbol).reflectee;
1812
}
1913

2014
Setter lookupSetter(String name) {
2115
var symbol = new Symbol(name);
2216
return (o, value) {
23-
if (o is Map) {
24-
return o[name] = value;
25-
} else {
26-
reflect(o).setField(symbol, value);
27-
return value;
28-
}
17+
reflect(o).setField(symbol, value);
18+
return value;
2919
};
3020
}
3121

@@ -37,19 +27,10 @@ class DynamicClosureMap implements ClosureMap {
3727
var symbol = symbols.putIfAbsent(name, () => new Symbol(name));
3828
sNamedArgs[symbol] = value;
3929
});
40-
if (o is Map) {
41-
var fn = o[name];
42-
if (fn is Function) {
43-
return Function.apply(fn, posArgs, sNamedArgs);
44-
} else {
45-
throw "Property '$name' is not of type function.";
46-
}
47-
} else {
48-
try {
49-
return reflect(o).invoke(symbol, posArgs, sNamedArgs).reflectee;
50-
} on NoSuchMethodError catch (e) {
51-
throw 'Undefined function $name';
52-
}
30+
try {
31+
return reflect(o).invoke(symbol, posArgs, sNamedArgs).reflectee;
32+
} on NoSuchMethodError catch (e) {
33+
throw 'Undefined function $name';
5334
}
5435
};
5536
}

lib/core/parser/parser_static.dart

+1-10
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,7 @@ class StaticClosureMap extends ClosureMap {
2727
return (o, posArgs, namedArgs) {
2828
var sNamedArgs = {};
2929
namedArgs.forEach((name, value) => sNamedArgs[symbols[name]] = value);
30-
if (o is Map) {
31-
var fn = o[name];
32-
if (fn is Function) {
33-
return Function.apply(fn, posArgs, sNamedArgs);
34-
} else {
35-
throw "Property '$name' is not of type function.";
36-
}
37-
} else {
38-
return Function.apply(fn(o), posArgs, sNamedArgs);
39-
}
30+
return Function.apply(fn(o), posArgs, sNamedArgs);
4031
};
4132
}
4233

lib/formatter/filter.dart

+51-58
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
part of angular.formatter_internal;
22

3-
// Too bad you can't stick typedef's inside a class.
4-
typedef bool _Predicate(e);
5-
typedef bool _Equals(a, b);
3+
typedef bool _PredicateFn(e);
4+
typedef bool _CompareFn(a, b);
65

76
/**
87
* Selects a subset of items from the provided [List] and returns it as a new
@@ -17,37 +16,37 @@ typedef bool _Equals(a, b);
1716
*
1817
* The expression can be of the following types:
1918
*
20-
* - [String], [bool] and [num] Only items in the list that directly
19+
* - [String], [bool] and [num]: Only items in the list that directly
2120
* match this expression, items that are Maps with any value matching this
2221
* item, and items that are lists containing a matching items are returned.
2322
*
24-
* - [Map]:  This defines a pattern map.  Filters specific properties on objects
23+
* - [Map]:  This defines a pattern map. Filters specific properties on objects
2524
* contained in the input list.  For example `{name:"M", phone:"1"}` predicate
2625
* will return a list of items which have property `name` containing "M" and
2726
* property `phone` containing "1".  A special property name, `$`, can be used
2827
* (as in `{$: "text"}`) to accept a match against any property of the object.
2928
* That's equivalent to the simple substring match with a `String` as
3029
* described above.
3130
*
32-
* - [Function] This allows you to supply a custom function to filter the
33-
* List.  The function is called for each element of the List.  The returned
31+
* - [Function]: This allows you to supply a custom function to filter the
32+
* List.  The function is called for each element of the List. The returned
3433
* List contains exactly those elements for which this function returned
3534
* `true`.
3635
*
3736
*
3837
* The comparator is optional and can be one of the following:
3938
*
40-
* - `bool comparator(expected, actual)` The function will be called with the
39+
* - `bool comparator(expected, actual)`: The function will be called with the
4140
* object value and the predicate value to compare and should return true if
4241
* the item should be included in filtered result.
4342
*
4443
* - `true`:  Specifies that only identical objects matching the expression
45-
* exactly should be considered matches.  Two strings are considered identical
46-
* if they are equal.  Two numbers are considered identical if they are either
47-
* equal or both are `NaN` All other objects are identical iff
44+
* exactly should be considered matches. Two strings are considered identical
45+
* if they are equal. Two numbers are considered identical if they are either
46+
* equal or both are `NaN`. All other objects are identical iff
4847
* identical(expected, actual) is true.
4948
*
50-
* - `false|null`:  Specifies case insensitive substring matching.
49+
* - `false|null`: Specifies case insensitive substring matching.
5150
*
5251
*
5352
* # Example ([view in plunker](http://plnkr.co/edit/6Mxz6r?p=info)):
@@ -105,27 +104,30 @@ typedef bool _Equals(a, b);
105104
@Formatter(name: 'filter')
106105
class Filter implements Function {
107106
Parser _parser;
108-
_Equals _comparator;
109-
_Equals _stringComparator;
107+
_CompareFn _comparator;
108+
_CompareFn _stringComparator;
110109

111110
static _nop(e) => e;
111+
112112
static _ensureBool(val) => (val is bool && val);
113+
113114
static _isSubstringCaseInsensitive(String a, String b) =>
114115
a != null && b != null && a.toLowerCase().contains(b.toLowerCase());
116+
115117
static _identical(a, b) => identical(a, b) ||
116118
(a is String && b is String && a == b) ||
117119
(a is num && b is num && a.isNaN && b.isNaN);
118120

119121
Filter(this._parser);
120122

121-
void _configureComparator(var comparatorExpression) {
123+
void _configureComparator(comparatorExpression) {
122124
if (comparatorExpression == null || comparatorExpression == false) {
123125
_stringComparator = _isSubstringCaseInsensitive;
124126
_comparator = _defaultComparator;
125127
} else if (comparatorExpression == true) {
126128
_stringComparator = _identical;
127129
_comparator = _defaultComparator;
128-
} else if (comparatorExpression is _Equals) {
130+
} else if (comparatorExpression is _CompareFn) {
129131
_comparator = (a, b) => _ensureBool(comparatorExpression(a, b));
130132
} else {
131133
_comparator = null;
@@ -135,67 +137,58 @@ class Filter implements Function {
135137
// Preconditions
136138
// - what: NOT a Map
137139
// - item: neither a Map nor a List
138-
bool _defaultComparator(var item, var what) {
139-
if (what == null) {
140-
return false;
141-
} else if (item == null) {
142-
return what == '';
143-
} else if (what is String && what.startsWith('!')) {
144-
return !_search(item, what.substring(1));
145-
} else if (item is String) {
146-
return (what is String) && _stringComparator(item, what);
147-
} else if (item is bool) {
140+
bool _defaultComparator(item, what) {
141+
if (what == null) return false;
142+
if (item == null) return what == '';
143+
if (what is String && what.startsWith('!')) return !_search(item, what.substring(1));
144+
if (item is String) return (what is String) && _stringComparator(item, what);
145+
if (item is bool) {
148146
if (what is bool) {
149147
return item == what;
150148
} else if (what is String) {
151149
what = (what as String).toLowerCase();
152150
return item ? (what == "true" || what == "yes" || what == "on")
153151
: (what == "false" || what == "no" || what == "off");
154-
} else {
155-
return false;
156152
}
157-
} else if (item is num) {
158-
if (what is num) {
159-
return item == what || (item.isNaN && what.isNaN);
160-
} else {
161-
return what is String && _stringComparator('$item', what);
162-
}
163-
} else {
164-
return false; // Unsupported item type.
153+
return false;
165154
}
155+
if (item is num) {
156+
return what is num ?
157+
item == what || (item.isNaN && what.isNaN) :
158+
what is String && _stringComparator('$item', what);
159+
}
160+
return false; // Unsupported item type.
166161
}
167162

168-
bool _search(var item, var what) {
163+
bool _search(item, what) {
169164
if (what is Map) {
170165
return what.keys.every((key) => _search(
171166
(key == r'$') ? item : _parser(key).eval(item), what[key]));
172-
} else if (item is Map) {
173-
return item.keys.any((k) => !k.startsWith(r'$') && _search(item[k], what));
174-
} else if (item is List) {
175-
return item.any((i) => _search(i, what));
176-
} else {
177-
return _comparator(item, what);
178167
}
168+
if (item is Map) return item.keys.any((k) => !k.startsWith(r'$') && _search(item[k], what));
169+
if (item is List) return item.any((i) => _search(i, what));
170+
return _comparator(item, what);
179171
}
180172

181-
_Predicate _toPredicate(var expression) {
182-
if (expression is _Predicate) {
183-
return (item) => _ensureBool(expression(item));
184-
} else if (_comparator == null) {
185-
return (item) => false; // Bad comparator → no items for you!
186-
} else {
187-
return (item) => _search(item, expression);
188-
}
173+
_PredicateFn _toPredicate(var expression) {
174+
if (expression is _PredicateFn) return (item) => _ensureBool(expression(item));
175+
if (_comparator == null) return (_) => false; // Bad comparator → no items for you!
176+
return (item) => _search(item, expression);
189177
}
190178

191-
List call(List items, var expression, [var comparator]) {
192-
if (expression == null) {
193-
return items.toList(growable: false); // Missing expression → passthrough.
194-
} else if (expression is! Map && expression is! Function &&
195-
expression is! String && expression is! bool &&
196-
expression is! num) {
197-
return const []; // Bad expression → no items for you!
179+
List call(List items, expression, [comparator]) {
180+
// Missing expression → passthrough.
181+
if (expression == null) return items.toList(growable: false);
182+
183+
// Bad expression → no items for you!
184+
if (expression is! Map &&
185+
expression is! Function &&
186+
expression is! String &&
187+
expression is! bool &&
188+
expression is! num) {
189+
return const [];
198190
}
191+
199192
_configureComparator(comparator);
200193
List results = items.where(_toPredicate(expression)).toList(growable: false);
201194
_comparator = null;

lib/mock/module.dart

+52-4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import 'dart:async' as dart_async;
1717
import 'dart:collection' show ListBase;
1818
import 'dart:html';
1919
import 'dart:js' as js;
20+
import 'dart:mirrors' as mirrors;
2021

2122
import 'package:angular/angular.dart';
2223
import 'package:angular/core/module_internal.dart';
@@ -60,10 +61,57 @@ class AngularMockModule extends Module {
6061
bind(Element, toValue: document.body);
6162
bind(Node, toValue: document.body);
6263
bind(HttpBackend, toFactory: (Injector i) => i.get(MockHttpBackend));
63-
bind(VmTurnZone, toFactory: (_) {
64-
return new VmTurnZone()
65-
..onError = (e, s, LongStackTrace ls) => dump('EXCEPTION: $e\n$s\n$ls');
66-
});
64+
bind(VmTurnZone, toFactory: (_) =>
65+
new VmTurnZone()..onError = (e, s, LongStackTrace ls) => dump('EXCEPTION: $e\n$s\n$ls'));
6766
bind(Window, toImplementation: MockWindow);
67+
bind(Object, toImplementation: DynamicObject);
68+
}
69+
}
70+
71+
/**
72+
* [DynamicObject] helps testing angular.dart.
73+
*
74+
* Before GH-915, the `foo.bar` expression would evaluate either `foo.bar` or `foo["bar"]`. A lot
75+
* of unit tests are written using `scope.context["foo"]=bar`. To test this after GH-915, you should
76+
* create a context class with a `foo` property and initialize it with `scope.context.foo = bar`.
77+
*
78+
* Setting the test context to an instance of [DynamicObject] avoid having to write a specific class
79+
* for every new test by allowing the dynamic addition of properties through the use of
80+
* [Object.noSuchMethod]
81+
*
82+
*/
83+
class DynamicObject {
84+
Map _locals = {};
85+
86+
void addProperties(Map<String, dynamic> locals) {
87+
assert(locals != null);
88+
locals.forEach((k, v) => this[k] = v);
89+
}
90+
91+
// todo(vicb) - It would be great to remove this operator overloading when all tests are
92+
// converted from `scope['foo'] = bar` to `scope.foo = bar`
93+
void operator []=(k, v) {
94+
_locals[k] = v;
95+
}
96+
97+
// todo(vicb) - It would be great to remove this operator overloading when all tests are
98+
// converted from `scope['foo']` to `scope.foo`
99+
dynamic operator[](k) => _locals[k];
100+
101+
noSuchMethod(Invocation invocation) {
102+
var pArgs = invocation.positionalArguments;
103+
var field = mirrors.MirrorSystem.getName(invocation.memberName);
104+
if (invocation.isGetter) {
105+
return _locals[field];
106+
}
107+
if (invocation.isSetter) {
108+
field = field.substring(0, field.length - 1);
109+
_locals[field] = pArgs[0];
110+
return;
111+
}
112+
if (invocation.isMethod) {
113+
return Function.apply(_locals[field], pArgs, invocation.namedArguments);
114+
}
115+
throw new UnimplementedError(field);
68116
}
69117
}

0 commit comments

Comments
 (0)