| // 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 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: |
| /// * InterfaceTypes (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) { |
| var 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'; |
| } |
| |
| /// A [MockBuilder] instance for use by `build.yaml`. |
| Builder buildMocks(BuilderOptions options) => MockBuilder(); |