Make MockBuilder support build_extensions option.

This is useful to change the destination of the generated files.
i.e: instead of having them on the same folder,
you can specify a diferent folder for the mocks.

Closes https://github.com/dart-lang/mockito/issues/545
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7da3cb8..4d1dba8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 * Require analyzer 5.12.0, allow analyzer version 6.x;
 * Add example of writing a class to mock function objects.
+* Add support for the `build_extensions` build.yaml option
 
 ## 5.4.2
 
diff --git a/FAQ.md b/FAQ.md
index 53ca021..f203360 100644
--- a/FAQ.md
+++ b/FAQ.md
@@ -167,3 +167,37 @@
 
 [`verify`]: https://pub.dev/documentation/mockito/latest/mockito/verify.html
 [`verifyInOrder`]: https://pub.dev/documentation/mockito/latest/mockito/verifyInOrder.html
+
+
+### How can I customize where Mockito outputs its mocks?
+
+Mockito supports configuration of outputs by the configuration provided by the `build`
+package by creating (if it doesn't exist already) the `build.yaml` at the root folder
+of the project.
+
+It uses the `build_extensions` option, which can be used to alter not only the output directory but you
+can also do other filename manipulation, eg.: append/prepend strings to the filename or add another extension
+to the filename.
+
+To use `build_extensions` you can use `^` on the input string to match on the project root, and `{{}}` to capture the remaining path/filename.
+
+You can also have multiple build_extensions options, but they can't conflict with each other.
+For consistency, the output pattern must always end with `.mocks.dart` and the input pattern must always end with `.dart`
+
+```yaml
+targets:
+  $default:
+    builders:
+      mockito|mockBuilder:
+        generate_for:
+        options:
+          # build_extensions takes a source pattern and if it matches it will transform the output
+          # to your desired path. The default behaviour is to the .mocks.dart file to be in the same
+          # directory as the source .dart file. As seen below this is customizable, but the generated
+          # file must always end in `.mocks.dart`. 
+          build_extensions:
+            '^tests/{{}}.dart' : 'tests/mocks/{{}}.mocks.dart' 
+            '^integration-tests/{{}}.dart' : 'integration-tests/{{}}.mocks.dart' 
+```
+
+Also, you can also check out the example configuration in the Mockito repository. 
diff --git a/build.yaml b/build.yaml
index 0b4fb40..1c44479 100644
--- a/build.yaml
+++ b/build.yaml
@@ -5,6 +5,13 @@
         generate_for:
           - example/**.dart
           - test/end2end/*.dart
+        options:
+          # build_extensions takes a source pattern and if it matches it will transform the output
+          # to your desired path. The default behaviour is to the .mocks.dart file to be in the same
+          # directory as the source .dart file. As seen below this is customizable, but the generated
+          # file must always end in `.mocks.dart`.
+          build_extensions:
+            '^example/build_extensions/{{}}.dart' : 'example/build_extensions/mocks/{{}}.mocks.dart'
 
 builders:
   mockBuilder:
diff --git a/example/build_extensions/example.dart b/example/build_extensions/example.dart
new file mode 100644
index 0000000..fde5ca4
--- /dev/null
+++ b/example/build_extensions/example.dart
@@ -0,0 +1,40 @@
+// Copyright 2023 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:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+import 'package:test_api/scaffolding.dart';
+
+// Because we customized the `build_extensions` option, we can output
+// the generated mocks in a diferent directory
+import 'mocks/example.mocks.dart';
+
+class Dog {
+  String sound() => "bark";
+  bool? eatFood(String? food) => true;
+  Future<void> chew() async => print('Chewing...');
+  int? walk(List<String>? places) => 1;
+}
+
+@GenerateNiceMocks([MockSpec<Dog>()])
+void main() {
+  test("Verify some dog behaviour", () async {
+    MockDog mockDog = MockDog();
+    when(mockDog.eatFood(any));
+
+    mockDog.eatFood("biscuits");
+
+    verify(mockDog.eatFood(any)).called(1);
+  });
+}
diff --git a/lib/src/builder.dart b/lib/src/builder.dart
index 19381dd..a9d53f7 100644
--- a/lib/src/builder.dart
+++ b/lib/src/builder.dart
@@ -63,18 +63,40 @@
 /// 'foo.mocks.dart' will be created.
 class MockBuilder implements Builder {
   @override
+  final Map<String, List<String>> buildExtensions = {
+    '.dart': ['.mocks.dart']
+  };
+
+  MockBuilder({Map<String, List<String>>? buildExtensions}) {
+    this.buildExtensions.addAll(buildExtensions ?? {});
+  }
+
+  @override
   Future<void> build(BuildStep buildStep) async {
     if (!await buildStep.resolver.isLibrary(buildStep.inputId)) return;
     final entryLib = await buildStep.inputLibrary;
     final sourceLibIsNonNullable = entryLib.isNonNullableByDefault;
-    final mockLibraryAsset = buildStep.inputId.changeExtension('.mocks.dart');
+
+    // While it can be acceptable that we get more than 2 allowedOutputs,
+    // because it's the general one and the user defined one. Having
+    // more, means that user has conflicting patterns so we should throw.
+    if (buildStep.allowedOutputs.length > 2) {
+      throw ArgumentError('Build_extensions has conflicting outputs on file '
+          '`${buildStep.inputId.path}`, it usually caused by missconfiguration '
+          'on your `build.yaml` file');
+    }
+    // if not single, we always choose the user defined one.
+    final mockLibraryAsset = buildStep.allowedOutputs.singleOrNull ??
+        buildStep.allowedOutputs
+            .where((element) =>
+                element != buildStep.inputId.changeExtension('.mocks.dart'))
+            .single;
     final inheritanceManager = InheritanceManager3();
     final mockTargetGatherer =
         _MockTargetGatherer(entryLib, inheritanceManager);
 
-    final entryAssetId = await buildStep.resolver.assetIdForElement(entryLib);
     final assetUris = await _resolveAssetUris(buildStep.resolver,
-        mockTargetGatherer._mockTargets, entryAssetId.path, entryLib);
+        mockTargetGatherer._mockTargets, mockLibraryAsset.path, entryLib);
 
     final mockLibraryInfo = _MockLibraryInfo(mockTargetGatherer._mockTargets,
         assetUris: assetUris,
@@ -240,11 +262,6 @@
     }
     return element.library!;
   }
-
-  @override
-  final buildExtensions = const {
-    '.dart': ['.mocks.dart']
-  };
 }
 
 /// An [Element] visitor which collects the elements of all of the
@@ -2304,7 +2321,29 @@
 }
 
 /// A [MockBuilder] instance for use by `build.yaml`.
-Builder buildMocks(BuilderOptions options) => MockBuilder();
+Builder buildMocks(BuilderOptions options) {
+  final buildExtensions = options.config['build_extensions'];
+  if (buildExtensions == null) return MockBuilder();
+  if (buildExtensions is! Map) {
+    throw ArgumentError(
+        'build_extensions should be a map from inputs to outputs');
+  }
+  final result = <String, List<String>>{};
+  for (final entry in buildExtensions.entries) {
+    final input = entry.key;
+    final output = entry.value;
+    if (input is! String || !input.endsWith('.dart')) {
+      throw ArgumentError('Invalid key in build_extensions `$input`, it '
+          'should be a string ending with `.dart`');
+    }
+    if (output is! String || !output.endsWith('.mocks.dart')) {
+      throw ArgumentError('Invalid key in build_extensions `$output`, it '
+          'should be a string ending with `mocks.dart`');
+    }
+    result[input] = [output];
+  }
+  return MockBuilder(buildExtensions: result);
+}
 
 extension on Element {
   /// Returns the "full name" of a class or method element.
diff --git a/test/builder/auto_mocks_test.dart b/test/builder/auto_mocks_test.dart
index 1a7b0e0..f3237cc 100644
--- a/test/builder/auto_mocks_test.dart
+++ b/test/builder/auto_mocks_test.dart
@@ -22,8 +22,6 @@
 import 'package:package_config/package_config.dart';
 import 'package:test/test.dart';
 
-Builder buildMocks(BuilderOptions options) => MockBuilder();
-
 const annotationsAsset = {
   'mockito|lib/annotations.dart': '''
 class GenerateMocks {
@@ -86,25 +84,27 @@
 
   /// Test [MockBuilder] in a package which has not opted into null safety.
   Future<void> testPreNonNullable(Map<String, String> sourceAssets,
-      {Map<String, /*String|Matcher<String>*/ Object>? outputs}) async {
+      {Map<String, /*String|Matcher<String>*/ Object>? outputs,
+      Map<String, dynamic> config = const <String, dynamic>{}}) async {
     final packageConfig = PackageConfig([
       Package('foo', Uri.file('/foo/'),
           packageUriRoot: Uri.file('/foo/lib/'),
           languageVersion: LanguageVersion(2, 7))
     ]);
-    await testBuilder(buildMocks(BuilderOptions({})), sourceAssets,
+    await testBuilder(buildMocks(BuilderOptions(config)), sourceAssets,
         writer: writer, outputs: outputs, packageConfig: packageConfig);
   }
 
   /// Test [MockBuilder] in a package which has opted into null safety.
   Future<void> testWithNonNullable(Map<String, String> sourceAssets,
-      {Map<String, /*String|Matcher<List<int>>*/ Object>? outputs}) async {
+      {Map<String, /*String|Matcher<List<int>>*/ Object>? outputs,
+      Map<String, dynamic> config = const <String, dynamic>{}}) async {
     final packageConfig = PackageConfig([
       Package('foo', Uri.file('/foo/'),
           packageUriRoot: Uri.file('/foo/lib/'),
           languageVersion: LanguageVersion(3, 0))
     ]);
-    await testBuilder(buildMocks(BuilderOptions({})), sourceAssets,
+    await testBuilder(buildMocks(BuilderOptions(config)), sourceAssets,
         writer: writer, outputs: outputs, packageConfig: packageConfig);
   }
 
@@ -3662,6 +3662,114 @@
               contains('bar: _FakeBar_0('))));
     });
   });
+
+  group('build_extensions support', () {
+    test('should export mocks to different directory', () async {
+      await testWithNonNullable({
+        ...annotationsAsset,
+        ...simpleTestAsset,
+        'foo|lib/foo.dart': '''
+        import 'bar.dart';
+        class Foo extends Bar {}
+        ''',
+        'foo|lib/bar.dart': '''
+        import 'dart:async';
+        class Bar {
+          m(Future<void> a) {}
+        }
+        ''',
+      }, config: {
+        "build_extensions": {"^test/{{}}.dart": "test/mocks/{{}}.mocks.dart"}
+      });
+      final mocksAsset = AssetId('foo', 'test/mocks/foo_test.mocks.dart');
+      final mocksContent = utf8.decode(writer.assets[mocksAsset]!);
+      expect(mocksContent, contains("import 'dart:async' as _i3;"));
+      expect(mocksContent, contains('m(_i3.Future<void>? a)'));
+    });
+
+    test('should throw if it has confilicting outputs', () async {
+      await expectLater(
+          testWithNonNullable({
+            ...annotationsAsset,
+            ...simpleTestAsset,
+            'foo|lib/foo.dart': '''
+        import 'bar.dart';
+        class Foo extends Bar {}
+        ''',
+            'foo|lib/bar.dart': '''
+        import 'dart:async';
+        class Bar {
+          m(Future<void> a) {}
+        }
+        ''',
+          }, config: {
+            "build_extensions": {
+              "^test/{{}}.dart": "test/mocks/{{}}.mocks.dart",
+              "test/{{}}.dart": "test/{{}}.something.mocks.dart"
+            }
+          }),
+          throwsArgumentError);
+      final mocksAsset = AssetId('foo', 'test/mocks/foo_test.mocks.dart');
+      final otherMocksAsset = AssetId('foo', 'test/mocks/foo_test.mocks.dart');
+      final somethingMocksAsset =
+          AssetId('foo', 'test/mocks/foo_test.something.mocks.dart');
+
+      expect(writer.assets.containsKey(mocksAsset), false);
+      expect(writer.assets.containsKey(otherMocksAsset), false);
+      expect(writer.assets.containsKey(somethingMocksAsset), false);
+    });
+
+    test('should throw if input is in incorrect format', () async {
+      await expectLater(
+          testWithNonNullable({
+            ...annotationsAsset,
+            ...simpleTestAsset,
+            'foo|lib/foo.dart': '''
+        import 'bar.dart';
+        class Foo extends Bar {}
+        ''',
+            'foo|lib/bar.dart': '''
+        import 'dart:async';
+        class Bar {
+          m(Future<void> a) {}
+        }
+        ''',
+          }, config: {
+            "build_extensions": {"^test/{{}}": "test/mocks/{{}}.mocks.dart"}
+          }),
+          throwsArgumentError);
+      final mocksAsset = AssetId('foo', 'test/mocks/foo_test.mocks.dart');
+      final mocksAssetOriginal = AssetId('foo', 'test/foo_test.mocks.dart');
+
+      expect(writer.assets.containsKey(mocksAsset), false);
+      expect(writer.assets.containsKey(mocksAssetOriginal), false);
+    });
+
+    test('should throw if output is in incorrect format', () async {
+      await expectLater(
+          testWithNonNullable({
+            ...annotationsAsset,
+            ...simpleTestAsset,
+            'foo|lib/foo.dart': '''
+        import 'bar.dart';
+        class Foo extends Bar {}
+        ''',
+            'foo|lib/bar.dart': '''
+        import 'dart:async';
+        class Bar {
+          m(Future<void> a) {}
+        }
+        ''',
+          }, config: {
+            "build_extensions": {"^test/{{}}.dart": "test/mocks/{{}}.g.dart"}
+          }),
+          throwsArgumentError);
+      final mocksAsset = AssetId('foo', 'test/mocks/foo_test.mocks.dart');
+      final mocksAssetOriginal = AssetId('foo', 'test/foo_test.mocks.dart');
+      expect(writer.assets.containsKey(mocksAsset), false);
+      expect(writer.assets.containsKey(mocksAssetOriginal), false);
+    });
+  });
 }
 
 TypeMatcher<List<int>> _containsAllOf(a, [b]) => decodedMatches(