blob: 2d38275a262ddc6d40f33faa9a6ce53e4390de68 [file] [log] [blame]
// Copyright 2019 Dart Mockito authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:analyzer/dart/constant/value.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart' as analyzer;
import 'package:build/build.dart';
import 'package:code_builder/code_builder.dart';
import 'package:dart_style/dart_style.dart';
/// For a source Dart library, generate the mocks referenced therein.
///
/// Given an input library, 'foo.dart', this builder will search the top-level
/// elements for an annotation, `@GenerateMocks`, from the mockito package. For
/// example:
///
/// ```dart
/// @GenerateMocks([Foo])
/// void main() {}
/// ```
///
/// If this builder finds any classes to mock (for example, `Foo`, above), it
/// will produce a "'.mocks.dart' file with such mocks. In this example,
/// 'foo.mocks.dart' will be created.
class MockBuilder implements Builder {
@override
Future build(BuildStep buildStep) async {
final entryLib = await buildStep.inputLibrary;
final resolver = buildStep.resolver;
final mockLibraryAsset = buildStep.inputId.changeExtension('.mocks.dart');
final classesToMock = <DartObject>[];
for (final element in entryLib.topLevelElements) {
final annotation = element.metadata.firstWhere(
(annotation) =>
annotation.element is ConstructorElement &&
annotation.element.enclosingElement.name == 'GenerateMocks',
orElse: () => null);
if (annotation == null) continue;
final generateMocksValue = annotation.computeConstantValue();
// TODO(srawlins): handle `generateMocksValue == null`?
final classesField = generateMocksValue.getField('classes');
if (classesField.isNull) {
throw InvalidMockitoAnnotationException(
'The "classes" argument has unknown types');
}
classesToMock.addAll(classesField.toListValue());
}
final mockLibrary = Library((b) {
var mockLibraryInfo = _MockLibraryInfo(classesToMock);
b.body.addAll(mockLibraryInfo.fakeClasses);
b.body.addAll(mockLibraryInfo.mockClasses);
});
if (mockLibrary.body.isEmpty) {
// Nothing to mock here!
return;
}
final emitter = DartEmitter.scoped();
final mockLibraryContent =
DartFormatter().format(mockLibrary.accept(emitter).toString());
await buildStep.writeAsString(mockLibraryAsset, mockLibraryContent);
}
@override
final buildExtensions = const {
'.dart': ['.mocks.dart']
};
}
class _MockLibraryInfo {
/// Mock classes to be added to the generated library.
final mockClasses = <Class>[];
/// Fake classes to be added to the library.
///
/// A fake class is only generated when it is needed for non-nullable return
/// values.
final fakeClasses = <Class>[];
/// [ClassElement]s which are used in non-nullable return types, for which
/// fake classes are added to the generated library.
final fakedClassElements = <ClassElement>[];
/// Build mock classes for [classesToMock], a list of classes obtained from a
/// `@GenerateMocks` annotation.
_MockLibraryInfo(List<DartObject> classesToMock) {
for (final classToMock in classesToMock) {
final dartTypeToMock = classToMock.toTypeValue();
if (dartTypeToMock == null) {
throw InvalidMockitoAnnotationException(
'The "classes" argument includes a non-type: $classToMock');
}
final elementToMock = dartTypeToMock.element;
if (elementToMock is ClassElement) {
if (elementToMock.isEnum) {
throw InvalidMockitoAnnotationException(
'The "classes" argument includes an enum: '
'${elementToMock.displayName}');
}
// TODO(srawlins): Catch when someone tries to generate mocks for an
// un-subtypable class, like bool, String, FutureOr, etc.
mockClasses.add(_buildMockClass(dartTypeToMock, elementToMock));
} else if (elementToMock is GenericFunctionTypeElement &&
elementToMock.enclosingElement is FunctionTypeAliasElement) {
throw InvalidMockitoAnnotationException(
'The "classes" argument includes a typedef: '
'${elementToMock.enclosingElement.displayName}');
} else {
throw InvalidMockitoAnnotationException(
'The "classes" argument includes a non-class: '
'${elementToMock.displayName}');
}
}
}
Class _buildMockClass(analyzer.DartType dartType, ClassElement classToMock) {
final className = dartType.displayName;
return Class((cBuilder) {
cBuilder
..name = 'Mock$className'
..extend = refer('Mock', 'package:mockito/mockito.dart')
..docs.add('/// A class which mocks [$className].')
..docs.add('///')
..docs.add('/// See the documentation for Mockito\'s code generation '
'for more information.');
// 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>[];
if (classToMock.typeParameters != null) {
for (var typeParameter in classToMock.typeParameters) {
cBuilder.types.add(_typeParameterReference(typeParameter));
typeArguments.add(refer(typeParameter.name));
}
}
cBuilder.implements.add(TypeReference((b) {
b
..symbol = dartType.name
..url = _typeImport(dartType)
..types.addAll(typeArguments);
}));
for (final field in classToMock.fields) {
if (field.isPrivate || field.isStatic) {
continue;
}
// Handle getters when we handle non-nullable return types.
final setter = field.setter;
if (setter != null) {
cBuilder.methods.add(
Method((mBuilder) => _buildOverridingSetter(mBuilder, setter)));
}
}
for (final method in classToMock.methods) {
if (method.isPrivate || method.isStatic) {
continue;
}
if (_returnTypeIsNonNullable(method) ||
_hasNonNullableParameter(method)) {
cBuilder.methods.add(
Method((mBuilder) => _buildOverridingMethod(mBuilder, method)));
}
}
});
}
// TODO(srawlins): Update this logic to correctly handle non-nullable return
// types. Right now this logic does not seem to be available on DartType.
bool _returnTypeIsNonNullable(MethodElement method) {
var type = method.returnType;
if (type.isDynamic || type.isVoid) return false;
if (method.isAsynchronous && type.isDartAsyncFuture ||
type.isDartAsyncFutureOr) {
var typeArgument = (type as analyzer.InterfaceType).typeArguments.first;
if (typeArgument.isDynamic || typeArgument.isVoid) {
// An asynchronous method which returns `Future<void>`, for example,
// does not need a dummy return value.
return false;
}
}
return true;
}
// TODO(srawlins): Update this logic to correctly handle non-nullable return
// types. Right now this logic does not seem to be available on DartType.
bool _hasNonNullableParameter(MethodElement method) =>
method.parameters.isNotEmpty;
/// Build a method which overrides [method], with all non-nullable
/// parameter types widened to be nullable.
///
/// This new method just calls `super.noSuchMethod`, optionally passing a
/// return value for methods with a non-nullable return type.
// TODO(srawlins): This method does no widening yet. Widen parameters. Include
// tests for typedefs, old-style function parameters, and function types.
// TODO(srawlins): This method declares no specific non-null return values
// yet.
void _buildOverridingMethod(MethodBuilder builder, MethodElement method) {
// TODO(srawlins): generator methods like async*, sync*.
var name = method.displayName;
if (method.isOperator) name = 'operator$name';
builder
..name = name
..returns = _typeReference(method.returnType);
if (method.typeParameters != null) {
builder.types.addAll(method.typeParameters.map(_typeParameterReference));
}
if (method.isAsynchronous) {
builder.modifier =
method.isGenerator ? MethodModifier.asyncStar : MethodModifier.async;
} else if (method.isGenerator) {
builder.modifier = MethodModifier.syncStar;
}
// These two variables store the arguments that will be passed to the
// [Invocation] built for `noSuchMethod`.
final invocationPositionalArgs = <Expression>[];
final invocationNamedArgs = <Expression, Expression>{};
for (final parameter in method.parameters) {
if (parameter.isRequiredPositional) {
builder.requiredParameters.add(_matchingParameter(parameter));
invocationPositionalArgs.add(refer(parameter.displayName));
} else if (parameter.isOptionalPositional) {
builder.optionalParameters.add(_matchingParameter(parameter));
invocationPositionalArgs.add(refer(parameter.displayName));
} else if (parameter.isNamed) {
builder.optionalParameters.add(_matchingParameter(parameter));
invocationNamedArgs[refer('#${parameter.displayName}')] =
refer(parameter.displayName);
}
}
// TODO(srawlins): Optionally pass a non-null return value to `noSuchMethod`
// which `Mock.noSuchMethod` will simply return, in order to satisfy runtime
// type checks.
// TODO(srawlins): Handle getter invocations with `Invocation.getter`,
// and operators???
final invocation = refer('Invocation').property('method').call([
refer('#${method.displayName}'),
literalList(invocationPositionalArgs),
if (invocationNamedArgs.isNotEmpty) literalMap(invocationNamedArgs),
]);
final noSuchMethodArgs = <Expression>[invocation];
if (_returnTypeIsNonNullable(method)) {
final dummyReturnValue = _dummyValue(method.returnType);
noSuchMethodArgs.add(dummyReturnValue);
}
final returnNoSuchMethod =
refer('super').property('noSuchMethod').call(noSuchMethodArgs);
builder.body = returnNoSuchMethod.code;
}
Expression _dummyValue(analyzer.DartType type) {
if (type.isDartCoreBool) {
return literalFalse;
} else if (type.isDartCoreDouble) {
return literalNum(0.0);
} else if (type.isDartAsyncFuture || type.isDartAsyncFutureOr) {
var typeArgument = (type as analyzer.InterfaceType).typeArguments.first;
return refer('Future')
.property('value')
.call([_dummyValue(typeArgument)]);
} else if (type.isDartCoreInt) {
return literalNum(0);
} else if (type.isDartCoreList) {
return literalList([]);
} else if (type.isDartCoreMap) {
return literalMap({});
} else if (type.isDartCoreNum) {
return literalNum(0);
} else if (type.isDartCoreSet) {
// This is perhaps a dangerous hack. The code, `{}`, is parsed as a Set
// literal if it is used in a context which explicitly expects a Set.
return literalMap({});
} else if (type.isDartCoreString) {
return literalString('');
} else {
// This class is unknown; we must likely generate a fake class, and return
// an instance here.
return _dummyValueImplementing(type);
}
}
Expression _dummyValueImplementing(analyzer.DartType dartType) {
// 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>[];
var elementToFake = dartType.element;
if (elementToFake is ClassElement) {
if (elementToFake.isEnum) {
return _typeReference(dartType).property(
elementToFake.fields.firstWhere((f) => f.isEnumConstant).name);
} else {
var fakeName = '_Fake${dartType.name}';
// Only make one fake class for each class that needs to be faked.
if (!fakedClassElements.contains(elementToFake)) {
fakeClasses.add(Class((cBuilder) {
cBuilder
..name = fakeName
..extend = refer('Fake', 'package:mockito/mockito.dart');
if (elementToFake.typeParameters != null) {
for (var typeParameter in elementToFake.typeParameters) {
cBuilder.types.add(_typeParameterReference(typeParameter));
typeArguments.add(refer(typeParameter.name));
}
}
cBuilder.implements.add(TypeReference((b) {
b
..symbol = dartType.name
..url = _typeImport(dartType)
..types.addAll(typeArguments);
}));
}));
fakedClassElements.add(elementToFake);
}
return refer(fakeName).newInstance([]);
}
} else if (dartType is analyzer.FunctionType) {
return Method((b) {
// The positional parameters in a FunctionType have no names. This
// counter lets us create unique dummy names.
var counter = 0;
for (final parameter in dartType.parameters) {
if (parameter.isRequiredPositional) {
b.requiredParameters
.add(_matchingParameter(parameter, defaultName: '__p$counter'));
counter++;
} else if (parameter.isOptionalPositional) {
b.optionalParameters
.add(_matchingParameter(parameter, defaultName: '__p$counter'));
counter++;
} else if (parameter.isNamed) {
b.optionalParameters.add(_matchingParameter(parameter));
}
}
if (dartType.returnType.isVoid) {
b.body = Code('');
} else {
b.body = _dummyValue(dartType.returnType).code;
}
}).closure;
}
// We shouldn't get here.
return literalNull;
}
/// Returns a [Parameter] which matches [parameter].
Parameter _matchingParameter(ParameterElement parameter,
{String defaultName}) {
var name = parameter.name?.isEmpty ?? false ? defaultName : parameter.name;
return Parameter((pBuilder) {
pBuilder
..name = name
..type = _typeReference(parameter.type);
if (parameter.isNamed) pBuilder.named = true;
if (parameter.defaultValueCode != null) {
pBuilder.defaultTo = Code(parameter.defaultValueCode);
}
});
}
/// Build a setter which overrides [setter], widening the single parameter
/// type to be nullable if it is non-nullable.
///
/// This new setter just calls `super.noSuchMethod`.
// TODO(srawlins): This method does no widening yet.
void _buildOverridingSetter(
MethodBuilder builder, PropertyAccessorElement setter) {
builder
..name = setter.displayName
..type = MethodType.setter;
final invocationPositionalArgs = <Expression>[];
// There should only be one required positional parameter. Should we assert
// on that? Leave it alone?
for (final parameter in setter.parameters) {
if (parameter.isRequiredPositional) {
builder.requiredParameters.add(Parameter((pBuilder) => pBuilder
..name = parameter.displayName
..type = _typeReference(parameter.type)));
invocationPositionalArgs.add(refer(parameter.displayName));
}
}
final invocation = refer('Invocation').property('setter').call([
refer('#${setter.displayName}'),
literalList(invocationPositionalArgs),
]);
final returnNoSuchMethod =
refer('super').property('noSuchMethod').call([invocation]);
builder.body = returnNoSuchMethod.code;
}
/// Create a reference for [typeParameter], properly referencing all types
/// in bounds.
TypeReference _typeParameterReference(TypeParameterElement typeParameter) {
return TypeReference((b) {
b.symbol = typeParameter.name;
if (typeParameter.bound != null) {
b.bound = _typeReference(typeParameter.bound);
}
});
}
/// Create a reference for [type], properly referencing all attached types.
///
/// This creates proper references for:
/// * [InterfaceType]s (classes, generic classes),
/// * FunctionType parameters (like `void callback(int i)`),
/// * type aliases (typedefs), both new- and old-style,
/// * enums.
// TODO(srawlins): Contribute this back to a common location, like
// package:source_gen?
Reference _typeReference(analyzer.DartType type) {
if (type is analyzer.InterfaceType) {
return TypeReference((TypeReferenceBuilder b) {
b
..symbol = type.name
..url = _typeImport(type)
..types.addAll(type.typeArguments.map(_typeReference));
});
} else if (type is analyzer.FunctionType) {
GenericFunctionTypeElement element = type.element;
if (element == null) {
// [type] represents a FunctionTypedFormalParameter.
return FunctionType((b) {
b
..returnType = _typeReference(type.returnType)
..requiredParameters
.addAll(type.normalParameterTypes.map(_typeReference))
..optionalParameters
.addAll(type.optionalParameterTypes.map(_typeReference));
for (var parameter in type.namedParameterTypes.entries) {
b.namedParameters[parameter.key] = _typeReference(parameter.value);
}
});
}
return TypeReference((TypeReferenceBuilder trBuilder) {
var typedef = element.enclosingElement;
trBuilder
..symbol = typedef.name
..url = _typeImport(type);
for (var typeArgument in type.typeArguments) {
trBuilder.types.add(_typeReference(typeArgument));
}
});
} else {
return refer(type.displayName, _typeImport(type));
}
}
/// Returns the import URL for [type].
///
/// For some types, like `dynamic`, this may return null.
String _typeImport(analyzer.DartType type) {
var library = type.element?.library;
// For types like `dynamic`, return null; no import needed.
if (library == null) return null;
// TODO(srawlins): See what other code generators do here to guarantee sane
// URIs.
return library.source.uri.toString();
}
}
/// An exception which is thrown when Mockito encounters an invalid annotation.
class InvalidMockitoAnnotationException implements Exception {
final String message;
InvalidMockitoAnnotationException(this.message);
@override
String toString() => 'Invalid @GenerateMocks annotation: $message';
}