Skip to content
This repository was archived by the owner on Feb 22, 2018. It is now read-only.

Commit 08cec54

Browse files
vicbchirayuk
authored andcommitted
fix(change_detector): fix NaN move detection in collections
Closes #1136 Closes #1149
1 parent d7bd9ba commit 08cec54

File tree

2 files changed

+64
-44
lines changed

2 files changed

+64
-44
lines changed

lib/change_detection/dirty_checking_change_detector.dart

+49-44
Original file line numberDiff line numberDiff line change
@@ -514,19 +514,10 @@ class DirtyCheckingRecord<H> implements Record<H>, WatchRecord<H> {
514514
}
515515

516516
var last = currentValue;
517-
if (!identical(last, current)) {
518-
if (last is String && current is String &&
519-
last == current) {
520-
// This is false change in strings we need to recover, and pretend it
521-
// is the same. We save the value so that next time identity will pass
522-
currentValue = current;
523-
} else if (last is num && last.isNaN && current is num && current.isNaN) {
524-
// we need this for the compiled JavaScript since in JS NaN !== NaN.
525-
} else {
526-
previousValue = last;
527-
currentValue = current;
528-
return true;
529-
}
517+
if (!_looseIdentical(last, current)) {
518+
previousValue = currentValue;
519+
currentValue = current;
520+
return true;
530521
}
531522
return false;
532523
}
@@ -620,14 +611,10 @@ class _MapChangeRecord<K, V> implements MapChangeRecord<K, V> {
620611
var newSeqRecord;
621612
if (oldSeqRecord != null && key == oldSeqRecord.key) {
622613
newSeqRecord = oldSeqRecord;
623-
if (!identical(value, oldSeqRecord._currentValue)) {
614+
if (!_looseIdentical(value, oldSeqRecord._currentValue)) {
624615
var prev = oldSeqRecord._previousValue = oldSeqRecord._currentValue;
625616
oldSeqRecord._currentValue = value;
626-
if (!((value is String && prev is String && value == prev) ||
627-
(value is num && value.isNaN && prev is num && prev.isNaN))) {
628-
// Check string by value rather than reference
629-
_addToChanges(oldSeqRecord);
630-
}
617+
_addToChanges(oldSeqRecord);
631618
}
632619
} else {
633620
seqChanged = true;
@@ -955,7 +942,7 @@ class _CollectionChangeRecord<V> implements CollectionChangeRecord<V> {
955942
_length = list.length;
956943
for (int index = 0; index < _length; index++) {
957944
var item = list[index];
958-
if (record == null || !identical(item, record.item)) {
945+
if (record == null || !_looseIdentical(record.item, item)) {
959946
record = mismatch(record, item, index);
960947
maybeDirty = true;
961948
} else if (maybeDirty) {
@@ -967,7 +954,7 @@ class _CollectionChangeRecord<V> implements CollectionChangeRecord<V> {
967954
} else {
968955
int index = 0;
969956
for (var item in collection) {
970-
if (record == null || !identical(item, record.item)) {
957+
if (record == null || !_looseIdentical(record.item, item)) {
971958
record = mismatch(record, item, index);
972959
maybeDirty = true;
973960
} else if (maybeDirty) {
@@ -1037,19 +1024,6 @@ class _CollectionChangeRecord<V> implements CollectionChangeRecord<V> {
10371024
* - [index] is the position of the item in the collection
10381025
*/
10391026
ItemRecord<V> mismatch(ItemRecord<V> record, item, int index) {
1040-
if (record != null) {
1041-
if (item is String && record.item is String && record.item == item) {
1042-
// this is false change in strings we need to recover, and pretend it is
1043-
// the same. We save the value so that next time identity can pass
1044-
return record..item = item;
1045-
}
1046-
1047-
if (item is num && (item as num).isNaN && record.item is num && (record.item as num).isNaN) {
1048-
// we need this for JavaScript since in JS NaN !== NaN.
1049-
return record;
1050-
}
1051-
}
1052-
10531027
// The previous record after which we will append the current one.
10541028
ItemRecord<V> previousRecord;
10551029

@@ -1340,7 +1314,8 @@ class _DuplicateItemRecordList {
13401314
record._prevDup = null;
13411315
} else {
13421316
// adding a duplicate [ItemRecord] to the list
1343-
assert(record.item == _head.item);
1317+
assert(record.item == _head.item ||
1318+
record.item is num && record.item.isNaN && _head.item is num && _head.item.isNaN);
13441319
if (insertBefore == null) {
13451320
_tail._nextDup = record;
13461321
record._prevDup = _tail;
@@ -1367,10 +1342,10 @@ class _DuplicateItemRecordList {
13671342
ItemRecord record;
13681343
for (record = _head; record != null; record = record._nextDup) {
13691344
if ((afterIndex == null || afterIndex < record.currentIndex) &&
1370-
identical(record.item, item)) {
1371-
return record;
1345+
_looseIdentical(record.item, item)) {
1346+
return record;
1347+
}
13721348
}
1373-
}
13741349
return null;
13751350
}
13761351

@@ -1411,10 +1386,11 @@ class _DuplicateItemRecordList {
14111386
* The list of duplicates is implemented by [_DuplicateItemRecordList].
14121387
*/
14131388
class DuplicateMap {
1389+
static final nanKey = const Object();
14141390
final map = new HashMap<dynamic, _DuplicateItemRecordList>();
14151391

14161392
void put(ItemRecord record, [ItemRecord insertBefore = null]) {
1417-
var key = record.item;
1393+
var key = _getKey(record.item);
14181394
_DuplicateItemRecordList duplicates = map[key];
14191395
if (duplicates == null) {
14201396
duplicates = map[key] = new _DuplicateItemRecordList();
@@ -1429,9 +1405,10 @@ class DuplicateMap {
14291405
* Use case: `[a, b, c, a, a]` if we are at index `3` which is the second `a` then asking if we
14301406
* have any more `a`s needs to return the last `a` not the first or second.
14311407
*/
1432-
ItemRecord get(key, [int afterIndex]) {
1408+
ItemRecord get(value, [int afterIndex]) {
1409+
var key = _getKey(value);
14331410
_DuplicateItemRecordList recordList = map[key];
1434-
return recordList == null ? null : recordList.get(key, afterIndex);
1411+
return recordList == null ? null : recordList.get(value, afterIndex);
14351412
}
14361413

14371414
/**
@@ -1440,10 +1417,11 @@ class DuplicateMap {
14401417
* The list of duplicates also is removed from the map if it gets empty.
14411418
*/
14421419
ItemRecord remove(ItemRecord record) {
1443-
assert(map.containsKey(record.item));
1444-
_DuplicateItemRecordList recordList = map[record.item];
1420+
var key = _getKey(record.item);
1421+
assert(map.containsKey(key));
1422+
_DuplicateItemRecordList recordList = map[key];
14451423
// Remove the list of duplicates when it gets empty
1446-
if (recordList.remove(record)) map.remove(record.item);
1424+
if (recordList.remove(record)) map.remove(key);
14471425
return record;
14481426
}
14491427

@@ -1453,5 +1431,32 @@ class DuplicateMap {
14531431
map.clear();
14541432
}
14551433

1434+
/// Required to handle num.NAN as a Map value
1435+
dynamic _getKey(value) => value is num && value.isNaN ? nanKey : value;
1436+
14561437
String toString() => "DuplicateMap($map)";
14571438
}
1439+
1440+
/**
1441+
* Returns whether the [dst] and [src] are loosely identical:
1442+
* * true when the value are identical,
1443+
* * true when both values are equal strings,
1444+
* * true when both values are NaN
1445+
*
1446+
* If both values are equal string, src is assigned to dst.
1447+
*/
1448+
bool _looseIdentical(dst, src) {
1449+
if (identical(dst, src)) return true;
1450+
1451+
if (dst is String && src is String && dst == src) {
1452+
// this is false change in strings we need to recover, and pretend it is the same. We save the
1453+
// value so that next time identity can pass
1454+
dst = src;
1455+
return true;
1456+
}
1457+
1458+
// we need this for JavaScript since in JS NaN !== NaN.
1459+
if (dst is num && (dst as num).isNaN && src is num && (src as num).isNaN) return true;
1460+
1461+
return false;
1462+
}

test/change_detection/dirty_checking_change_detector_spec.dart

+15
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,21 @@ void testWithGetterFactory(FieldGetterFactory getterFactory) {
390390
expect(detector.collectChanges().moveNext()).toEqual(false);
391391
});
392392

393+
it('should detect [NaN] moves', () {
394+
var list = [double.NAN, double.NAN];
395+
detector..watch(list, null, null)..collectChanges();
396+
397+
list..clear()..addAll(['foo', double.NAN, double.NAN]);
398+
var iterator = detector.collectChanges();
399+
expect(iterator.moveNext()).toEqual(true);
400+
expect(iterator.current.currentValue, toEqualCollectionRecord(
401+
collection: ['foo[null -> 0]', 'NaN[0 -> 1]', 'NaN[1 -> 2]'],
402+
previous: ['NaN[0 -> 1]', 'NaN[1 -> 2]'],
403+
additions: ['foo[null -> 0]'],
404+
moves: ['NaN[0 -> 1]', 'NaN[1 -> 2]'],
405+
removals: []));
406+
});
407+
393408
it('should remove and add same item', () {
394409
var list = ['a', 'b', 'c'];
395410
var record = detector.watch(list, null, 'handler');

0 commit comments

Comments
 (0)