8000 Allow generating a mock class which includes overriding members with … · dart-lang/mockito@21f486f · GitHub
[go: up one dir, main page]

Skip to content

Commit 21f486f

Browse files
committed
Allow generating a mock class which includes overriding members with private types.
Such members cannot be stubbed with mockito, and will only be generated when specified in MockSpec `unsupportedMembers`. PiperOrigin-RevId: 470794362
1 parent ced77c9 commit 21f486f

File tree

3 files changed

+206
-39
lines changed

3 files changed

+206
-39
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
* Fix analyzer and code_builder dependencies.
44
* Reference `@GenerateNiceMocks` in documentation.
5+
* Allow generating a mock class which includes overriding members with private
6+
types in their signature. Such members cannot be stubbed with mockito, and
7+
will only be generated when specified in MockSpec `unsupportedMembers`.
58

69
## 5.3.0
710

lib/src/builder.dart

Lines changed: 95 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -836,13 +836,19 @@ class _MockTargetGatherer {
836836
}
837837
}
838838

839+
String get _tryUnsupportedMembersMessage => 'Try generating this mock with '
840+
"a MockSpec with 'unsupportedMembers' or a dummy generator (see "
841+
'https://pub.dev/documentation/mockito/latest/annotations/MockSpec-class.html).';
842+
839843
/// Checks [function] for properties that would make it un-stubbable.
840844
///
841845
/// Types are checked in the following positions:
842846
/// - return type
843847
/// - parameter types
844848
/// - bounds of type parameters
845-
/// - type arguments on types in the above three positions
849+
/// - recursively, written types on types in the above three positions
850+
/// (namely, type arguments, return types of function types, and parameter
851+
/// types of function types)
846852
///
847853
/// If any type in the above positions is private, [function] is un-stubbable.
848854
/// If the return type is potentially non-nullable, [function] is
@@ -861,14 +867,19 @@ class _MockTargetGatherer {
861867
final errorMessages = <String>[];
862868
final returnType = function.returnType;
863869
if (returnType is analyzer.InterfaceType) {
864-
if (returnType.element2.isPrivate) {
865-
errorMessages.add(
866-
'${enclosingElement.fullName} features a private return type, and '
867-
'cannot be stubbed.');
870+
if (returnType.containsPrivateName) {
871+
if (!allowUnsupportedMember && !hasDummyGenerator) {
872+
errorMessages.add(
873+
'${enclosingElement.fullName} features a private return type, '
874+
'and cannot be stubbed. $_tryUnsupportedMembersMessage');
875+
}
868876
}
869877
errorMessages.addAll(_checkTypeArguments(
870-
returnType.typeArguments, enclosingElement,
871-
isParameter: isParameter));
878+
returnType.typeArguments,
879+
enclosingElement,
880+
isParameter: isParameter,
881+
allowUnsupportedMember: allowUnsupportedMember,
882+
));
872883
} else if (returnType is analyzer.FunctionType) {
873884
errorMessages.addAll(_checkFunction(returnType, enclosingElement,
874885
allowUnsupportedMember: allowUnsupportedMember,
@@ -878,11 +889,10 @@ class _MockTargetGatherer {
878889
!allowUnsupportedMember &&
879890
!hasDummyGenerator &&
880891
_entryLib.typeSystem.isPotentiallyNonNullable(returnType)) {
881-
errorMessages.add(
882-
'${enclosingElement.fullName} features a non-nullable unknown '
883-
'return type, and cannot be stubbed. Try generating this mock with '
884-
"a MockSpec with 'unsupportedMembers' or a dummy generator (see "
885-
'https://pub.dev/documentation/mockito/latest/annotations/MockSpec-class.html).');
892+
errorMessages
893+
.add('${enclosingElement.fullName} features a non-nullable unknown '
894+
'return type, and cannot be stubbed. '
895+
'$_tryUnsupportedMembersMessage');
886896
}
887897
}
888898

@@ -894,13 +904,19 @@ class _MockTargetGatherer {
894904
// Technically, we can expand the type in the mock to something like
895905
// `Object?`. However, until there is a decent use case, we will not
896906
// generate such a mock.
897-
errorMessages.add(
898-
'${enclosingElement.fullName} features a private parameter type, '
899-
"'${parameterTypeElement.name}', and cannot be stubbed.");
907+
if (!allowUnsupportedMember) {
908+
errorMessages.add(
909+
'${enclosingElement.fullName} features a private parameter '
910+
"type, '${parameterTypeElement.name}', and cannot be stubbed. "
911+
'$_tryUnsupportedMembersMessage');
912+
}
900913
}
901914
errorMessages.addAll(_checkTypeArguments(
902-
parameterType.typeArguments, enclosingElement,
903-
isParameter: true));
915+
parameterType.typeArguments,
916+
enclosingElement,
917+
isParameter: true,
918+
allowUnsupportedMember: allowUnsupportedMember,
919+
));
904920
} else if (parameterType is analyzer.FunctionType) {
905921
errorMessages.addAll(
906922
_checkFunction(parameterType, enclosingElement, isParameter: true));
@@ -928,6 +944,8 @@ class _MockTargetGatherer {
928944
var typeParameter = element.bound;
929945
if (typeParameter == null) continue;
930946
if (typeParameter is analyzer.InterfaceType) {
947+
// TODO(srawlins): Check for private names in bound; could be
948+
// `List<_Bar>`.
931949
if (typeParameter.element2.isPrivate) {
932950
errorMessages.add(
933951
'${enclosingElement.fullName} features a private type parameter '
@@ -947,18 +965,23 @@ class _MockTargetGatherer {
947965
List<analyzer.DartType> typeArguments,
948966
Element enclosingElement, {
949967
bool isParameter = false,
968+
bool allowUnsupportedMember = false,
950969
}) {
951970
var errorMessages = <String>[];
952971
for (var typeArgument in typeArguments) {
953972
if (typeArgument is analyzer.InterfaceType) {
954-
if (typeArgument.element2.isPrivate) {
973+
if (typeArgument.element2.isPrivate && !allowUnsupportedMember) {
955974
errorMessages.add(
956975
'${enclosingElement.fullName} features a private type argument, '
957-
'and cannot be stubbed.');
976+
'and cannot be stubbed. $_tryUnsupportedMembersMessage');
958977
}
959978
} else if (typeArgument is analyzer.FunctionType) {
960-
errorMessages.addAll(_checkFunction(typeArgument, enclosingElement,
961-
isParameter: isParameter));
979+
errorMessages.addAll(_checkFunction(
980+
typeArgument,
981+
enclosingElement,
982+
isParameter: isParameter,
983+
allowUnsupportedMember: allowUnsupportedMember,
984+
));
962985
}
963986
}
964987
return errorMessages;
@@ -1233,11 +1256,16 @@ class _MockClassInfo {
12331256
void _buildOverridingMethod(MethodBuilder builder, MethodElement method) {
12341257
var name = method.displayName;
12351258
if (method.isOperator) name = 'operator$name';
1259+
final returnType = method.returnType;
12361260
builder
12371261
..name = name
12381262
..annotations.add(referImported('override', 'dart:core'))
1239-
..returns = _typeReference(method.returnType)
12401263
..types.addAll(method.typeParameters.map(_typeParameterReference));
1264+
// We allow overriding a method with a private return type by omitting the
1265+
// return type (which is then inherited).
1266+
if (!returnType.containsPrivateName) {
1267+
builder.returns = _typeReference(returnType);
1268+
}
12411269

12421270
// These two variables store the arguments that will be passed to the
12431271
// [Invocation] built for `noSuchMethod`.
@@ -1246,20 +1274,16 @@ class _MockClassInfo {
12461274

12471275
var position = 0;
12481276
for (final parameter in method.parameters) {
1249-
if (parameter.isRequiredPositional) {
1250-
final superParameterType =
1251-
_escapeCovariance(parameter, position: position);
1252-
final matchingParameter = _matchingParameter(parameter,
1253-
superParameterType: superParameterType, forceNullable: true);
1254-
builder.requiredParameters.add(matchingParameter);
1255-
invocationPositionalArgs.add(refer(parameter.displayName));
1256-
position++;
1257-
} else if (parameter.isOptionalPositional) {
1277+
if (parameter.isRequiredPositional || parameter.isOptionalPositional) {
12581278
final superParameterType =
12591279
_escapeCovariance(parameter, position: position);
12601280
final matchingParameter = _matchingParameter(parameter,
12611281
superParameterType: superParameterType, forceNullable: true);
1262-
builder.optionalParameters.add(matchingParameter);
1282+
if (parameter.isRequiredPositional) {
1283+
builder.requiredParameters.add(matchingParameter);
1284+
} else {
1285+
builder.optionalParameters.add(matchingParameter);
1286+
}
12631287
invocationPositionalArgs.add(refer(parameter.displayName));
12641288
position++;
12651289
} else if (parameter.isNamed) {
@@ -1282,11 +1306,18 @@ class _MockClassInfo {
12821306
return;
12831307
}
12841308

1285-
final returnType = method.returnType;
1309+
final returnTypeIsTypeVariable =
1310+
typeSystem.isPotentiallyNonNullable(returnType) &&
1311+
returnType is analyzer.TypeParameterType;
12861312
final fallbackGenerator = fallbackGenerators[method.name];
1287-
if (typeSystem.isPotentiallyNonNullable(returnType) &&
1288-
returnType is analyzer.TypeParameterType &&
1289-
fallbackGenerator == null) {
1313+
final parametersContainPrivateName =
1314+
method.parameters.any((p) => p.type.containsPrivateName);
1315+
final throwsUnsupported = fallbackGenerator == null &&
1316+
(returnTypeIsTypeVariable ||
1317+
returnType.containsPrivateName ||
1318+
parametersContainPrivateName);
1319+
1320+
if (throwsUnsupported) {
12901321
if (!mockTarget.unsupportedMembers.contains(name)) {
12911322
// We shouldn't get here as this is guarded against in
12921323
// [_MockTargetGatherer._checkFunction].
@@ -1557,10 +1588,11 @@ class _MockClassInfo {
15571588
'$defaultName');
15581589
final name = parameter.name.isEmpty ? defaultName! : parameter.name;
15591590
return Parameter((pBuilder) {
1560-
pBuilder
1561-
..name = name
1562-
..type =
1591+
pBuilder.name = name;
1592+
if (!superParameterType.containsPrivateName) {
1593+
pBuilder.type =
15631594
_typeReference(superParameterType, forceNullable: forceNullable);
1595+
}
15641596
if (parameter.isNamed) pBuilder.named = true;
15651597
if (parameter.defaultValueCode != null) {
15661598
try {
@@ -2025,6 +2057,30 @@ extension on Element {
20252057
}
20262058

20272059
extension on analyzer.DartType {
2060+
/// Whether this type contains a private name, perhaps in a type argument or a
2061+
/// function type's parameters, etc.
2062+
bool get containsPrivateName {
2063+
final self = this;
2064+
if (self is analyzer.DynamicType) {
2065+
return false;
2066+
} else if (self is analyzer.InterfaceType) {
2067+
return self.element2.isPrivate ||
2068+
self.typeArguments.any((t) => t.containsPrivateName);
2069+
} else if (self is analyzer.FunctionType) {
2070+
return self.returnType.containsPrivateName ||
2071+
self.parameters.any((p) => p.type.containsPrivateName);
2072+
} else if (self is analyzer.NeverType) {
2073+
return false;
2074+
} else if (self is analyzer.TypeParameterType) {
2075+
return false;
2076+
} else if (self is analyzer.VoidType) {
2077+
return false;
2078+
} else {
2079+
assert(false, 'Unexpected subtype of DartType: ${self.runtimeType}');
2080+
return false;
2081+
}
2082+
}
2083+
20282084
/// Returns whether this type is `Future<void>` or `Future<void>?`.
20292085
bool get isFutureOfVoid =>
20302086
isDartAsyncFuture &&

test/builder/custom_mocks_test.dart

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,114 @@ void main() {
462462
r" '\'m\' cannot be used without a mockito fallback generator.');"));
463463
});
464464

465+
test(
466+
'generates mock methods with private return types, given '
467+
'unsupportedMembers', () async {
468+
var mocksContent = await buildWithNonNullable({
469+
...annotationsAsset,
470+
'foo|lib/foo.dart': dedent(r'''
471+
abstract class Foo {
472+
_Bar m();
473+
}
474+
class _Bar {}
475+
'''),
476+
'foo|test/foo_test.dart': '''
477+
import 'package:foo/foo.dart';
478+
import 'package:mockito/annotations.dart';
479+
480+
@GenerateNiceMocks([
481+
MockSpec<Foo>(unsupportedMembers: {#m}),
482+
])
483+
void main() {}
484+
'''
485+
});
486+
expect(
487+
mocksContent,
488+
contains(' m() => throw UnsupportedError(\n'
489+
r" '\'m\' cannot be used without a mockito fallback generator.');"));
490+
});
491+
492+
test(
493+
'generates mock methods with return types with private names in type '
494+
'arguments, given unsupportedMembers', () async {
495+
var mocksContent = await buildWithNonNullable({
496+
...annotationsAsset,
497+
'foo|lib/foo.dart': dedent(r'''
498+
abstract class Foo {
499+
List<_Bar> m();
500+
}
501+
class _Bar {}
502+
'''),
503+
'foo|test/foo_test.dart': '''
504+
import 'package:foo/foo.dart';
505+
import 'package:mockito/annotations.dart';
506+
507+
@GenerateNiceMocks([
508+
MockSpec<Foo>(unsupportedMembers: {#m}),
509+
])
510+
void main() {}
511+
'''
512+
});
513+
expect(
514+
mocksContent,
515+
contains(' m() => throw UnsupportedError(\n'
516+
r" '\'m\' cannot be used without a mockito fallback generator.');"));
517+
});
518+
519+
test(
520+
'generates mock methods with return types with private names in function '
521+
'types, given unsupportedMembers', () async {
522+
var mocksContent = await buildWithNonNullable({
523+
...annotationsAsset,
524+
'foo|lib/foo.dart': dedent(r'''
525+
abstract class Foo {
526+
void Function(_Bar) m();
527+
}
528+
class _Bar {}
529+
'''),
530+
'foo|test/foo_test.dart': '''
531+
import 'package:foo/foo.dart';
532+
import 'package:mockito/annotations.dart';
533+
534+
@GenerateNiceMocks([
535+
MockSpec<Foo>(unsupportedMembers: {#m}),
536+
])
537+
void main() {}
538+
'''
539+
});
540+
expect(
541+
mocksContent,
542+
contains(' m() => throw UnsupportedError(\n'
543+
r" '\'m\' cannot be used without a mockito fallback generator.');"));
544+
});
545+
546+
test(
547+
'generates mock methods with private parameter types, given '
548+
'unsupportedMembers', () async {
549+
var mocksContent = await buildWithNonNullable({
550+
...annotationsAsset,
551+
'foo|lib/foo.dart': dedent(r'''
552+
abstract class Foo {
553+
void m(_Bar b);
554+
}
555+
class _Bar {}
556+
'''),
557+
'foo|test/foo_test.dart': '''
558+
import 'package:foo/foo.dart';
559+
import 'package:mockito/annotations.dart';
560+
561+
@GenerateNiceMocks([
562+
MockSpec<Foo>(unsupportedMembers: {#m}),
563+
])
564+
void main() {}
565+
'''
566+
});
567+
expect(
568+
mocksContent,
569+
contains(' void m(b) => throw UnsupportedError(\n'
570+
r" '\'m\' cannot be used without a mockito fallback generator.');"));
571+
});
572+
465573
test(
466574
'generates mock methods with non-nullable return types, specifying '
467575
'legal default values for basic known types', () async {

0 commit comments

Comments
 (0)
0