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

Commit 09c73eb

Browse files
vicbchirayuk
authored andcommitted
fix(change_detector): fix NaN move detection in collections
Closes #1136 Closes #1149
1 parent eed1c66 commit 09c73eb

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

@@ -1343,7 +1317,8 @@ class _DuplicateItemRecordList {
13431317
record._prevDup = null;
13441318
} else {
13451319
// adding a duplicate [ItemRecord] to the list
1346-
assert(record.item == _head.item);
1320+
assert(record.item == _head.item ||
1321+
record.item is num && record.item.isNaN && _head.item is num && _head.item.isNaN);
13471322
if (insertBefore == null) {
13481323
_tail._nextDup = record;
13491324
record._prevDup = _tail;
@@ -1370,10 +1345,10 @@ class _DuplicateItemRecordList {
13701345
ItemRecord record;
13711346
for (record = _head; record != null; record = record._nextDup) {
13721347
if ((afterIndex == null || afterIndex < record.currentIndex) &&
1373-
identical(record.item, item)) {
1374-
return record;
1348+
_looseIdentical(record.item, item)) {
1349+
return record;
1350+
}
13751351
}
1376-
}
13771352
return null;
13781353
}
13791354

@@ -1414,10 +1389,11 @@ class _DuplicateItemRecordList {
14141389
* The list of duplicates is implemented by [_DuplicateItemRecordList].
14151390
*/
14161391
class DuplicateMap {
1392+
static final nanKey = const Object();
14171393
final map = new HashMap<dynamic, _DuplicateItemRecordList>();
14181394

14191395
void put(ItemRecord record, [ItemRecord insertBefore = null]) {
1420-
var key = record.item;
1396+
var key = _getKey(record.item);
14211397
_DuplicateItemRecordList duplicates = map[key];
14221398
if (duplicates == null) {
14231399
duplicates = map[key] = new _DuplicateItemRecordList();
@@ -1432,9 +1408,10 @@ class DuplicateMap {
14321408
* Use case: `[a, b, c, a, a]` if we are at index `3` which is the second `a` then asking if we
14331409
* have any more `a`s needs to return the last `a` not the first or second.
14341410
*/
1435-
ItemRecord get(key, [int afterIndex]) {
1411+
ItemRecord get(value, [int afterIndex]) {
1412+
var key = _getKey(value);
14361413
_DuplicateItemRecordList recordList = map[key];
1437-
return recordList == null ? null : recordList.get(key, afterIndex);
1414+
return recordList == null ? null : recordList.get(value, afterIndex);
14381415
}
14391416

14401417
/**
@@ -1443,10 +1420,11 @@ class DuplicateMap {
14431420
* The list of duplicates also is removed from the map if it gets empty.
14441421
*/
14451422
ItemRecord remove(ItemRecord record) {
1446-
assert(map.containsKey(record.item));
1447-
_DuplicateItemRecordList recordList = map[record.item];
1423+
var key = _getKey(record.item);
1424+
assert(map.containsKey(key));
1425+
_DuplicateItemRecordList recordList = map[key];
14481426
// Remove the list of duplicates when it gets empty
1449-
if (recordList.remove(record)) map.remove(record.item);
1427+
if (recordList.remove(record)) map.remove(key);
14501428
return record;
14511429
}
14521430

@@ -1456,5 +1434,32 @@ class DuplicateMap {
14561434
map.clear();
14571435
}
14581436

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

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)