Skip to content

Commit

Permalink
GitHub Sync (#602)
Browse files Browse the repository at this point in the history
* Added support for generated mocks from typedef-aliased classes.

PiperOrigin-RevId: 501338177

* Update the mockito readme file.

PiperOrigin-RevId: 503229973

* Ignore an upcoming diagnostic for `operator ==`.

The comment on the `operator ==` override explains
why it is needed.

PiperOrigin-RevId: 503381552
  • Loading branch information
srawlins committed Jan 20, 2023
1 parent 0128352 commit 09aabe7
Show file tree
Hide file tree
Showing 7 changed files with 575 additions and 66 deletions.
2 changes: 1 addition & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates

version: 2
updates:
updates:
- package-ecosystem: github-actions
directory: /
schedule:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Fix nice mocks generation in mixed mode (generated code is pre null-safety,
while mocked class is null-safe).
* Require Dart >= 2.17.0.
* Support typedef-aliased classes in `@GenerateMocks` and `@GenerateNiceMocks`

## 5.3.2

Expand Down
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# mockito
[![Dart CI](https://github.com/dart-lang/mockito/actions/workflows/test-package.yml/badge.svg)](https://github.com/dart-lang/mockito/actions/workflows/test-package.yml)
[![Pub](https://img.shields.io/pub/v/mockito.svg)](https://pub.dev/packages/mockito)
[![package publisher](https://img.shields.io/pub/publisher/mockito.svg)](https://pub.dev/packages/mockito/publisher)

Mock library for Dart inspired by [Mockito](https://github.com/mockito/mockito).

[![Pub](https://img.shields.io/pub/v/mockito.svg)](https://pub.dev/packages/mockito)
[![Build Status](https://travis-ci.org/dart-lang/mockito.svg?branch=master)](https://travis-ci.org/dart-lang/mockito)

## Let's create mocks

Mockito 5.0.0 supports Dart's new **null safety** language feature in Dart 2.12,
Expand Down Expand Up @@ -330,7 +329,7 @@ void main() {
}
```

The `Cat.sound` method retuns a non-nullable String, but no stub has been made
The `Cat.sound` method returns a non-nullable String, but no stub has been made
with `when(cat.sound())`, so what should the code do? What is the "missing stub"
behavior?

Expand Down
125 changes: 78 additions & 47 deletions lib/src/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ $rawOutput
seenTypes.add(type);
librariesWithTypes.add(type.element.library);
type.element.accept(typeVisitor);
if (type.alias != null) type.alias!.element.accept(typeVisitor);
// For a type like `Foo<Bar>`, add the `Bar`.
type.typeArguments
.whereType<analyzer.InterfaceType>()
Expand Down Expand Up @@ -296,6 +297,12 @@ class _TypeVisitor extends RecursiveElementVisitor<void> {
super.visitTypeParameterElement(element);
}

@override
void visitTypeAliasElement(TypeAliasElement element) {
_elements.add(element);
super.visitTypeAliasElement(element);
}

/// Adds [type] to the collected [_elements].
void _addType(analyzer.DartType? type) {
if (type == null) return;
Expand Down Expand Up @@ -406,14 +413,15 @@ class _MockTarget {
final bool hasExplicitTypeArguments;

_MockTarget(
this.classType,
this.mockName, {
this.classType, {
required this.mixins,
required this.onMissingStub,
required this.unsupportedMembers,
required this.fallbackGenerators,
this.hasExplicitTypeArguments = false,
});
String? mockName,
}) : mockName = mockName ??
'Mock${classType.alias?.element.name ?? classType.element.name}';

InterfaceElement get interfaceElement => classType.element;
}
Expand Down Expand Up @@ -509,17 +517,17 @@ class _MockTargetGatherer {
throw InvalidMockitoAnnotationException(
'Mockito cannot mock `dynamic`');
}
final type = _determineDartType(typeToMock, entryLib.typeProvider);
// For a generic class like `Foo<T>` or `Foo<T extends num>`, a type
// literal (`Foo`) cannot express type arguments. The type argument(s) on
// `type` have been instantiated to bounds here. Switch to the
// declaration, which will be an uninstantiated type.
final declarationType =
(type.element.declaration as InterfaceElement).thisType;
final mockName = 'Mock${declarationType.element.name}';
var type = _determineDartType(typeToMock, entryLib.typeProvider);
if (type.alias == null) {
// For a generic class without an alias like `Foo<T>` or
// `Foo<T extends num>`, a type literal (`Foo`) cannot express type
// arguments. The type argument(s) on `type` have been instantiated to
// bounds here. Switch to the declaration, which will be an
// uninstantiated type.
type = (type.element.declaration as InterfaceElement).thisType;
}
mockTargets.add(_MockTarget(
declarationType,
mockName,
type,
mixins: [],
onMissingStub: OnMissingStub.throwException,
unsupportedMembers: {},
Expand Down Expand Up @@ -562,35 +570,41 @@ class _MockTargetGatherer {
}
}
var type = _determineDartType(typeToMock, entryLib.typeProvider);

final mockTypeArguments = mockType?.typeArguments;
if (mockTypeArguments == null) {
// The type was given without explicit type arguments. In
// this case the type argument(s) on `type` have been instantiated to
// bounds. Switch to the declaration, which will be an uninstantiated
// type.
type = (type.element.declaration as InterfaceElement).thisType;
} else {
if (mockTypeArguments != null) {
final typeName =
type.alias?.element.getDisplayString(withNullability: false) ??
'type $type';
final typeArguments = type.alias?.typeArguments ?? type.typeArguments;
// Check explicit type arguments for unknown types that were
// turned into `dynamic` by the analyzer.
type.typeArguments.forEachIndexed((typeArgIdx, typeArgument) {
typeArguments.forEachIndexed((typeArgIdx, typeArgument) {
if (!typeArgument.isDynamic) return;
if (typeArgIdx >= mockTypeArguments.arguments.length) return;
final typeArgAst = mockTypeArguments.arguments[typeArgIdx];
if (typeArgAst is! ast.NamedType) {
// Is this even possible?
throw InvalidMockitoAnnotationException(
'Undefined type $typeArgAst passed as the ${(typeArgIdx + 1).ordinal} type argument for mocked type $type');
'Undefined type $typeArgAst passed as the '
'${(typeArgIdx + 1).ordinal} type argument for mocked '
'$typeName.');
}
if (typeArgAst.name.name == 'dynamic') return;
throw InvalidMockitoAnnotationException(
'Undefined type $typeArgAst passed as the ${(typeArgIdx + 1).ordinal} type argument for mocked type $type. '
'Are you trying to pass to-be-generated mock class as a type argument? Mockito does not support that (yet).',
'Undefined type $typeArgAst passed as the '
'${(typeArgIdx + 1).ordinal} type argument for mocked $typeName. Are '
'you trying to pass to-be-generated mock class as a type argument? '
'Mockito does not support that (yet).',
);
});
} else if (type.alias == null) {
// The mock type was given without explicit type arguments. In this case
// the type argument(s) on `type` have been instantiated to bounds if this
// isn't a type alias. Switch to the declaration, which will be an
// uninstantiated type.
type = (type.element.declaration as InterfaceElement).thisType;
}
final mockName = mockSpec.getField('mockName')!.toSymbolValue() ??
'Mock${type.element.name}';
final mockName = mockSpec.getField('mockName')!.toSymbolValue();
final mixins = <analyzer.InterfaceType>[];
for (final m in mockSpec.getField('mixins')!.toListValue()!) {
final typeToMixin = m.toTypeValue();
Expand Down Expand Up @@ -662,7 +676,7 @@ class _MockTargetGatherer {
mockSpec.getField('fallbackGenerators')!.toMapValue()!;
return _MockTarget(
type,
mockName,
mockName: mockName,
mixins: mixins,
onMissingStub: onMissingStub,
unsupportedMembers: unsupportedMembers,
Expand Down Expand Up @@ -740,17 +754,17 @@ class _MockTargetGatherer {
"'${elementToMock.displayName}' for the following reasons:\n"
'$joinedMessages');
}
if (typeToMock.alias != null &&
typeToMock.nullabilitySuffix == NullabilitySuffix.question) {
throw InvalidMockitoAnnotationException(
'Mockito cannot mock a type-aliased nullable type: '
'${typeToMock.alias!.element.name}');
}
return typeToMock;
}

var aliasElement = typeToMock.alias?.element;
if (aliasElement != null) {
throw InvalidMockitoAnnotationException('Mockito cannot mock a typedef: '
'${aliasElement.displayName}');
} else {
throw InvalidMockitoAnnotationException(
'Mockito cannot mock a non-class: $typeToMock');
}
throw InvalidMockitoAnnotationException('Mockito cannot mock a non-class: '
'${typeToMock.alias?.element.name ?? typeToMock.toString()}');
}

void _checkClassesToMockAreValid() {
Expand Down Expand Up @@ -1097,10 +1111,14 @@ class _MockClassInfo {
});

Class _buildMockClass() {
final typeToMock = mockTarget.classType;
final typeAlias = mockTarget.classType.alias;
final aliasedElement = typeAlias?.element;
final aliasedType =
typeAlias?.element.aliasedType as analyzer.InterfaceType?;
final typeToMock = aliasedType ?? mockTarget.classType;
final classToMock = mockTarget.interfaceElement;
final classIsImmutable = classToMock.metadata.any((it) => it.isImmutable);
final className = classToMock.name;
final className = aliasedElement?.name ?? classToMock.name;

return Class((cBuilder) {
cBuilder
Expand All @@ -1118,25 +1136,32 @@ class _MockClassInfo {
// For each type parameter on [classToMock], the Mock class needs a type
// parameter with same type variables, and a mirrored type argument for
// the "implements" clause.
var typeArguments = <Reference>[];
final typeReferences = <Reference>[];

final typeParameters =
aliasedElement?.typeParameters ?? classToMock.typeParameters;
final typeArguments =
typeAlias?.typeArguments ?? typeToMock.typeArguments;

if (mockTarget.hasExplicitTypeArguments) {
// [typeToMock] is a reference to a type with type arguments (for
// example: `Foo<int>`). Generate a non-generic mock class which
// implements the mock target with said type arguments. For example:
// `class MockFoo extends Mock implements Foo<int> {}`
for (var typeArgument in typeToMock.typeArguments) {
typeArguments.add(_typeReference(typeArgument));
for (var typeArgument in typeArguments) {
typeReferences.add(_typeReference(typeArgument));
}
} else {
// [typeToMock] is a simple reference to a generic type (for example:
// `Foo`, a reference to `class Foo<T> {}`). Generate a generic mock
// class which perfectly mirrors the type parameters on [typeToMock],
// forwarding them to the "implements" clause.
for (var typeParameter in classToMock.typeParameters) {
for (var typeParameter in typeParameters) {
cBuilder.types.add(_typeParameterReference(typeParameter));
typeArguments.add(refer(typeParameter.name));
typeReferences.add(refer(typeParameter.name));
}
}

for (final mixin in mockTarget.mixins) {
cBuilder.mixins.add(TypeReference((b) {
b
Expand All @@ -1147,15 +1172,21 @@ class _MockClassInfo {
}
cBuilder.implements.add(TypeReference((b) {
b
..symbol = classToMock.name
..url = _typeImport(mockTarget.interfaceElement)
..types.addAll(typeArguments);
..symbol = className
..url = _typeImport(aliasedElement ?? classToMock)
..types.addAll(typeReferences);
}));
if (mockTarget.onMissingStub == OnMissingStub.throwException) {
cBuilder.constructors.add(_constructorWithThrowOnMissingStub);
}

final substitution = Substitution.fromInterfaceType(typeToMock);
final substitution = Substitution.fromPairs([
...classToMock.typeParameters,
...?aliasedElement?.typeParameters,
], [
...typeToMock.typeArguments,
...?typeAlias?.typeArguments,
]);
final members =
inheritanceManager.getInterface(classToMock).map.values.map((member) {
return ExecutableMember.from2(member, substitution);
Expand Down
1 change: 1 addition & 0 deletions lib/src/mock.dart
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ class SmartFake {
// make Analyzer happy, if we fake classes that override `==` to
// accept `Object?` or `dynamic` (most notably [Interceptor]).
@override
// ignore: non_nullable_equals_parameter
bool operator ==(Object? other) => identical(this, other);

@override
Expand Down
Loading

0 comments on commit 09aabe7

Please sign in to comment.