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

Commit

Permalink
fix(Dirty Checking): fix watching methods/closures
Browse files Browse the repository at this point in the history
-  BUG: DynamicFieldGetterFactory::isMethod did not handle methods
   defined in superclasses.
-  BUG: Upon detecting a method, the code assumed that you would only
   invoke it.  This broke application code that watched a method (e.g.
   by way of Component mapping) just to get the closurized value and
   store it somewhere to invoke later (test case and stack trace at end
   of this commit message.)
-  BUG: StaticFieldGetterFactory::method() and
   DynamicFieldGetterFactory::method() differed.  There was no
   difference between StaticFieldGetterFactory::method() and
   StaticFieldGetterFactory::getter(). 
   DynamicFieldGetterFactory::method(), as mentioned before, assumed
   that the only thing you could do with it was to invoke it (i.e. not a
   leaf watch.)
-  There was very little testing for StaticFieldGetterFactory.  This
   meant that, though it was out of sync with DynamicFieldGetterFactory,
   no tests were failing.

Changes in this commit:

- run the same tests against StaticFieldGetterFactory that are run
  against DynamicFieldGetterFactory
- do not call the result of GetterFactory.method()
  in "set object(value)"
- reduce the difference between the two different factories. 
  GetterFactory now only has one method in it's interface definition -
  the getter function.  `_MODE_METHOD_INVOKE_`, `isMethod`,
  `isMethodInvoke`, etc. are gone.

**Bug Details:**

Refer to the repro case at
chirayuk/angular.dart@issue_999^...issue_999

```dart
// Given this object.
class Foo {
  bar(x) => x+1;
}

// This test case (in an appropriate file like `scope_spec.dart`) fails
// with a traceback

it('should watch closures', (RootScope rootScope, Logger log) {
  rootScope.context['foo'] = new Foo();
  rootScope.context['func'] = null;
  rootScope.watch('foo.bar', (v, _) { rootScope.context['func'] = v; });
  rootScope.watch('func(1)', (v, o) => log([v, o]));
  rootScope.apply();
  expect(log).toEqual([[null, null], [2, null]]);
});
```

**Stack Trace:**

    Chrome 34.0.1847 (Mac OS X 10.9.2) scope watch/digest should watch closures FAILED
    Test failed: Caught Closure call with mismatched arguments: function 'DynamicFieldGetterFactory.method.<anonymous closure>'

    NoSuchMethodError: incorrect number of arguments passed to method named 'DynamicFieldGetterFactory.method.<anonymous closure>'
    Receiver: Closure: (List, Map) => dynamic
    Tried calling: DynamicFieldGetterFactory.method.<anonymous closure>(Instance of 'Foo')
    Found: DynamicFieldGetterFactory.method.<anonymous closure>(args, namedArgs)
    #0      Object.noSuchMethod (dart:core-patch/object_patch.dart:45)
    #1      DirtyCheckingRecord.object= (package:angular/change_detection/dirty_checking_change_detector.dart:465:78)
    #2      _FieldHandler.acceptValue (package:angular/change_detection/watch_group.dart:630:17)
    #3      WatchGroup.addFieldWatch (package:angular/change_detection/watch_group.dart:171:29)
    #4      FieldReadAST.setupWatch (package:angular/change_detection/ast.dart:67:31)
    #5      WatchGroup.watch.<anonymous closure> (package:angular/change_detection/watch_group.dart:144:40)
    #6      _HashMap.putIfAbsent (dart:collection-patch/collection_patch.dart:124)
    #7      WatchGroup.watch (package:angular/change_detection/watch_group.dart:143:27)
    #8      Scope.watch (package:angular/core/scope.dart:240:31)
    #9      main.<anonymous closure>.<anonymous closure>.<anonymous closure> (/Users/chirayu/work/angular.dart/test/core/scope_spec.dart:1308:24)
    #10     _LocalInstanceMirror._invoke (dart:mirrors-patch/mirrors_impl.dart:440)
    #11     _LocalInstanceMirror.invoke (dart:mirrors-patch/mirrors_impl.dart:436)
    #12     _LocalClosureMirror.apply (dart:mirrors-patch/mirrors_impl.dart:466)
    #13     DynamicInjector.invoke (package:di/dynamic_injector.dart:97:20)
    #14     _SpecInjector.inject (package:angular/mock/test_injection.dart:58:22)
    #15     inject.<anonymous closure> (package:angular/mock/test_injection.dart:100:44)
    #16     _rootRun (dart:async/zone.dart:723)
    #17     _ZoneDelegate.run (dart:async/zone.dart:453)
    #18     _CustomizedZone.run (dart:async/zone.dart:663)
    #19     runZoned (dart:async/zone.dart:954)
    #20     _syncOuter.<anonymous closure> (package:angular/mock/zone.dart:227:22)
    #21     _withSetup.<anonymous closure> (/Users/chirayu/work/angular.dart/test/jasmine_syntax.dart:13:14)
    #22     _run.<anonymous closure> (package:unittest/src/test_case.dart:102:27)
    #23     _rootRunUnary (dart:async/zone.dart:730)
    #24     _ZoneDelegate.runUnary (dart:async/zone.dart:462)
    #25     _CustomizedZone.runUnary (dart:async/zone.dart:667)
    #26     _Future._propagateToListeners.handleValueCallback (dart:async/future_impl.dart:488)
    #27     _Future._propagateToListeners (dart:async/future_impl.dart:571)
    #28     _Future._completeWithValue (dart:async/future_impl.dart:331)
    #29     _Future._asyncComplete.<anonymous closure> (dart:async/future_impl.dart:393)
    #30     _rootRun (dart:async/zone.dart:723)
    #31     _ZoneDelegate.run (dart:async/zone.dart:453)
    #32     _CustomizedZone.run (dart:async/zone.dart:663)
    #33     _BaseZone.runGuarded (dart:async/zone.dart:574)
    #34     _BaseZone.bindCallback.<anonymous closure> (dart:async/zone.dart:599)
    #35     _asyncRunCallbackLoop (dart:async/schedule_microtask.dart:23)
    #36     _asyncRunCallback (dart:async/schedule_microtask.dart:32)
    #37     _handleMutation (file:///Volumes/data/b/build/slave/dartium-mac-full-dev/build/src/dart/tools/dom/src/native_DOMImplementation.dart:588)

    DECLARED AT:#0      inject (package:angular/mock/test_injection.dart:97:5)
    #1      _injectify (/Users/chirayu/work/angular.dart/test/_specs.dart:236:25)
    #2      iit (/Users/chirayu/work/angular.dart/test/_specs.dart:244:53)
    #3      main.<anonymous closure>.<anonymous closure> (/Users/chirayu/work/angular.dart/test/core/scope_spec.dart:1305:10)
    #4      describe.<anonymous closure> (/Users/chirayu/work/angular.dart/test/jasmine_syntax.dart:62:9)
    #5      group (package:unittest/unittest.dart:396:9)
    #6      describe (/Users/chirayu/work/angular.dart/test/jasmine_syntax.dart:60:15)
    #7      describe (/Users/chirayu/work/angular.dart/test/_specs.dart:248:46)
    #8      main.<anonymous closure> (/Users/chirayu/work/angular.dart/test/core/scope_spec.dart:1009:13)
    #9      describe.<anonymous closure> (/Users/chirayu/work/angular.dart/test/jasmine_syntax.dart:62:9)
    #10     group (package:unittest/unittest.dart:396:9)
    #11     describe (/Users/chirayu/work/angular.dart/test/jasmine_syntax.dart:60:15)
    #12     describe (/Users/chirayu/work/angular.dart/test/_specs.dart:248:46)
    #13     main (/Users/chirayu/work/angular.dart/test/core/scope_spec.dart:14:11)
    #14     main.<anonymous closure> (http://localhost:8765/__adapter_dart_unittest.dart:169:15)
    #15     _rootRun (dart:async/zone.dart:723)
    #16     _ZoneDelegate.run (dart:async/zone.dart:453)
    #17     _CustomizedZone.run (dart:async/zone.dart:663)
    #18     runZoned (dart:async/zone.dart:954)
    #19     main (http://localhost:8765/__adapter_dart_unittest.dart:146:11)

    #0      _SpecInjector.inject (package:angular/mock/test_injection.dart:60:7)
    #1      inject.<anonymous closure> (package:angular/mock/test_injection.dart:100:44)
    #2      _rootRun (dart:async/zone.dart:723)
    #3      _rootRun (dart:async/zone.dart:724)
    #4      _rootRun (dart:async/zone.dart:724)
    #5      _ZoneDelegate.run (dart:async/zone.dart:453)
    #6      _CustomizedZone.run (dart:async/zone.dart:663)
    #7      runZoned (dart:async/zone.dart:954)
    #8      _syncOuter.<anonymous closure> (package:angular/mock/zone.dart:227:22)
    #9      _withSetup.<anonymous closure> (/Users/chirayu/work/angular.dart/test/jasmine_syntax.dart:13:14)
    #10     _withSetup.<anonymous closure> (/Users/chirayu/work/angular.dart/test/jasmine_syntax.dart:14:5)
    #11     _withSetup.<anonymous closure> (/Users/chirayu/work/angular.dart/test/jasmine_syntax.dart:14:5)
    #12     _run.<anonymous closure> (package:unittest/src/test_case.dart:102:27)
    #13     _rootRunUnary (dart:async/zone.dart:730)
    #14     _ZoneDelegate.runUnary (dart:async/zone.dart:462)
    #15     _CustomizedZone.runUnary (dart:async/zone.dart:667)
    #16     _Future._propagateToListeners.handleValueCallback (dart:async/future_impl.dart:488)
    #17     _Future._propagateToListeners (dart:async/future_impl.dart:571)
    #18     _Future._completeWithValue (dart:async/future_impl.dart:331)
    #19     _Future._asyncComplete.<anonymous closure> (dart:async/future_impl.dart:393)
    #20     _rootRun (dart:async/zone.dart:723)
    #21     _ZoneDelegate.run (dart:async/zone.dart:453)
    #22     _CustomizedZone.run (dart:async/zone.dart:663)
    #23     _BaseZone.runGuarded (dart:async/zone.dart:574)
    #24     _BaseZone.bindCallback.<anonymous closure> (dart:async/zone.dart:599)
    #25     _asyncRunCallbackLoop (dart:async/schedule_microtask.dart:23)
    #26     _asyncRunCallback (dart:async/schedule_microtask.dart:32)
    #27     _handleMutation (file:///Volumes/data/b/build/slave/dartium-mac-full-dev/build/src/dart/tools/dom/src/native_DOMImplementation.dart:588)

Closes #999
  • Loading branch information
chirayuk committed May 2, 2014
1 parent 50fc615 commit 4c3d7ee
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 150 deletions.
3 changes: 0 additions & 3 deletions lib/change_detection/change_detection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,6 @@ typedef dynamic FieldGetter(object);
typedef void FieldSetter(object, value);

abstract class FieldGetterFactory {
get isMethodInvoke;
bool isMethod(Object object, String name);
Function method(Object object, String name);
FieldGetter getter(Object object, String name);
}

Expand Down
29 changes: 20 additions & 9 deletions lib/change_detection/dirty_checking_change_detector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,8 @@ class DirtyCheckingRecord<H> implements Record<H>, WatchRecord<H> {
DirtyCheckingRecord<H> _prevRecord;
Record<H> _nextChange;
var _object;
var _identityValue;
bool _checkForVaryingClosure = false;
FieldGetter _getter;

DirtyCheckingRecord(this._group, this._fieldGetterFactory, this.handler,
Expand All @@ -418,6 +420,7 @@ class DirtyCheckingRecord<H> implements Record<H>, WatchRecord<H> {
_object = obj;
if (obj == null) {
_mode = _MODE_IDENTITY_;
_identityValue = null;
_getter = null;
return;
}
Expand Down Expand Up @@ -451,6 +454,7 @@ class DirtyCheckingRecord<H> implements Record<H>, WatchRecord<H> {
}
} else {
_mode = _MODE_IDENTITY_;
_identityValue = obj;
}

return;
Expand All @@ -460,14 +464,9 @@ class DirtyCheckingRecord<H> implements Record<H>, WatchRecord<H> {
_mode = _MODE_MAP_FIELD_;
_getter = null;
} else {
if (_fieldGetterFactory.isMethod(obj, field)) {
_mode = _MODE_IDENTITY_;
previousValue = currentValue = _fieldGetterFactory.method(obj, field)(obj);
assert(previousValue is Function);
} else {
_mode = _MODE_GETTER_;
_getter = _fieldGetterFactory.getter(obj, field);
}
_mode = _MODE_GETTER_;
_checkForVaryingClosure = true;
_getter = _fieldGetterFactory.getter(obj, field);
}
}

Expand All @@ -479,12 +478,24 @@ class DirtyCheckingRecord<H> implements Record<H>, WatchRecord<H> {
return false;
case _MODE_GETTER_:
current = _getter(object);
if (_checkForVaryingClosure) {
_checkForVaryingClosure = false;
// NOTE: Method as handled as _MODE_IDENTITY_. When Dart looks up a
// method "foo" on object "x", it returns a new closure for each lookup.
// They compare equal via "==" but are no identical(). There's no point
// getting a new value each time and decide it's the same so we'll skip
// further checking after the first time.
if (current is Function && !identical(current, _getter(object))) {
_mode = _MODE_IDENTITY_;
_identityValue = current;
}
}
break;
case _MODE_MAP_FIELD_:
current = object[field];
break;
case _MODE_IDENTITY_:
current = object;
current = _identityValue;
break;
case _MODE_MAP_:
return (currentValue as _MapChangeRecord)._check(object);
Expand Down
18 changes: 1 addition & 17 deletions lib/change_detection/dirty_checking_change_detector_dynamic.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,9 @@ export 'package:angular/change_detection/change_detection.dart' show
import 'dart:mirrors';

class DynamicFieldGetterFactory implements FieldGetterFactory {
final isMethodInvoke = true;

isMethod(Object object, String name) {
var declaration = reflectClass(object.runtimeType).declarations[new Symbol(name)];
return declaration != null &&
declaration is MethodMirror &&
(declaration as MethodMirror).isRegularMethod;
}

Function method(Object object, String name) {
Symbol symbol = new Symbol(name);
InstanceMirror instanceMirror = reflect(object);
return (List args, Map namedArgs) =>
instanceMirror.invoke(symbol, args, namedArgs).reflectee;
}

FieldGetter getter(Object object, String name) {
Symbol symbol = new Symbol(name);
InstanceMirror instanceMirror = reflect(object);
return (Object object) => instanceMirror.getField(symbol).reflectee;
return (Object object) => instanceMirror.getField(symbol).reflectee;
}
}
17 changes: 0 additions & 17 deletions lib/change_detection/dirty_checking_change_detector_static.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,13 @@ library dirty_checking_change_detector_static;
import 'package:angular/change_detection/change_detection.dart';

class StaticFieldGetterFactory implements FieldGetterFactory {
final isMethodInvoke = false;
Map<String, FieldGetter> getters;

StaticFieldGetterFactory(this.getters);

bool isMethod(Object object, String name) {
// We need to know if we are referring to method or field which is a
// function. We can find out by calling it twice and seeing if we get
// the same value. Methods create a new closure each time.
FieldGetter getterFn = getter(object, name);
dynamic property = getterFn(object);
return (property is Function) &&
(!identical(property, getterFn(object)));
}

FieldGetter getter(Object object, String name) {
var getter = getters[name];
if (getter == null) throw "Missing getter: (o) => o.$name";
return getter;
}

Function method(Object object, String name) {
var method = getters[name];
if (method == null) throw "Missing method: $name";
return method;
}
}
54 changes: 29 additions & 25 deletions lib/change_detection/watch_group.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ typedef void ChangeLog(String expression, current, previous);
* number of arguments with which the function will get called with.
*/
abstract class FunctionApply {
// dartbug.com/16401
// dynamic call() { throw new StateError('Use apply()'); }
dynamic call() { throw new StateError('Use apply()'); }
dynamic apply(List arguments);
}

Expand Down Expand Up @@ -198,7 +197,7 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList {
* - [isPure] A pure function is one which holds no internal state. This implies that the
* function is idempotent.
*/
_EvalWatchRecord addFunctionWatch(/* dartbug.com/16401 Function */ fn, List<AST> argsAST,
_EvalWatchRecord addFunctionWatch(Function fn, List<AST> argsAST,
Map<Symbol, AST> namedArgsAST,
String expression, bool isPure) =>
_addEvalWatch(null, fn, null, argsAST, namedArgsAST, expression, isPure);
Expand All @@ -214,11 +213,11 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList {
_EvalWatchRecord addMethodWatch(AST lhs, String name, List<AST> argsAST,
Map<Symbol, AST> namedArgsAST,
String expression) =>
_addEvalWatch(lhs, null, name, argsAST, namedArgsAST, expression, false);
_addEvalWatch(lhs, null, name, argsAST, namedArgsAST, expression, false);



_EvalWatchRecord _addEvalWatch(AST lhsAST, /* dartbug.com/16401 Function */ fn, String name,
_EvalWatchRecord _addEvalWatch(AST lhsAST, Function fn, String name,
List<AST> argsAST,
Map<Symbol, AST> namedArgsAST,
String expression, bool isPure) {
Expand Down Expand Up @@ -722,19 +721,19 @@ class _EvalWatchRecord implements WatchRecord<_Handler> {
static const int _MODE_FUNCTION_ = 2;
static const int _MODE_PURE_FUNCTION_APPLY_ = 3;
static const int _MODE_NULL_ = 4;
static const int _MODE_FIELD_CLOSURE_ = 5;
static const int _MODE_MAP_CLOSURE_ = 6;
static const int _MODE_METHOD_ = 7;
static const int _MODE_METHOD_INVOKE_ = 8;
static const int _MODE_METHOD_ = 5;
static const int _MODE_FIELD_CLOSURE_ = 6;
static const int _MODE_MAP_CLOSURE_ = 7;
WatchGroup watchGrp;
final _Handler handler;
final List args;
final Map<Symbol, dynamic> namedArgs = new Map<Symbol, dynamic>();
final String name;
int mode;
/* dartbug.com/16401 Function*/ var fn;
Function fn;
FieldGetterFactory _fieldGetterFactory;
bool dirtyArgs = true;
bool _checkForVaryingClosure = false;

dynamic currentValue, previousValue, _object;
_EvalWatchRecord _prevEvalWatch, _nextEvalWatch;
Expand Down Expand Up @@ -789,13 +788,9 @@ class _EvalWatchRecord implements WatchRecord<_Handler> {
if (value is Map) {
mode = _MODE_MAP_CLOSURE_;
} else {
if (_fieldGetterFactory.isMethod(value, name)) {
mode = _fieldGetterFactory.isMethodInvoke ? _MODE_METHOD_INVOKE_ : _MODE_METHOD_;
fn = _fieldGetterFactory.method(value, name);
} else {
mode = _MODE_FIELD_CLOSURE_;
fn = _fieldGetterFactory.getter(value, name);
}
mode = _MODE_FIELD_CLOSURE_;
fn = _fieldGetterFactory.getter(value, name);
_checkForVaryingClosure = true;
}
}
}
Expand All @@ -820,19 +815,28 @@ class _EvalWatchRecord implements WatchRecord<_Handler> {
value = (fn as FunctionApply).apply(args);
dirtyArgs = false;
break;
case _MODE_METHOD_:
value = Function.apply(fn, args, namedArgs);
break;
case _MODE_FIELD_CLOSURE_:
var closure = fn(_object);
value = closure == null ? null : Function.apply(closure, args, namedArgs);
if (_checkForVaryingClosure) {
_checkForVaryingClosure = false;
// NOTE: Method as handled as _MODE_IDENTITY_. When Dart looks up a
// method "foo" on object "x", it returns a new closure for each lookup.
// They compare equal via "==" but are no identical(). There's no point
// getting a new value each time and decide it's the same so we'll skip
// further checking after the first time.
if (closure is Function && !identical(closure, fn(_object))) {
mode = _MODE_METHOD_;
fn = closure;
}
}
value = (closure == null) ? null : Function.apply(closure, args, namedArgs);
break;
case _MODE_MAP_CLOSURE_:
var closure = object[name];
value = closure == null ? null : Function.apply(closure, args, namedArgs);
break;
case _MODE_METHOD_:
value = Function.apply(fn, args, namedArgs);
break;
case _MODE_METHOD_INVOKE_:
value = fn(args, namedArgs);
value = (closure == null) ? null : Function.apply(closure, args, namedArgs);
break;
default:
assert(false);
Expand Down
95 changes: 17 additions & 78 deletions test/change_detection/dirty_checking_change_detector_spec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,89 +8,14 @@ import 'package:angular/change_detection/dirty_checking_change_detector_dynamic.
import 'dart:collection';
import 'dart:math';

void main() {
describe('DirtyCheckingChangeDetector', () {
void testWithGetterFactory(FieldGetterFactory getterFactory) {
describe('DirtyCheckingChangeDetector with ${getterFactory.runtimeType}', () {
DirtyCheckingChangeDetector<String> detector;
FieldGetterFactory getterFactory = new StaticFieldGetterFactory({
"first": (o) => o.first,
"age": (o) => o.age,
"last": (o) => o.last,
"toString": (o) => o.toString,
"isUnderAge": (o) => o.isUnderAge,
"isUnderAgeAsVariable": (o) => o.isUnderAgeAsVariable
});

beforeEach(() {
detector = new DirtyCheckingChangeDetector<String>(getterFactory);
});

describe('StaticFieldGetterFactory', () {
DirtyCheckingChangeDetector<String> detector;
var user = new _User('Marko', 'Vuksanovic', 30);
FieldGetterFactory getterFactory = new StaticFieldGetterFactory({
"first": (o) => o.first,
"age": (o) => o.age,
"last": (o) => o.last,
"toString": (o) => o.toString,
"isUnderAge": (o) => o.isUnderAge,
"isUnderAgeAsVariable": (o) => o.isUnderAgeAsVariable,
"list": (o) => o.list,
"map": (o) => o.map
});

beforeEach(() {
detector = new DirtyCheckingChangeDetector<String>(getterFactory);
});

it('should detect methods', () {
var obj = new _User();
expect(getterFactory.isMethod(obj, 'toString')).toEqual(true);
expect(getterFactory.isMethod(obj, 'age')).toEqual(false);
});

it('should return true is method is real method', () {
expect(getterFactory.isMethod(user, 'isUnderAge')).toEqual(true);
});

it('should return false is field is a function', () {
expect(getterFactory.isMethod(user, 'isUnderAgeAsVariable')).toEqual(false);
});

it('should return false is field is a list', () {
expect(getterFactory.isMethod(user, 'list')).toEqual(false);
});

it('should return false is field is a map', () {
expect(getterFactory.isMethod(user, 'map')).toEqual(false);
});
});

describe('Dynamic GetterFactory', () {
DirtyCheckingChangeDetector<String> detector;
var user = new _User('Marko', 'Vuksanovic', 30);
FieldGetterFactory getterFactory = new DynamicFieldGetterFactory();

beforeEach(() {
detector = new DirtyCheckingChangeDetector<String>(getterFactory);
});

it('should return true is method is real method', () {
expect(getterFactory.isMethod(user, 'isUnderAge')).toEqual(true);
});

it('should return false is field is a function', () {
expect(getterFactory.isMethod(user, 'isUnderAgeAsVariable')).toEqual(false);
});

it('should return false is field is a list', () {
expect(getterFactory.isMethod(user, 'list')).toEqual(false);
});

it('should return false is field is a map', () {
expect(getterFactory.isMethod(user, 'map')).toEqual(false);
});
});

describe('object field', () {
it('should detect nothing', () {
var changes = detector.collectChanges();
Expand Down Expand Up @@ -787,6 +712,21 @@ void main() {
});
}


void main() {
testWithGetterFactory(new DynamicFieldGetterFactory());

testWithGetterFactory(new StaticFieldGetterFactory({
"first": (o) => o.first,
"age": (o) => o.age,
"last": (o) => o.last,
"toString": (o) => o.toString,
"isUnderAge": (o) => o.isUnderAge,
"isUnderAgeAsVariable": (o) => o.isUnderAgeAsVariable,
}));
}


class _User {
String first;
String last;
Expand Down Expand Up @@ -1022,7 +962,6 @@ class MapRecordMatcher extends _CollectionMatcher<KeyValueRecord> {
}
}


class FooBar {
static int fooIds = 0;

Expand Down

0 comments on commit 4c3d7ee

Please sign in to comment.