diff --git a/lib/src/model_utils.dart b/lib/src/model_utils.dart index db2e211bf2..5dbcfbf622 100644 --- a/lib/src/model_utils.dart +++ b/lib/src/model_utils.dart @@ -12,6 +12,7 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/src/generated/engine.dart'; import 'package:analyzer/src/generated/sdk.dart'; import 'package:analyzer/src/generated/source_io.dart'; +import 'package:collection/collection.dart'; import 'package:dartdoc/src/model.dart'; import 'package:quiver_hashcode/hashcode.dart'; @@ -191,8 +192,40 @@ class _HashableList extends UnmodifiableListView { get hashCode => hashObjects(this); } -/// Extend or use as a mixin to track object-specific cached values, or -/// instantiate directly to track other values. +/// Like [Memoizer], except in checked mode will validate that the value of the +/// memoized function is unchanging using [DeepCollectionEquality]. Still +/// returns the cached value assuming the assertion passes. +class ValidatingMemoizer extends Memoizer { + bool _assert_on_difference = false; + + ValidatingMemoizer() : super() { + // Assignment within assert to take advantage of the expression only + // being executed in checked mode. + assert(_assert_on_difference = true); + invalidateMemos(); + } + + /// In checked mode and when constructed with assert_on_difference == true, + /// validate that the return value from f() equals the memoized value. + /// Otherwise, a wrapper around putIfAbsent. + @override + R _cacheIfAbsent(_HashableList key, R Function() f) { + if (_assert_on_difference) { + if (_memoizationTable.containsKey(key)) { + R value = f(); + if (!new DeepCollectionEquality() + .equals(value, _memoizationTable[key])) { + throw new AssertionError('${value} != $_memoizationTable[key]'); + } + } + } + return super._cacheIfAbsent(key, f); + } +} + +/// A basic Memoizer class. Instantiate as a member variable, extend, or use +/// as a mixin to track object-specific cached values, or instantiate directly +/// to track other values. /// /// For all methods in this class, the parameter [f] must be a tear-off method /// or top level function (not an inline closure) for memoization to work. @@ -219,36 +252,37 @@ class _HashableList extends UnmodifiableListView { /// ``` class Memoizer { /// Map of a function and its positional parameters (if any), to a value. - Map<_HashableList, dynamic> _memoizationTable; - - Memoizer() { - invalidateMemos(); - } + Map<_HashableList, dynamic> _memoizationTable = new Map(); /// Reset the memoization table, forcing calls of the underlying functions. void invalidateMemos() { _memoizationTable = new Map(); } + /// A wrapper around putIfAbsent, exposed to allow overrides. + R _cacheIfAbsent(_HashableList key, R Function() f) { + return _memoizationTable.putIfAbsent(key, f); + } + /// Calls and caches the return value of [f]() if not in the cache, then /// returns the cached value of [f](). R memoized(Function f) { _HashableList key = new _HashableList([f]); - return _memoizationTable.putIfAbsent(key, f); + return _cacheIfAbsent(key, f); } /// Calls and caches the return value of [f]([param1]) if not in the cache, then /// returns the cached value of [f]([param1]). R memoized1(R Function(A) f, A param1) { _HashableList key = new _HashableList([f, param1]); - return _memoizationTable.putIfAbsent(key, () => f(param1)); + return _cacheIfAbsent(key, () => f(param1)); } /// Calls and caches the return value of [f]([param1], [param2]) if not in the /// cache, then returns the cached value of [f]([param1], [param2]). R memoized2(R Function(A, B) f, A param1, B param2) { _HashableList key = new _HashableList([f, param1, param2]); - return _memoizationTable.putIfAbsent(key, () => f(param1, param2)); + return _cacheIfAbsent(key, () => f(param1, param2)); } /// Calls and caches the return value of [f]([param1], [param2], [param3]) if @@ -256,7 +290,7 @@ class Memoizer { /// [param2], [param3]). R memoized3(R Function(A, B, C) f, A param1, B param2, C param3) { _HashableList key = new _HashableList([f, param1, param2, param3]); - return _memoizationTable.putIfAbsent(key, () => f(param1, param2, param3)); + return _cacheIfAbsent(key, () => f(param1, param2, param3)); } /// Calls and caches the return value of [f]([param1], [param2], [param3], @@ -265,8 +299,7 @@ class Memoizer { R memoized4( R Function(A, B, C, D) f, A param1, B param2, C param3, D param4) { _HashableList key = new _HashableList([f, param1, param2, param3, param4]); - return _memoizationTable.putIfAbsent( - key, () => f(param1, param2, param3, param4)); + return _cacheIfAbsent(key, () => f(param1, param2, param3, param4)); } /// Calls and caches the return value of [f]([param1], [param2], [param3], @@ -276,8 +309,7 @@ class Memoizer { C param3, D param4, E param5) { _HashableList key = new _HashableList([f, param1, param2, param3, param4, param5]); - return _memoizationTable.putIfAbsent( - key, () => f(param1, param2, param3, param4, param5)); + return _cacheIfAbsent(key, () => f(param1, param2, param3, param4, param5)); } /// Calls and caches the return value of [f]([param1], [param2], [param3], @@ -287,7 +319,7 @@ class Memoizer { B param2, C param3, D param4, E param5, F param6) { _HashableList key = new _HashableList([f, param1, param2, param3, param4, param5, param6]); - return _memoizationTable.putIfAbsent( + return _cacheIfAbsent( key, () => f(param1, param2, param3, param4, param5, param6)); } @@ -299,7 +331,7 @@ class Memoizer { A param1, B param2, C param3, D param4, E param5, F param6, G param7) { _HashableList key = new _HashableList( [f, param1, param2, param3, param4, param5, param6, param7]); - return _memoizationTable.putIfAbsent( + return _cacheIfAbsent( key, () => f(param1, param2, param3, param4, param5, param6, param7)); } @@ -319,7 +351,7 @@ class Memoizer { H param8) { _HashableList key = new _HashableList( [f, param1, param2, param3, param4, param5, param6, param7, param8]); - return _memoizationTable.putIfAbsent( + return _cacheIfAbsent( key, () => f(param1, param2, param3, param4, param5, param6, param7, param8)); diff --git a/pubspec.lock b/pubspec.lock index cb4b01c9c9..7cb597eee7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -408,4 +408,4 @@ packages: source: hosted version: "2.1.13" sdks: - dart: ">=1.23.0 <=2.0.0-edge.77d3b702203a852723dd2c966d4c0889986a4703" + dart: ">=1.23.0 <=2.0.0-dev.15.0" diff --git a/test/model_utils_test.dart b/test/model_utils_test.dart index 9fe5c42a51..3503f48529 100644 --- a/test/model_utils_test.dart +++ b/test/model_utils_test.dart @@ -7,6 +7,17 @@ library dartdoc.model_utils_test; import 'package:dartdoc/src/model_utils.dart'; import 'package:test/test.dart'; +class ValidatingMemoizerUser extends ValidatingMemoizer { + int foo = 0; + // These are actually not things you would ordinarily memoize, because + // they change. But they are useful for testing. + List _toMemoize() { + return [foo++]; + } + + List get toMemoize => memoized(_toMemoize); +} + class MemoizerUser extends Memoizer { int foo = 0; // These are actually not things you would ordinarily memoize, because @@ -130,7 +141,15 @@ void main() { }); }); - group('model_utils MethodMemoizer', () { + group('model_utils ValidatingMemoizer', () { + test('assert on changing underlying function', () { + var m = new ValidatingMemoizerUser(); + expect(m.toMemoize.first, equals(0)); + expect(() => m.toMemoize, throwsA(new isInstanceOf())); + }); + }); + + group('model_utils Memoizer', () { test('basic memoization and invalidation', () { var m = new MemoizerUser(); expect(m.toMemoize, equals(0), reason: "initialization problem");