From 46c69e1b95eb7363907e37b90886a6dadc491296 Mon Sep 17 00:00:00 2001 From: Tom Ha Date: Wed, 15 Apr 2026 11:50:50 +0100 Subject: [PATCH 1/4] feat: First-pass implementation of supporting getters in mock notifiers --- .../lib/src/notifiers/notifier/notifier.dart | 2 + example/test/unit/notifier/notifier_test.dart | 4 +- .../code_builders/fake_type_code_builder.dart | 23 +- .../code_builders/method_code_builder.dart | 241 ++++++++++-------- 4 files changed, 165 insertions(+), 105 deletions(-) diff --git a/example/lib/src/notifiers/notifier/notifier.dart b/example/lib/src/notifiers/notifier/notifier.dart index b1a606c..5ca98a3 100644 --- a/example/lib/src/notifiers/notifier/notifier.dart +++ b/example/lib/src/notifiers/notifier/notifier.dart @@ -10,6 +10,8 @@ class DummySource extends _$DummySource { } Future someSideEffect() async {} + + String get dummy => "Hello, world!"; } @riverpod diff --git a/example/test/unit/notifier/notifier_test.dart b/example/test/unit/notifier/notifier_test.dart index 4b7a9cc..ba61761 100644 --- a/example/test/unit/notifier/notifier_test.dart +++ b/example/test/unit/notifier/notifier_test.dart @@ -15,13 +15,15 @@ void main() async { () => MockDummySource(() async => 36), ) ]); + final mockDummySource = container.read(dummySourceProvider(6).notifier) as MockDummySource; + when(mockDummySource.dummy).thenReturn("Hello!"); // Run test final result = await container.read(dummyProvider(6).future); // Verify expect(result, 36); - final mockDummySource = container.read(dummySourceProvider(6).notifier) as MockDummySource; verify(mockDummySource.someSideEffect()).called(1); + expect(mockDummySource.dummy, "Hello!"); }); } diff --git a/lib/src/code_builders/fake_type_code_builder.dart b/lib/src/code_builders/fake_type_code_builder.dart index a644246..df3c9e0 100644 --- a/lib/src/code_builders/fake_type_code_builder.dart +++ b/lib/src/code_builders/fake_type_code_builder.dart @@ -116,7 +116,7 @@ class FakeTypeCodeBuilder { .toList(); } - Code? getStubValue( + Code? getMethodStubValue( FakeType fakeType, String methodName, String positionalArgs, @@ -125,7 +125,7 @@ class FakeTypeCodeBuilder { if (element == null) { return null; } else if (element case InterfaceElement(isFutureOrStream: true)) { - return getStubValue( + return getMethodStubValue( fakeType.parameterTypes!.first, methodName, positionalArgs); } else { return Code.scope((allocate) { @@ -139,6 +139,25 @@ class FakeTypeCodeBuilder { } } + Code? getGetterStubValue( + FakeType fakeType, + String methodName, + ) { + final element = fakeType.element; + if (element == null) { + return null; + } else { + return Code.scope((allocate) { + final dummyInvocation = + allocate(refer("dummyValue", "package:mockito/src/dummies.dart")); + final typeParam = _allocateType(fakeType, allocate); + final positionalParams = + "this, Invocation.getter(#$methodName)"; + return "$dummyInvocation<$typeParam>($positionalParams)"; + }); + } + } + String _allocateType(FakeType type, String Function(Reference) allocate) { final parameterTypes = type.parameterTypes; final topLevelTypeCode = allocate(refer( diff --git a/lib/src/code_builders/method_code_builder.dart b/lib/src/code_builders/method_code_builder.dart index 1979e04..53f4091 100644 --- a/lib/src/code_builders/method_code_builder.dart +++ b/lib/src/code_builders/method_code_builder.dart @@ -14,137 +14,174 @@ class MethodCodeBuilder { final FakeTypeCodeBuilder _fakeTypeGenerator; final TypeReferencer _typeReferencer; - const MethodCodeBuilder( - this._fakeTypeGenerator, - this._typeReferencer, - ); + const MethodCodeBuilder(this._fakeTypeGenerator, this._typeReferencer); - List generateMethods( - ClassElement classElement, { + List generateMethods(ClassElement classElement, { bool seedValueProvided = true, }) { final methodElements = classElement.methods; - final returnTypes = - methodElements.map((method) => method.returnType).toSet(); + final getterElements = classElement.getters; + final returnTypes = [...methodElements, ...getterElements] + .map((method) => method.returnType) + .toSet(); final fakedTypes = _fakeTypeGenerator.generateFakeTypes(returnTypes); - return methodElements - .map((methodElement) => switch (methodElement.name) { - "build" => generateBuildMethod( - methodElement, seedValueProvided), - _ => generateMethod(methodElement, fakedTypes), - }) + final methods = methodElements + .map( + (methodElement) => + switch (methodElement.name) { + "build" => generateBuildMethod(methodElement, seedValueProvided), + _ => generateMethod(methodElement, fakedTypes), + }, + ) .whereType() .toList(); + final propertyAccessors = getterElements.map( + (getter) => generateGetter(getter, fakedTypes) + ).whereType().toList(); + return propertyAccessors + methods; } - Method generateBuildMethod( - MethodElement method, - bool seedValueProvided, - ) { - final isFuture = method.returnType.isDartAsyncFuture || + Method? generateGetter(PropertyAccessorElement propertyAccessor, + Set fakeTypes) { + final localName = propertyAccessor.name; + if (localName == null) { + return null; + } + final returnType = propertyAccessor.returnType; + final fakedReturnType = fakeTypes.cast().firstWhere( + (fakeType) => fakeType?.originalType == returnType, + orElse: () => null, + ); + final stubValue = switch(fakedReturnType) { + null => null, + _ => _fakeTypeGenerator.getGetterStubValue(fakedReturnType, localName), + }; + return Method((methodBuilder) { + methodBuilder + ..name = localName + ..type = MethodType.getter + ..returns = _typeReferencer.obtainReferenceForType( + propertyAccessor.returnType) + ..body = Block((blockBuilder) { + blockBuilder.addStaticCode( + "return noSuchMethod(Invocation.getter(#$localName)"); + if (stubValue != null) { + blockBuilder + ..addStaticCode(", returnValueForMissingStub: ") + ..addCode(stubValue) + ..addStaticCode(", returnValue: ") + ..addCode(stubValue); + } + blockBuilder.addStaticCode(",);"); + }); + }); + } + + Method generateBuildMethod(MethodElement method, bool seedValueProvided) { + final isFuture = + method.returnType.isDartAsyncFuture || method.returnType.isDartAsyncFutureOr; final isStream = method.returnType.isDartAsyncStream; - final positionalArgs = - method.formalParameters.where((p) => p.isPositional).toList(); + final positionalArgs = method.formalParameters + .where((p) => p.isPositional) + .toList(); final positionalArgsCode = positionalArgs.map((arg) => arg.name).join(", "); - return Method( - (methodBuilder) { - methodBuilder - ..name = "build" - ..body = Block((blockBuilder) { - blockBuilder.addStaticCode(_keepAliveInsert); - if (isStream) { - blockBuilder.addStaticCode("yield* "); - } else { - blockBuilder.addStaticCode("return "); - } + return Method((methodBuilder) { + methodBuilder + ..name = "build" + ..body = Block((blockBuilder) { + blockBuilder.addStaticCode(_keepAliveInsert); + if (isStream) { + blockBuilder.addStaticCode("yield* "); + } else { + blockBuilder.addStaticCode("return "); + } + blockBuilder.addStaticCode( + "noSuchMethod(Invocation.method(#build, [$positionalArgsCode])", + ); + if (seedValueProvided) { blockBuilder.addStaticCode( - "noSuchMethod(Invocation.method(#build, [$positionalArgsCode])", + ", returnValueForMissingStub: seedBuilder()", ); - if (seedValueProvided) { - blockBuilder.addStaticCode( - ", returnValueForMissingStub: seedBuilder()", - ); - } - blockBuilder.addStaticCode(");"); - }) - ..copyParameters( - method: method, - obtainReferenceForType: (type) => - _typeReferencer.obtainReferenceForType(type, true), - ) - ..annotations.add(const CodeExpression(Code("override"))) - ..returns = _typeReferencer.obtainReferenceForType(method.returnType); - if (isFuture) { - methodBuilder.modifier = MethodModifier.async; - } else if (isStream) { - methodBuilder.modifier = MethodModifier.asyncStar; - } - }, - ); + } + blockBuilder.addStaticCode(");"); + }) + ..copyParameters( + method: method, + obtainReferenceForType: (type) => + _typeReferencer.obtainReferenceForType(type, true), + ) + ..annotations.add(const CodeExpression(Code("override"))) + ..returns = _typeReferencer.obtainReferenceForType(method.returnType); + if (isFuture) { + methodBuilder.modifier = MethodModifier.async; + } else if (isStream) { + methodBuilder.modifier = MethodModifier.asyncStar; + } + }); } - Method? generateMethod( - MethodElement method, - Set fakeTypes, - ) { + Method? generateMethod(MethodElement method, Set fakeTypes) { if (!method.isPublic) { return null; } final methodName = method.name; - if(methodName == null) { + if (methodName == null) { return null; } - final isFuture = method.returnType.isDartAsyncFuture || + final isFuture = + method.returnType.isDartAsyncFuture || method.returnType.isDartAsyncFutureOr; final isStream = method.returnType.isDartAsyncStream; - final positionalArgs = - method.formalParameters.where((p) => p.isPositional).toList(); + final positionalArgs = method.formalParameters + .where((p) => p.isPositional) + .toList(); final positionalArgsCode = positionalArgs.map((arg) => arg.name).join(", "); final returnType = method.returnType; final fakedReturnType = fakeTypes.cast().firstWhere( - (fakeType) => fakeType?.originalType == returnType, - orElse: () => null, - ); + (fakeType) => fakeType?.originalType == returnType, + orElse: () => null, + ); final stubValue = fakedReturnType != null - ? _fakeTypeGenerator.getStubValue( - fakedReturnType, methodName, positionalArgsCode) - : null; - return Method( - (methodBuilder) { - methodBuilder - ..name = method.name - ..body = Block((blockBuilder) { - if (isStream) { - blockBuilder.addStaticCode("yield* "); - } else { - blockBuilder.addStaticCode("return "); - } - blockBuilder.addStaticCode( - "noSuchMethod(Invocation.method(#${method.name}, [$positionalArgsCode])", - ); - if (stubValue != null) { - blockBuilder.addStaticCode(", returnValueForMissingStub: "); - blockBuilder.addCode(stubValue); - blockBuilder.addStaticCode(", returnValue: "); - blockBuilder.addCode(stubValue); - } - blockBuilder.addStaticCode(",);"); - }) - ..copyParameters( - method: method, - obtainReferenceForType: (type) => - _typeReferencer.obtainReferenceForType(type, true), + ? _fakeTypeGenerator.getMethodStubValue( + fakedReturnType, + methodName, + positionalArgsCode, ) - ..annotations.add(const CodeExpression(Code("override"))) - ..returns = _typeReferencer.obtainReferenceForType(method.returnType); - if (isFuture) { - methodBuilder.modifier = MethodModifier.async; - } else if (isStream) { - methodBuilder.modifier = MethodModifier.asyncStar; - } - }, - ); + : null; + return Method((methodBuilder) { + methodBuilder + ..name = method.name + ..body = Block((blockBuilder) { + if (isStream) { + blockBuilder.addStaticCode("yield* "); + } else { + blockBuilder.addStaticCode("return "); + } + blockBuilder.addStaticCode( + "noSuchMethod(Invocation.method(#${method.name}, [$positionalArgsCode])", + ); + if (stubValue != null) { + blockBuilder.addStaticCode(", returnValueForMissingStub: "); + blockBuilder.addCode(stubValue); + blockBuilder.addStaticCode(", returnValue: "); + blockBuilder.addCode(stubValue); + } + blockBuilder.addStaticCode(",);"); + }) + ..copyParameters( + method: method, + obtainReferenceForType: (type) => + _typeReferencer.obtainReferenceForType(type, true), + ) + ..annotations.add(const CodeExpression(Code("override"))) + ..returns = _typeReferencer.obtainReferenceForType(method.returnType); + if (isFuture) { + methodBuilder.modifier = MethodModifier.async; + } else if (isStream) { + methodBuilder.modifier = MethodModifier.asyncStar; + } + }); } } From fd88d86e0cfcf857252c68db9cd264bb41e2e079 Mon Sep 17 00:00:00 2001 From: Tom Ha Date: Wed, 15 Apr 2026 13:18:54 +0100 Subject: [PATCH 2/4] test: Add unit tests for getter generation --- .../code_builders/method_code_builder.dart | 1 + .../method_code_builder_test.dart | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/lib/src/code_builders/method_code_builder.dart b/lib/src/code_builders/method_code_builder.dart index 53f4091..3143288 100644 --- a/lib/src/code_builders/method_code_builder.dart +++ b/lib/src/code_builders/method_code_builder.dart @@ -62,6 +62,7 @@ class MethodCodeBuilder { ..type = MethodType.getter ..returns = _typeReferencer.obtainReferenceForType( propertyAccessor.returnType) + ..annotations.add(const CodeExpression(Code("override"))) ..body = Block((blockBuilder) { blockBuilder.addStaticCode( "return noSuchMethod(Invocation.getter(#$localName)"); diff --git a/test/unit/code_builders/method_code_builder/method_code_builder_test.dart b/test/unit/code_builders/method_code_builder/method_code_builder_test.dart index 9394898..cab9468 100644 --- a/test/unit/code_builders/method_code_builder/method_code_builder_test.dart +++ b/test/unit/code_builders/method_code_builder/method_code_builder_test.dart @@ -16,6 +16,7 @@ import 'method_code_builder_test.mocks.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec() ]) void main() { group("Method code builder unit tests", () { @@ -427,5 +428,29 @@ void main() { ), ); }); + + test("generateGetter - generates expected method", () { + // Setup + final returnTypeFixture = MockDartType(); + final getterElementFixture = MockGetterElement(); + when(getterElementFixture.isPublic).thenReturn(true); + when(getterElementFixture.returnType).thenReturn(returnTypeFixture); + when(getterElementFixture.name).thenReturn("getter"); + when(mockTypeReferencer.obtainReferenceForType(any, any)).thenReturn( + const Reference("String", "dart:core"), + ); + + // Run test + final result = sut.generateGetter(getterElementFixture, {}); + + // Verify + expect(result, + isA() + .having((it) => it.name, "expected name", "getter",) + .having((it) => + it.returns?.symbol, "expected return type", "String",) + .having((it) => it.annotations.first.code.toString(), + "expected override annotation", "override",),); + }); }); } From dc34143fee34d7febee015a3a699cf6020c2d626 Mon Sep 17 00:00:00 2001 From: Tom Ha Date: Wed, 15 Apr 2026 13:27:41 +0100 Subject: [PATCH 3/4] test: Add getter reference to golden tests --- test/golden/flumepod/notifier/expected.flumepod.dart | 10 ++++++++++ test/golden/flumepod/notifier/input.dart | 2 ++ 2 files changed, 12 insertions(+) diff --git a/test/golden/flumepod/notifier/expected.flumepod.dart b/test/golden/flumepod/notifier/expected.flumepod.dart index feee9ea..4dfbf17 100644 --- a/test/golden/flumepod/notifier/expected.flumepod.dart +++ b/test/golden/flumepod/notifier/expected.flumepod.dart @@ -28,6 +28,16 @@ class MockTestNotifier extends _i1.$Notifier } } + @override + String get getter { + return noSuchMethod( + Invocation.getter(#getter), + returnValueForMissingStub: + _i5.dummyValue(this, Invocation.getter(#getter)), + returnValue: _i5.dummyValue(this, Invocation.getter(#getter)), + ); + } + @override String build() { // keepAlive() used so mock instance with stubbed/real calls isn't disposed. diff --git a/test/golden/flumepod/notifier/input.dart b/test/golden/flumepod/notifier/input.dart index a9f03aa..681d9ba 100644 --- a/test/golden/flumepod/notifier/input.dart +++ b/test/golden/flumepod/notifier/input.dart @@ -32,4 +32,6 @@ class TestNotifier extends _$TestNotifier { Future asyncSideEffect() async { return; } + + String get getter => "value"; } From e0e752068642bb3869520ea0101a0b6ddab7117a Mon Sep 17 00:00:00 2001 From: Tom Ha Date: Wed, 15 Apr 2026 13:29:56 +0100 Subject: [PATCH 4/4] style: Format `method_code_builder.dart` --- .../code_builders/method_code_builder.dart | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/lib/src/code_builders/method_code_builder.dart b/lib/src/code_builders/method_code_builder.dart index 3143288..9d1834b 100644 --- a/lib/src/code_builders/method_code_builder.dart +++ b/lib/src/code_builders/method_code_builder.dart @@ -16,43 +16,47 @@ class MethodCodeBuilder { const MethodCodeBuilder(this._fakeTypeGenerator, this._typeReferencer); - List generateMethods(ClassElement classElement, { + List generateMethods( + ClassElement classElement, { bool seedValueProvided = true, }) { final methodElements = classElement.methods; final getterElements = classElement.getters; - final returnTypes = [...methodElements, ...getterElements] - .map((method) => method.returnType) - .toSet(); + final returnTypes = [ + ...methodElements, + ...getterElements, + ].map((method) => method.returnType).toSet(); final fakedTypes = _fakeTypeGenerator.generateFakeTypes(returnTypes); final methods = methodElements .map( - (methodElement) => - switch (methodElement.name) { - "build" => generateBuildMethod(methodElement, seedValueProvided), - _ => generateMethod(methodElement, fakedTypes), - }, - ) + (methodElement) => switch (methodElement.name) { + "build" => generateBuildMethod(methodElement, seedValueProvided), + _ => generateMethod(methodElement, fakedTypes), + }, + ) + .whereType() + .toList(); + final propertyAccessors = getterElements + .map((getter) => generateGetter(getter, fakedTypes)) .whereType() .toList(); - final propertyAccessors = getterElements.map( - (getter) => generateGetter(getter, fakedTypes) - ).whereType().toList(); return propertyAccessors + methods; } - Method? generateGetter(PropertyAccessorElement propertyAccessor, - Set fakeTypes) { + Method? generateGetter( + PropertyAccessorElement propertyAccessor, + Set fakeTypes, + ) { final localName = propertyAccessor.name; if (localName == null) { return null; } final returnType = propertyAccessor.returnType; final fakedReturnType = fakeTypes.cast().firstWhere( - (fakeType) => fakeType?.originalType == returnType, + (fakeType) => fakeType?.originalType == returnType, orElse: () => null, ); - final stubValue = switch(fakedReturnType) { + final stubValue = switch (fakedReturnType) { null => null, _ => _fakeTypeGenerator.getGetterStubValue(fakedReturnType, localName), }; @@ -61,11 +65,13 @@ class MethodCodeBuilder { ..name = localName ..type = MethodType.getter ..returns = _typeReferencer.obtainReferenceForType( - propertyAccessor.returnType) + propertyAccessor.returnType, + ) ..annotations.add(const CodeExpression(Code("override"))) ..body = Block((blockBuilder) { blockBuilder.addStaticCode( - "return noSuchMethod(Invocation.getter(#$localName)"); + "return noSuchMethod(Invocation.getter(#$localName)", + ); if (stubValue != null) { blockBuilder ..addStaticCode(", returnValueForMissingStub: ")