From c2c5357a25b3115705effc87b4e7fe0fb2a3a10e Mon Sep 17 00:00:00 2001 From: Nicholas Shahan Date: Fri, 14 Nov 2025 08:53:51 -0800 Subject: [PATCH] [front_end] Add option for expanded invalidation In this mode when an edit only touches the body of a mixin (not the public API) libraries that apply the mixin will also be invalidated. Add mixin invalidation test. Issue: https://github.com/dart-lang/sdk/issues/61864 Change-Id: Id4dde67364dcef958ae1e0e0bbf124cec389f5f0 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/459343 Commit-Queue: Nicholas Shahan Reviewed-by: Jens Johansen --- pkg/dev_compiler/lib/src/kernel/target.dart | 9 + .../lib/src/base/incremental_compiler.dart | 24 +++ .../dartdevc/mixin_collection/main.dart | 3 + .../mixin_collection/main.dart.strong.expect | 44 +++++ .../main.dart.strong.modular.expect | 44 +++++ .../main.dart.strong.outline.expect | 41 ++++ .../main.dart.strong.transformed.expect | 44 +++++ .../main.dart.textual_outline.expect | 3 + .../main.dart.textual_outline_modelled.expect | 3 + .../dartdevc/mixin_collection/main_lib1.dart | 3 + .../dartdevc/mixin_collection/main_lib2.dart | 9 + .../test/frontend_server_test.dart | 175 ++++++++++++++++++ pkg/kernel/lib/target/targets.dart | 8 + .../change_mixin_member_body/class.0.dart | 7 + .../change_mixin_member_body/main.0.dart | 17 ++ .../change_mixin_member_body/mixin.0.dart | 9 + .../change_mixin_member_body/mixin.1.dart | 20 ++ 17 files changed, 463 insertions(+) create mode 100644 pkg/front_end/testcases/dartdevc/mixin_collection/main.dart create mode 100644 pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.strong.expect create mode 100644 pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.strong.modular.expect create mode 100644 pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.strong.outline.expect create mode 100644 pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.strong.transformed.expect create mode 100644 pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.textual_outline.expect create mode 100644 pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.textual_outline_modelled.expect create mode 100644 pkg/front_end/testcases/dartdevc/mixin_collection/main_lib1.dart create mode 100644 pkg/front_end/testcases/dartdevc/mixin_collection/main_lib2.dart create mode 100644 tests/hot_reload/change_mixin_member_body/class.0.dart create mode 100644 tests/hot_reload/change_mixin_member_body/main.0.dart create mode 100644 tests/hot_reload/change_mixin_member_body/mixin.0.dart create mode 100644 tests/hot_reload/change_mixin_member_body/mixin.1.dart diff --git a/pkg/dev_compiler/lib/src/kernel/target.dart b/pkg/dev_compiler/lib/src/kernel/target.dart index b45b6208a7b..85cc2b695a6 100644 --- a/pkg/dev_compiler/lib/src/kernel/target.dart +++ b/pkg/dev_compiler/lib/src/kernel/target.dart @@ -328,6 +328,15 @@ class DevCompilerTarget extends Target { @override DartLibrarySupport get dartLibrarySupport => const DevCompilerDartLibrarySupport(); + + // For correctness the DDC runtime needs to reevaluate libraries that contain + // mixin applications when the mixin was edited. If the edit was only within + // the body of a mixin member the experimental invalidation logic would only + // invalidate that library. This enables a search for applications of the + // mixin in other libraries adds them to the invalidated set. + @override + bool get incrementalCompilerIncludeMixinApplicationInvalidatedLibraries => + true; } class DevCompilerDartLibrarySupport extends CustomizedDartLibrarySupport { diff --git a/pkg/front_end/lib/src/base/incremental_compiler.dart b/pkg/front_end/lib/src/base/incremental_compiler.dart index 000fbaffbcf..230fe5321cd 100644 --- a/pkg/front_end/lib/src/base/incremental_compiler.dart +++ b/pkg/front_end/lib/src/base/incremental_compiler.dart @@ -332,6 +332,10 @@ class IncrementalCompiler implements IncrementalKernelGenerator { reusedResult, c, uriTranslator, + context + .options + .target + .incrementalCompilerIncludeMixinApplicationInvalidatedLibraries, ); recorderForTesting?.recordRebuildBodiesCount( experimentalInvalidation?.missingSources.length ?? 0, @@ -528,6 +532,14 @@ class IncrementalCompiler implements IncrementalKernelGenerator { c, cleanedUpBuilders: cleanedUpBuilders, ); + if (experimentalInvalidation != null && + experimentalInvalidation.invalidatedMixinApplicationLibraries != + null) { + outputLibraries = { + ...outputLibraries, + ...experimentalInvalidation.invalidatedMixinApplicationLibraries!, + }.toList(); + } List problemsAsJson = _componentProblems.reissueProblems( context, currentKernelTarget, @@ -1118,6 +1130,7 @@ class IncrementalCompiler implements IncrementalKernelGenerator { ReusageResult reusedResult, CompilerContext c, UriTranslator uriTranslator, + bool collectMixinsToo, ) async { Set? rebuildBodies; Set originalNotReusedLibraries; @@ -1232,6 +1245,7 @@ class IncrementalCompiler implements IncrementalKernelGenerator { // procedures, if the changed file is used as a mixin anywhere else // we can't only recompile the changed file. // TODO(jensj): Check for mixins in a smarter and faster way. + Set? invalidatedMixinApplicationLibraries; if (!skipExperimentalInvalidationChecksForTesting) { for (LibraryBuilder builder in reusedResult.notReusedLibraries) { if (missingSources!.contains(builder.fileUri)) { @@ -1256,6 +1270,13 @@ class IncrementalCompiler implements IncrementalKernelGenerator { return null; } } + if (collectMixinsToo && + c.mixedInClass != null && + missingSources.contains(c.mixedInClass!.fileUri)) { + (invalidatedMixinApplicationLibraries ??= {}).add( + c.enclosingLibrary, + ); + } } } @@ -1324,6 +1345,7 @@ class IncrementalCompiler implements IncrementalKernelGenerator { rebuildBodies, originalNotReusedLibraries, missingSources, + invalidatedMixinApplicationLibraries, ); } @@ -2727,11 +2749,13 @@ class ExperimentalInvalidation { final Set rebuildBodies; final Set originalNotReusedLibraries; final Set missingSources; + final Set? invalidatedMixinApplicationLibraries; ExperimentalInvalidation( this.rebuildBodies, this.originalNotReusedLibraries, this.missingSources, + this.invalidatedMixinApplicationLibraries, ); } diff --git a/pkg/front_end/testcases/dartdevc/mixin_collection/main.dart b/pkg/front_end/testcases/dartdevc/mixin_collection/main.dart new file mode 100644 index 00000000000..e1d53332664 --- /dev/null +++ b/pkg/front_end/testcases/dartdevc/mixin_collection/main.dart @@ -0,0 +1,3 @@ +import 'main_lib1.dart'; + +C c = new C(); diff --git a/pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.strong.expect b/pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.strong.expect new file mode 100644 index 00000000000..cb71cdc4e41 --- /dev/null +++ b/pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.strong.expect @@ -0,0 +1,44 @@ +library; +import self as self; +import "main_lib1.dart" as mai; + +import "org-dartlang-testcase:///main_lib1.dart"; + +static field mai::C c = new mai::C::•(); + +library; +import self as mai; +import "dart:core" as core; +import "main_lib2.dart" as mai2; + +import "org-dartlang-testcase:///main_lib2.dart"; + +class C extends mai::_C&Object&M { + synthetic constructor •() → mai::C + : super mai::_C&Object&M::•() + ; + static synthetic method _#new#tearOff() → mai::C + return new mai::C::•(); +} +abstract class _C&Object&M = core::Object with mai2::M /*isAnonymousMixin,hasConstConstructor*/ { + const synthetic constructor •() → mai::_C&Object&M + : super core::Object::•() + ; + synthetic mixin-super-stub method m1() → core::String + return super.{mai2::M::m1}(); + synthetic mixin-super-stub method m2() → core::String + return super.{mai2::M::m2}(); +} + +library; +import self as mai2; +import "dart:core" as core; + +abstract class M extends core::Object /*isMixinDeclaration*/ { + method m1() → core::String { + return "hello"; + } + method m2() → core::String { + return "hello"; + } +} diff --git a/pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.strong.modular.expect b/pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.strong.modular.expect new file mode 100644 index 00000000000..cb71cdc4e41 --- /dev/null +++ b/pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.strong.modular.expect @@ -0,0 +1,44 @@ +library; +import self as self; +import "main_lib1.dart" as mai; + +import "org-dartlang-testcase:///main_lib1.dart"; + +static field mai::C c = new mai::C::•(); + +library; +import self as mai; +import "dart:core" as core; +import "main_lib2.dart" as mai2; + +import "org-dartlang-testcase:///main_lib2.dart"; + +class C extends mai::_C&Object&M { + synthetic constructor •() → mai::C + : super mai::_C&Object&M::•() + ; + static synthetic method _#new#tearOff() → mai::C + return new mai::C::•(); +} +abstract class _C&Object&M = core::Object with mai2::M /*isAnonymousMixin,hasConstConstructor*/ { + const synthetic constructor •() → mai::_C&Object&M + : super core::Object::•() + ; + synthetic mixin-super-stub method m1() → core::String + return super.{mai2::M::m1}(); + synthetic mixin-super-stub method m2() → core::String + return super.{mai2::M::m2}(); +} + +library; +import self as mai2; +import "dart:core" as core; + +abstract class M extends core::Object /*isMixinDeclaration*/ { + method m1() → core::String { + return "hello"; + } + method m2() → core::String { + return "hello"; + } +} diff --git a/pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.strong.outline.expect b/pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.strong.outline.expect new file mode 100644 index 00000000000..cc1ed7b43a0 --- /dev/null +++ b/pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.strong.outline.expect @@ -0,0 +1,41 @@ +library; +import self as self; +import "main_lib1.dart" as mai; + +import "org-dartlang-testcase:///main_lib1.dart"; + +static field mai::C c; + +library; +import self as mai; +import "dart:core" as core; +import "main_lib2.dart" as mai2; + +import "org-dartlang-testcase:///main_lib2.dart"; + +class C extends mai::_C&Object&M { + synthetic constructor •() → mai::C + ; + static synthetic method _#new#tearOff() → mai::C + return new mai::C::•(); +} +abstract class _C&Object&M = core::Object with mai2::M /*isAnonymousMixin,hasConstConstructor*/ { + const synthetic constructor •() → mai::_C&Object&M + : super core::Object::•() + ; + synthetic mixin-super-stub method m1() → core::String + return super.{mai2::M::m1}(); + synthetic mixin-super-stub method m2() → core::String + return super.{mai2::M::m2}(); +} + +library; +import self as mai2; +import "dart:core" as core; + +abstract class M extends core::Object /*isMixinDeclaration*/ { + method m1() → core::String + ; + method m2() → core::String + ; +} diff --git a/pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.strong.transformed.expect b/pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.strong.transformed.expect new file mode 100644 index 00000000000..cb71cdc4e41 --- /dev/null +++ b/pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.strong.transformed.expect @@ -0,0 +1,44 @@ +library; +import self as self; +import "main_lib1.dart" as mai; + +import "org-dartlang-testcase:///main_lib1.dart"; + +static field mai::C c = new mai::C::•(); + +library; +import self as mai; +import "dart:core" as core; +import "main_lib2.dart" as mai2; + +import "org-dartlang-testcase:///main_lib2.dart"; + +class C extends mai::_C&Object&M { + synthetic constructor •() → mai::C + : super mai::_C&Object&M::•() + ; + static synthetic method _#new#tearOff() → mai::C + return new mai::C::•(); +} +abstract class _C&Object&M = core::Object with mai2::M /*isAnonymousMixin,hasConstConstructor*/ { + const synthetic constructor •() → mai::_C&Object&M + : super core::Object::•() + ; + synthetic mixin-super-stub method m1() → core::String + return super.{mai2::M::m1}(); + synthetic mixin-super-stub method m2() → core::String + return super.{mai2::M::m2}(); +} + +library; +import self as mai2; +import "dart:core" as core; + +abstract class M extends core::Object /*isMixinDeclaration*/ { + method m1() → core::String { + return "hello"; + } + method m2() → core::String { + return "hello"; + } +} diff --git a/pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.textual_outline.expect b/pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.textual_outline.expect new file mode 100644 index 00000000000..e1d53332664 --- /dev/null +++ b/pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.textual_outline.expect @@ -0,0 +1,3 @@ +import 'main_lib1.dart'; + +C c = new C(); diff --git a/pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.textual_outline_modelled.expect b/pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.textual_outline_modelled.expect new file mode 100644 index 00000000000..e1d53332664 --- /dev/null +++ b/pkg/front_end/testcases/dartdevc/mixin_collection/main.dart.textual_outline_modelled.expect @@ -0,0 +1,3 @@ +import 'main_lib1.dart'; + +C c = new C(); diff --git a/pkg/front_end/testcases/dartdevc/mixin_collection/main_lib1.dart b/pkg/front_end/testcases/dartdevc/mixin_collection/main_lib1.dart new file mode 100644 index 00000000000..2abd8d6940b --- /dev/null +++ b/pkg/front_end/testcases/dartdevc/mixin_collection/main_lib1.dart @@ -0,0 +1,3 @@ +import 'main_lib2.dart'; + +class C with M {} diff --git a/pkg/front_end/testcases/dartdevc/mixin_collection/main_lib2.dart b/pkg/front_end/testcases/dartdevc/mixin_collection/main_lib2.dart new file mode 100644 index 00000000000..e29fa79f9eb --- /dev/null +++ b/pkg/front_end/testcases/dartdevc/mixin_collection/main_lib2.dart @@ -0,0 +1,9 @@ +mixin M { + String m1() { + return 'hello'; + } + + String m2() { + return 'hello'; + } +} diff --git a/pkg/frontend_server/test/frontend_server_test.dart b/pkg/frontend_server/test/frontend_server_test.dart index 4c350c5117b..204f24e7d99 100644 --- a/pkg/frontend_server/test/frontend_server_test.dart +++ b/pkg/frontend_server/test/frontend_server_test.dart @@ -2783,6 +2783,181 @@ e() { }); }); + group('changes to mixin body invalidate libraries that apply the mixin', + () { + Future runTests( + {required String moduleFormat, bool canary = false}) async { + new File('${tempDir.path}/main.dart') + ..createSync() + ..writeAsStringSync("import 'class2.dart';" + "import 'helper.dart';" + "final h = new Helper();" + "final c = new C2();" + "main() {" + " print(c.fn1());" + " print(c.gn1());" + " print(c.fn2());" + " print(c.gn2());" + " print(h.x());" + "}"); + new File('${tempDir.path}/helper.dart') + ..createSync() + ..writeAsStringSync( + "class Helper { String x() { return 'Helper'; } }"); + new File('${tempDir.path}/class2.dart') + ..createSync() + ..writeAsStringSync("import 'mixin2.dart';\n" + "import 'class1.dart';\n" + "class C2 extends C1 with M2 {}\n"); + new File('${tempDir.path}/class1.dart') + ..createSync() + ..writeAsStringSync("import 'mixin1.dart';\n" + "class C1 with M1 {}\n"); + new File('${tempDir.path}/mixin1.dart') + ..createSync() + ..writeAsStringSync("mixin M1 {\n" + " String fn1() { return 'hello'; }\n" + " String gn1() => 'hello';\n" + "}\n"); + new File('${tempDir.path}/mixin2.dart') + ..createSync() + ..writeAsStringSync("mixin M2 {\n" + " String fn2() { return 'hello'; }\n" + " String gn2() => 'hello';\n" + "}\n"); + File packageConfig = + new File('${tempDir.path}/.dart_tool/package_config.json') + ..createSync(recursive: true) + ..writeAsStringSync('{\n' + ' "configVersion": 2,\n' + ' "packages": [\n' + ' {\n' + ' "name": "hello",\n' + ' "rootUri": "../",\n' + ' "packageUri": "./"\n' + ' }\n' + ' ]\n' + '}\n'); + final String entrypoint = 'package:hello/main.dart'; + final File dillFile = new File('${tempDir.path}/out.dill'); + final List args = [ + '--sdk-root=${sdkRoot.toFilePath()}', + '--incremental', + '--platform=${ddcPlatformKernel.path}', + '--output-dill=${dillFile.path}', + '--target=dartdevc', + '--dartdevc-module-format=$moduleFormat', + if (canary) '--dartdevc-canary', + '--packages=${packageConfig.path}', + ]; + final FrontendServer frontendServer = new FrontendServer(); + final Future result = frontendServer.open(args); + frontendServer.compile(entrypoint); + int count = 0; + final Completer expectationCompleter = new Completer(); + frontendServer.listen((Result compiledResult) { + if (count == 0) { + compiledResult.expectNoErrors(filename: dillFile.path); + // Should find all files in the output for the initial compile. + final File manifestFile = new File('${dillFile.path}.json'); + expect(manifestFile.existsSync(), true); + frontendServer.accept(); + count++; + final Map manifest = + json.decode(utf8.decode(manifestFile.readAsBytesSync())); + expect( + manifest.keys, + unorderedEquals([ + '/packages/hello/main.dart.lib.js', + '/packages/hello/helper.dart.lib.js', + '/packages/hello/class1.dart.lib.js', + '/packages/hello/mixin1.dart.lib.js', + '/packages/hello/class2.dart.lib.js', + '/packages/hello/mixin2.dart.lib.js', + ])); + // Modify the body expression of an arrow function in the M1 mixin. + final File mixinFile = new File('${tempDir.path}/mixin1.dart') + ..createSync() + ..writeAsStringSync("mixin M1 {\n" + " String fn1() { return 'hello'; }\n" + " String gn1() => 'goodbye';\n" + "}\n"); + + frontendServer.recompile(mixinFile.uri, entryPoint: entrypoint); + } else if (count == 1) { + final File dillIncFile = + new File('${dillFile.path}.incremental.dill'); + compiledResult.expectNoErrors(filename: dillIncFile.path); + frontendServer.accept(); + count++; + // Find four output files have been invalidated corresponding to the + // library that contains the modified mixin and the reverse imports + // back to the main entrypoint. + // + // This is a side effect of the arrow function body expressions + // being present in the incremental compilers "textual outline" so + // changes to the body will invalidate the "public" API. This is + // more than DDC needs for correctness. + final File manifestFile = new File('${dillIncFile.path}.json'); + final Map manifest = + json.decode(utf8.decode(manifestFile.readAsBytesSync())); + expect( + manifest.keys, + unorderedEquals([ + '/packages/hello/main.dart.lib.js', + '/packages/hello/class2.dart.lib.js', + '/packages/hello/class1.dart.lib.js', + '/packages/hello/mixin1.dart.lib.js' + ])); + // Modify an expression in the block body of a method in the mixin + // M1. + final File mixinFile = new File('${tempDir.path}/mixin1.dart') + ..createSync() + ..writeAsStringSync("mixin M1 {\n" + " String fn1() { return 'goodbye'; }\n" + " String gn1() => 'goodbye';\n" + "}\n"); + + frontendServer.recompile(mixinFile.uri, entryPoint: entrypoint); + } else if (count == 2) { + final File dillIncFile = + new File('${dillFile.path}.incremental.dill'); + compiledResult.expectNoErrors(filename: dillIncFile.path); + frontendServer.accept(); + count++; + // Find two output files have been invalidated corresponding to the + // library that contains the modified mixin and the library contains + // an application of the modified mixin. + // + // DDC needs to reevaluate the library with the mixin application to + // ensure the mixin method body is applied. + final File manifestFile = new File('${dillIncFile.path}.json'); + final Map manifest = + json.decode(utf8.decode(manifestFile.readAsBytesSync())); + expect( + manifest.keys, + unorderedEquals([ + '/packages/hello/class1.dart.lib.js', + '/packages/hello/mixin1.dart.lib.js' + ])); + frontendServer.quit(); + expectationCompleter.complete(true); + } + }); + expect(await result, 0); + await expectationCompleter.future; + frontendServer.close(); + } + + test('AMD module format', () async { + await runTests(moduleFormat: 'amd'); + }); + + test('DDC module format and canary', () async { + await runTests(moduleFormat: 'ddc', canary: true); + }); + }); + group('compile expression to JavaScript', () { Future runTests( {required String moduleFormat, bool canary = false}) async { diff --git a/pkg/kernel/lib/target/targets.dart b/pkg/kernel/lib/target/targets.dart index add0eba0640..6280aabab03 100644 --- a/pkg/kernel/lib/target/targets.dart +++ b/pkg/kernel/lib/target/targets.dart @@ -555,6 +555,14 @@ abstract class Target { /// Should this target-specific pragma be recognized by annotation parsers? bool isSupportedPragma(String pragmaName) => false; + + /// When `true` the incremental compiler will always include libraries that + /// apply invalidated mixins in the output of a recompile. + /// + /// They will be included even when only the mixin was edited, and even if the + /// invalidation was only within the body of the mixin member. + bool get incrementalCompilerIncludeMixinApplicationInvalidatedLibraries => + false; } class NoneConstantsBackend extends ConstantsBackend { diff --git a/tests/hot_reload/change_mixin_member_body/class.0.dart b/tests/hot_reload/change_mixin_member_body/class.0.dart new file mode 100644 index 00000000000..42bfd9549f7 --- /dev/null +++ b/tests/hot_reload/change_mixin_member_body/class.0.dart @@ -0,0 +1,7 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'mixin.dart'; + +class C with M {} diff --git a/tests/hot_reload/change_mixin_member_body/main.0.dart b/tests/hot_reload/change_mixin_member_body/main.0.dart new file mode 100644 index 00000000000..837ed4bea2e --- /dev/null +++ b/tests/hot_reload/change_mixin_member_body/main.0.dart @@ -0,0 +1,17 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:expect/expect.dart'; +import 'package:reload_test/reload_test_utils.dart'; + +import 'class.dart'; +import 'mixin.dart'; + +var c = C(); + +Future main() async { + Expect.equals('hello', c.fn()); + await hotReload(); + Expect.equals('goodbye', c.fn()); +} diff --git a/tests/hot_reload/change_mixin_member_body/mixin.0.dart b/tests/hot_reload/change_mixin_member_body/mixin.0.dart new file mode 100644 index 00000000000..8e66670864a --- /dev/null +++ b/tests/hot_reload/change_mixin_member_body/mixin.0.dart @@ -0,0 +1,9 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +mixin M { + String fn() { + return 'hello'; + } +} diff --git a/tests/hot_reload/change_mixin_member_body/mixin.1.dart b/tests/hot_reload/change_mixin_member_body/mixin.1.dart new file mode 100644 index 00000000000..9ca7035aeaa --- /dev/null +++ b/tests/hot_reload/change_mixin_member_body/mixin.1.dart @@ -0,0 +1,20 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +mixin M { + String fn() { + return 'goodbye'; + } +} + +/** DIFF **/ +/* + + mixin M { + String fn() { +- return 'hello'; ++ return 'goodbye'; + } + } +*/