Version 2.15.0-291.0.dev

Merge commit '9b7caf3b93856838dd51053effec3ff37d2c69a4' into 'dev'
diff --git a/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart b/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart
index a09c3b7..2a8a2ae 100644
--- a/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart
+++ b/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart
@@ -19,6 +19,7 @@
 import 'package:analyzer/file_system/file_system.dart';
 import 'package:analyzer/instrumentation/service.dart';
 import 'package:analyzer/source/error_processor.dart';
+import 'package:analyzer/source/source_range.dart';
 import 'package:analyzer/src/error/codes.dart';
 import 'package:analyzer/src/util/file_paths.dart' as file_paths;
 import 'package:analyzer_plugin/src/utilities/change_builder/change_builder_core.dart';
@@ -208,6 +209,7 @@
     final analysisOptions = unit.session.analysisContext.analysisOptions;
 
     var overrideSet = _readOverrideSet(unit);
+    var lockRanges = <String, List<SourceRange>>{};
     for (var error in errors) {
       final processor = ErrorProcessor.getProcessor(analysisOptions, error);
       // Only fix errors not filtered out in analysis options.
@@ -219,7 +221,7 @@
           error,
           (name) => [],
         );
-        await _fixSingleError(fixContext, unit, error, overrideSet);
+        await _fixSingleError(fixContext, unit, error, overrideSet, lockRanges);
       }
     }
 
@@ -236,6 +238,7 @@
       var errors = List.from(unitResult.errors, growable: false);
       errors.sort((a, b) => a.offset.compareTo(b.offset));
 
+      var lockRanges = <String, List<SourceRange>>{};
       for (var error in errors) {
         var processor = ErrorProcessor.getProcessor(analysisOptions, error);
         // Only fix errors not filtered out in analysis options.
@@ -247,7 +250,8 @@
             error,
             (name) => [],
           );
-          await _fixSingleError(fixContext, unitResult, error, overrideSet);
+          await _fixSingleError(
+              fixContext, unitResult, error, overrideSet, lockRanges);
         }
       }
     }
@@ -260,11 +264,13 @@
       DartFixContext fixContext,
       ResolvedUnitResult result,
       AnalysisError diagnostic,
-      TransformOverrideSet? overrideSet) async {
+      TransformOverrideSet? overrideSet,
+      Map<String, List<SourceRange>> lockRanges) async {
     var context = CorrectionProducerContext.create(
       applyingBulkFixes: true,
       dartFixContext: fixContext,
       diagnostic: diagnostic,
+      lockRanges: lockRanges,
       overrideSet: overrideSet,
       resolvedResult: result,
       selectionOffset: diagnostic.offset,
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/abstract_producer.dart b/pkg/analysis_server/lib/src/services/correction/dart/abstract_producer.dart
index 96389a0..c1d4638 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/abstract_producer.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/abstract_producer.dart
@@ -241,7 +241,7 @@
 
   /// A map keyed by lock names whose value is a list of the ranges for which a
   /// lock has already been acquired.
-  final Map<String, List<SourceRange>> _lockRanges = {};
+  final Map<String, List<SourceRange>> lockRanges;
 
   CorrectionProducerContext._({
     required this.resolvedResult,
@@ -253,6 +253,7 @@
     this.overrideSet,
     this.selectionOffset = -1,
     this.selectionLength = 0,
+    required this.lockRanges,
   })  : file = resolvedResult.path,
         session = resolvedResult.session,
         sessionHelper = AnalysisSessionHelper(resolvedResult.session),
@@ -276,6 +277,7 @@
     TransformOverrideSet? overrideSet,
     int selectionOffset = -1,
     int selectionLength = 0,
+    Map<String, List<SourceRange>>? lockRanges,
   }) {
     var selectionEnd = selectionOffset + selectionLength;
     var locator = NodeLocator(selectionOffset, selectionEnd);
@@ -292,6 +294,7 @@
       overrideSet: overrideSet,
       selectionOffset: selectionOffset,
       selectionLength: selectionLength,
+      lockRanges: lockRanges ?? {},
     );
   }
 }
@@ -470,7 +473,7 @@
   /// ensure this behavior by attempting to acquire a lock prior to creating any
   /// edits, and only create the edits if a lock could be acquired.
   bool acquireLockOnRange(String lockName, SourceRange range) {
-    var ranges = _context._lockRanges.putIfAbsent(lockName, () => []);
+    var ranges = _context.lockRanges.putIfAbsent(lockName, () => []);
     if (ranges.contains(range)) {
       return false;
     }
diff --git a/pkg/analysis_server/test/src/services/correction/fix/data_driven/data_driven_test_support.dart b/pkg/analysis_server/test/src/services/correction/fix/data_driven/data_driven_test_support.dart
index e26e0061..c464828 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/data_driven/data_driven_test_support.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/data_driven/data_driven_test_support.dart
@@ -2,6 +2,8 @@
 // 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:analysis_server/src/services/correction/bulk_fix_processor.dart';
+import 'package:analysis_server/src/services/correction/change_workspace.dart';
 import 'package:analysis_server/src/services/correction/dart/data_driven.dart';
 import 'package:analysis_server/src/services/correction/fix.dart';
 import 'package:analysis_server/src/services/correction/fix/data_driven/code_template.dart';
@@ -9,13 +11,72 @@
 import 'package:analysis_server/src/services/correction/fix/data_driven/transform_set.dart';
 import 'package:analysis_server/src/services/correction/fix/data_driven/transform_set_manager.dart';
 import 'package:analyzer/error/error.dart';
+import 'package:analyzer/src/dart/analysis/byte_store.dart';
 import 'package:analyzer/src/dart/error/hint_codes.dart';
+import 'package:analyzer/src/services/available_declarations.dart';
+import 'package:analyzer/src/test_utilities/platform.dart';
+import 'package:analyzer_plugin/protocol/protocol_common.dart' show SourceEdit;
 import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
+import 'package:test/test.dart';
 
+import '../../../../../utils/test_instrumentation_service.dart';
 import '../fix_processor.dart';
 
 /// A base class defining support for writing fix processor tests for
 /// data-driven fixes.
+abstract class DataDrivenBulkFixProcessorTest
+    extends DataDrivenFixProcessorTest {
+  /// Return `true` if this test uses config files.
+  bool get useConfigFiles => false;
+
+  /// The workspace in which fixes contributor operates.
+  @override
+  DartChangeWorkspace get workspace {
+    return DartChangeWorkspace([session]);
+  }
+
+  @override
+  Future<void> assertHasFix(String expected,
+      {bool Function(AnalysisError)? errorFilter,
+      int? length,
+      String? target,
+      int? expectedNumberOfFixesForKind,
+      String? matchFixMessage,
+      bool allowFixAllFixes = false}) async {
+    if (useLineEndingsForPlatform) {
+      expected = normalizeNewlinesForPlatform(expected);
+    }
+    var processor = await computeFixes();
+    change = processor.builder.sourceChange;
+
+    // apply to "file"
+    var fileEdits = change.edits;
+    expect(fileEdits, hasLength(1));
+
+    var fileContent = testCode;
+    if (target != null) {
+      expect(fileEdits.first.file, convertPath(target));
+      fileContent = getFile(target).readAsStringSync();
+    }
+
+    resultCode = SourceEdit.applySequence(fileContent, change.edits[0].edits);
+    expect(resultCode, expected);
+  }
+
+  /// Computes fixes for the specified [testUnit].
+  Future<BulkFixProcessor> computeFixes() async {
+    var tracker = DeclarationsTracker(MemoryByteStore(), resourceProvider);
+    var analysisContext = contextFor(testFile);
+    tracker.addContext(analysisContext);
+    var processor = BulkFixProcessor(TestInstrumentationService(), workspace,
+        useConfigFiles: useConfigFiles);
+    await processor.fixErrors([analysisContext]);
+    return processor;
+  }
+}
+
+/// A base class defining support for writing fix processor tests for
+/// data-driven fixes.
 abstract class DataDrivenFixProcessorTest extends FixProcessorTest {
   /// Return the URI used to import the library created by [setPackageContent].
   String get importUri => 'package:p/lib.dart';
diff --git a/pkg/analysis_server/test/src/services/correction/fix/data_driven/flutter_bulk_test.dart b/pkg/analysis_server/test/src/services/correction/fix/data_driven/flutter_bulk_test.dart
new file mode 100644
index 0000000..6e30841
--- /dev/null
+++ b/pkg/analysis_server/test/src/services/correction/fix/data_driven/flutter_bulk_test.dart
@@ -0,0 +1,178 @@
+// Copyright (c) 2021, 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:test_reflective_loader/test_reflective_loader.dart';
+
+import 'data_driven_test_support.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(FlutterBulkTest);
+  });
+}
+
+@reflectiveTest
+class FlutterBulkTest extends DataDrivenBulkFixProcessorTest {
+  Future<void>
+      test_material_ThemeData_textSelectionHandleColor_deprecated() async {
+    setPackageContent('''
+class ThemeData {
+  ThemeData({
+    @deprecated Color? textSelectionHandleColor,
+    @deprecated bool useTextSelectionTheme = false,
+    TextSelectionThemeData? textSelectionTheme}) {}
+}
+class TextSelectionThemeData {
+  TextSelectionThemeData({Color selectionHandleColor}) {}
+}
+class Color {}
+class Colors {
+  static Color yellow = Color();
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: "Migrate to 'TextSelectionThemeData'"
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      constructor: ''
+      inClass: 'ThemeData'
+    oneOf:
+      - if: "textSelectionColor != '' && cursorColor != '' && textSelectionHandleColor != ''"
+        changes:
+          - kind: 'addParameter'
+            index: 73
+            name: 'textSelectionTheme'
+            style: optional_named
+            argumentValue:
+              expression: 'TextSelectionThemeData(cursorColor: {% cursorColor %}, selectionColor: {% textSelectionColor %}, selectionHandleColor: {% textSelectionHandleColor %},)'
+              requiredIf: "textSelectionColor != '' && cursorColor != '' && textSelectionHandleColor != ''"
+          - kind: 'removeParameter'
+            name: 'textSelectionColor'
+          - kind: 'removeParameter'
+            name: 'cursorColor'
+          - kind: 'removeParameter'
+            name: 'textSelectionHandleColor'
+          - kind: 'removeParameter'
+            name: 'useTextSelectionTheme'
+      - if: "textSelectionColor == '' && cursorColor != '' && textSelectionHandleColor != ''"
+        changes:
+          - kind: 'addParameter'
+            index: 73
+            name: 'textSelectionTheme'
+            style: optional_named
+            argumentValue:
+              expression: 'TextSelectionThemeData(cursorColor: {% cursorColor %}, selectionHandleColor: {% textSelectionHandleColor %},)'
+              requiredIf: "textSelectionColor == '' && cursorColor != '' && textSelectionHandleColor != ''"
+          - kind: 'removeParameter'
+            name: 'cursorColor'
+          - kind: 'removeParameter'
+            name: 'textSelectionHandleColor'
+          - kind: 'removeParameter'
+            name: 'useTextSelectionTheme'
+      - if: "textSelectionColor != '' && cursorColor != '' && textSelectionHandleColor == ''"
+        changes:
+          - kind: 'addParameter'
+            index: 73
+            name: 'textSelectionTheme'
+            style: optional_named
+            argumentValue:
+              expression: 'TextSelectionThemeData(cursorColor: {% cursorColor %}, selectionColor: {% textSelectionColor %},)'
+              requiredIf: "textSelectionColor != '' && cursorColor != '' && textSelectionHandleColor == ''"
+          - kind: 'removeParameter'
+            name: 'textSelectionColor'
+          - kind: 'removeParameter'
+            name: 'cursorColor'
+          - kind: 'removeParameter'
+            name: 'useTextSelectionTheme'
+      - if: "textSelectionColor != '' && cursorColor == '' && textSelectionHandleColor != ''"
+        changes:
+          - kind: 'addParameter'
+            index: 73
+            name: 'textSelectionTheme'
+            style: optional_named
+            argumentValue:
+              expression: 'TextSelectionThemeData(selectionColor: {% textSelectionColor %}, selectionHandleColor: {% textSelectionHandleColor %},)'
+              requiredIf: "textSelectionColor != '' && cursorColor == '' && textSelectionHandleColor != ''"
+          - kind: 'removeParameter'
+            name: 'textSelectionColor'
+          - kind: 'removeParameter'
+            name: 'textSelectionHandleColor'
+          - kind: 'removeParameter'
+            name: 'useTextSelectionTheme'
+      - if: "textSelectionColor == '' && cursorColor != '' && textSelectionHandleColor == ''"
+        changes:
+          - kind: 'addParameter'
+            index: 73
+            name: 'textSelectionTheme'
+            style: optional_named
+            argumentValue:
+              expression: 'TextSelectionThemeData(cursorColor: {% cursorColor %})'
+              requiredIf: "textSelectionColor == '' && cursorColor != '' && textSelectionHandleColor == ''"
+          - kind: 'removeParameter'
+            name: 'cursorColor'
+          - kind: 'removeParameter'
+            name: 'useTextSelectionTheme'
+      - if: "textSelectionColor != '' && cursorColor == '' && textSelectionHandleColor == ''"
+        changes:
+          - kind: 'addParameter'
+            index: 73
+            name: 'textSelectionTheme'
+            style: optional_named
+            argumentValue:
+              expression: 'TextSelectionThemeData(selectionColor: {% textSelectionColor %})'
+              requiredIf: "textSelectionColor != '' && cursorColor == '' && textSelectionHandleColor == ''"
+          - kind: 'removeParameter'
+            name: 'textSelectionColor'
+          - kind: 'removeParameter'
+            name: 'useTextSelectionTheme'
+      - if: "textSelectionColor == '' && cursorColor == '' && textSelectionHandleColor != ''"
+        changes:
+          - kind: 'addParameter'
+            index: 73
+            name: 'textSelectionTheme'
+            style: optional_named
+            argumentValue:
+              expression: 'TextSelectionThemeData(selectionHandleColor: {% textSelectionHandleColor %})'
+              requiredIf: "textSelectionColor == '' && cursorColor == '' && textSelectionHandleColor != ''"
+          - kind: 'removeParameter'
+            name: 'textSelectionHandleColor'
+          - kind: 'removeParameter'
+            name: 'useTextSelectionTheme'
+      - if: "useTextSelectionTheme != ''"
+        changes:
+          - kind: 'removeParameter'
+            name: 'useTextSelectionTheme'
+    variables:
+      textSelectionColor:
+        kind: 'fragment'
+        value: 'arguments[textSelectionColor]'
+      cursorColor:
+        kind: 'fragment'
+        value: 'arguments[cursorColor]'
+      textSelectionHandleColor:
+        kind: 'fragment'
+        value: 'arguments[textSelectionHandleColor]'
+      useTextSelectionTheme:
+        kind: 'fragment'
+        value: 'arguments[useTextSelectionTheme]'
+''');
+    await resolveTestCode('''
+import '$importUri';
+
+void f() {
+  ThemeData(textSelectionHandleColor: Colors.yellow, useTextSelectionTheme: false);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f() {
+  ThemeData(textSelectionTheme: TextSelectionThemeData(selectionHandleColor: Colors.yellow));
+}
+''');
+  }
+}
diff --git a/pkg/analysis_server/test/src/services/correction/fix/data_driven/test_all.dart b/pkg/analysis_server/test/src/services/correction/fix/data_driven/test_all.dart
index 64cffa2..b2ced8a 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/data_driven/test_all.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/data_driven/test_all.dart
@@ -11,6 +11,7 @@
 import 'diagnostics/test_all.dart' as diagnostics;
 import 'element_matcher_test.dart' as element_matcher;
 import 'end_to_end_test.dart' as end_to_end;
+import 'flutter_bulk_test.dart' as flutter_bulk;
 import 'flutter_use_case_test.dart' as flutter_use_case;
 import 'modify_parameters_test.dart' as modify_parameters;
 import 'rename_parameter_test.dart' as rename_parameter;
@@ -30,6 +31,7 @@
     diagnostics.main();
     element_matcher.main();
     end_to_end.main();
+    flutter_bulk.main();
     flutter_use_case.main();
     modify_parameters.main();
     rename_parameter.main();
diff --git a/pkg/analysis_server/test/src/services/correction/fix/remove_argument_test.dart b/pkg/analysis_server/test/src/services/correction/fix/remove_argument_test.dart
index 736da8c..87759cd 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/remove_argument_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/remove_argument_test.dart
@@ -21,7 +21,7 @@
   @override
   String get lintCode => LintNames.avoid_redundant_argument_values;
 
-  Future<void> test_singleFile() async {
+  Future<void> test_independentInvocations() async {
     await resolveTestCode('''
 void f({bool valWithDefault = true, bool val}) {}
 void f2({bool valWithDefault = true, bool val}) {}
@@ -41,6 +41,23 @@
 }
 ''');
   }
+
+  Future<void> test_multipleInSingleInvocation() async {
+    await resolveTestCode('''
+void f() {
+  g(a: 0, b: 1, c: 2);
+}
+
+void g({int a = 0, int b = 1, int c = 2}) {}
+''');
+    await assertHasFix('''
+void f() {
+  g();
+}
+
+void g({int a = 0, int b = 1, int c = 2}) {}
+''');
+  }
 }
 
 @reflectiveTest
diff --git a/pkg/analyzer_plugin/lib/src/protocol/protocol_internal.dart b/pkg/analyzer_plugin/lib/src/protocol/protocol_internal.dart
index 777ccda..459974c 100644
--- a/pkg/analyzer_plugin/lib/src/protocol/protocol_internal.dart
+++ b/pkg/analyzer_plugin/lib/src/protocol/protocol_internal.dart
@@ -4,6 +4,7 @@
 
 import 'dart:collection';
 import 'dart:convert' hide JsonDecoder;
+import 'dart:math' as math;
 
 import 'package:analyzer_plugin/protocol/protocol.dart';
 import 'package:analyzer_plugin/protocol/protocol_common.dart';
@@ -28,6 +29,17 @@
 /// If the invariants can't be preserved, then a [ConflictingEditException] is
 /// thrown.
 void addEditForSource(SourceFileEdit sourceFileEdit, SourceEdit sourceEdit) {
+  /// If the [leftEdit] and the [rightEdit] can be merged, then merge them.
+  SourceEdit? _merge(SourceEdit leftEdit, SourceEdit rightEdit) {
+    assert(leftEdit.offset <= rightEdit.offset);
+    if (leftEdit.isDeletion && rightEdit.isDeletion) {
+      var offset = leftEdit.offset;
+      var end = math.max(leftEdit.end, rightEdit.end);
+      return SourceEdit(offset, end - offset, '');
+    }
+    return null;
+  }
+
   var edits = sourceFileEdit.edits;
   var length = edits.length;
   var index = 0;
@@ -39,7 +51,12 @@
     // The [previousEdit] has an offset that is strictly greater than the offset
     // of the [sourceEdit] so we only need to look at the end of the
     // [sourceEdit] to know whether they overlap.
-    if (sourceEdit.offset + sourceEdit.length > previousEdit.offset) {
+    if (sourceEdit.end > previousEdit.offset) {
+      var mergedEdit = _merge(sourceEdit, previousEdit);
+      if (mergedEdit != null) {
+        edits[index - 1] = mergedEdit;
+        return;
+      }
       throw ConflictingEditException(
           newEdit: sourceEdit, existingEdit: previousEdit);
     }
@@ -54,7 +71,12 @@
     if ((sourceEdit.offset == nextEdit.offset &&
             sourceEdit.length > 0 &&
             nextEdit.length > 0) ||
-        nextEdit.offset + nextEdit.length > sourceEdit.offset) {
+        nextEdit.end > sourceEdit.offset) {
+      var mergedEdit = _merge(nextEdit, sourceEdit);
+      if (mergedEdit != null) {
+        edits[index] = mergedEdit;
+        return;
+      }
       throw ConflictingEditException(
           newEdit: sourceEdit, existingEdit: nextEdit);
     }
@@ -468,3 +490,11 @@
   /// the given [id], where the request was received at the given [requestTime].
   Response toResponse(String id, int requestTime);
 }
+
+extension SourceEditExtensions on SourceEdit {
+  /// Return `true` if this source edit represents a deletion.
+  bool get isDeletion => replacement.isEmpty;
+
+  /// Return `true` if this source edit represents an insertion.
+  bool get isInsertion => length == 0;
+}
diff --git a/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_core_test.dart b/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_core_test.dart
index a571f04..6ed22f9 100644
--- a/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_core_test.dart
+++ b/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_core_test.dart
@@ -17,6 +17,7 @@
   defineReflectiveSuite(() {
     defineReflectiveTests(ChangeBuilderImplTest);
     defineReflectiveTests(EditBuilderImplTest);
+    defineReflectiveTests(FileEditBuilderImpl_ConflictingTest);
     defineReflectiveTests(FileEditBuilderImplTest);
     defineReflectiveTests(LinkedEditBuilderImplTest);
   });
@@ -297,6 +298,229 @@
   }
 }
 
+/// Tests that are specifically targeted at the handling of conflicting edits.
+@reflectiveTest
+class FileEditBuilderImpl_ConflictingTest extends AbstractChangeBuilderTest {
+  String path = '/test.dart';
+
+  Matcher get hasConflict => throwsA(isA<ConflictingEditException>());
+
+  Future<void> test_deletion_deletion_adjacent_left() async {
+    var firstOffset = 30;
+    var firstLength = 5;
+    var secondOffset = 23;
+    var secondLength = 7;
+    await builder.addGenericFileEdit(path, (builder) {
+      builder.addDeletion(SourceRange(firstOffset, firstLength));
+      builder.addDeletion(SourceRange(secondOffset, secondLength));
+    });
+    var edits = builder.sourceChange.edits[0].edits;
+    expect(edits, hasLength(2));
+    expect(edits[0].offset, firstOffset);
+    expect(edits[0].length, firstLength);
+    expect(edits[0].replacement, isEmpty);
+    expect(edits[1].offset, secondOffset);
+    expect(edits[1].length, secondLength);
+    expect(edits[1].replacement, isEmpty);
+  }
+
+  Future<void> test_deletion_deletion_adjacent_right() async {
+    var firstOffset = 23;
+    var firstLength = 7;
+    var secondOffset = 30;
+    var secondLength = 5;
+    await builder.addGenericFileEdit(path, (builder) {
+      builder.addDeletion(SourceRange(firstOffset, firstLength));
+      builder.addDeletion(SourceRange(secondOffset, secondLength));
+    });
+    var edits = builder.sourceChange.edits[0].edits;
+    expect(edits, hasLength(2));
+    expect(edits[0].offset, secondOffset);
+    expect(edits[0].length, secondLength);
+    expect(edits[0].replacement, isEmpty);
+    expect(edits[1].offset, firstOffset);
+    expect(edits[1].length, firstLength);
+    expect(edits[1].replacement, isEmpty);
+  }
+
+  Future<void> test_deletion_deletion_overlap_left() async {
+    var firstOffset = 27;
+    var firstLength = 8;
+    var secondOffset = 23;
+    var secondLength = 7;
+    await builder.addGenericFileEdit(path, (builder) {
+      builder.addDeletion(SourceRange(firstOffset, firstLength));
+      builder.addDeletion(SourceRange(secondOffset, secondLength));
+    });
+    var edits = builder.sourceChange.edits[0].edits;
+    expect(edits, hasLength(1));
+    expect(edits[0].offset, secondOffset);
+    expect(edits[0].length, firstOffset + firstLength - secondOffset);
+    expect(edits[0].replacement, isEmpty);
+  }
+
+  Future<void> test_deletion_deletion_overlap_right() async {
+    var firstOffset = 23;
+    var firstLength = 7;
+    var secondOffset = 27;
+    var secondLength = 8;
+    await builder.addGenericFileEdit(path, (builder) {
+      builder.addDeletion(SourceRange(firstOffset, firstLength));
+      builder.addDeletion(SourceRange(secondOffset, secondLength));
+    });
+    var edits = builder.sourceChange.edits[0].edits;
+    expect(edits, hasLength(1));
+    expect(edits[0].offset, firstOffset);
+    expect(edits[0].length, secondOffset + secondLength - firstOffset);
+    expect(edits[0].replacement, isEmpty);
+  }
+
+  Future<void> test_deletion_insertion_adjacent_left() async {
+    var deletionOffset = 23;
+    var deletionLength = 7;
+    var insertionOffset = 23;
+    var insertionText = 'x';
+    await builder.addGenericFileEdit(path, (builder) {
+      builder.addDeletion(SourceRange(deletionOffset, deletionLength));
+      expect(() {
+        builder.addSimpleInsertion(insertionOffset, insertionText);
+      }, hasConflict);
+    });
+    var edits = builder.sourceChange.edits[0].edits;
+    expect(edits, hasLength(1));
+    expect(edits[0].offset, deletionOffset);
+    expect(edits[0].length, deletionLength);
+    expect(edits[0].replacement, '');
+  }
+
+  Future<void> test_deletion_insertion_adjacent_right() async {
+    var deletionOffset = 23;
+    var deletionLength = 7;
+    var insertionOffset = 30;
+    var insertionText = 'x';
+    await builder.addGenericFileEdit(path, (builder) {
+      builder.addDeletion(SourceRange(deletionOffset, deletionLength));
+      builder.addSimpleInsertion(insertionOffset, insertionText);
+    });
+    var edits = builder.sourceChange.edits[0].edits;
+    expect(edits, hasLength(2));
+    expect(edits[0].offset, insertionOffset);
+    expect(edits[0].length, 0);
+    expect(edits[0].replacement, insertionText);
+    expect(edits[1].offset, deletionOffset);
+    expect(edits[1].length, deletionLength);
+    expect(edits[1].replacement, isEmpty);
+  }
+
+  Future<void> test_deletion_insertion_overlap() async {
+    var deletionOffset = 23;
+    var deletionLength = 7;
+    var insertionOffset = 26;
+    var insertionText = 'x';
+    await builder.addGenericFileEdit(path, (builder) {
+      builder.addDeletion(SourceRange(deletionOffset, deletionLength));
+      expect(() {
+        builder.addSimpleInsertion(insertionOffset, insertionText);
+      }, hasConflict);
+    });
+    var edits = builder.sourceChange.edits[0].edits;
+    expect(edits, hasLength(1));
+    expect(edits[0].offset, deletionOffset);
+    expect(edits[0].length, deletionLength);
+    expect(edits[0].replacement, '');
+  }
+
+  Future<void> test_insertion_deletion_adjacent_left() async {
+    var deletionOffset = 23;
+    var deletionLength = 7;
+    var insertionOffset = 23;
+    var insertionText = 'x';
+    await builder.addGenericFileEdit(path, (builder) {
+      builder.addSimpleInsertion(insertionOffset, insertionText);
+      builder.addDeletion(SourceRange(deletionOffset, deletionLength));
+    });
+    var edits = builder.sourceChange.edits[0].edits;
+    expect(edits, hasLength(2));
+    expect(edits[0].offset, deletionOffset);
+    expect(edits[0].length, deletionLength);
+    expect(edits[0].replacement, isEmpty);
+    expect(edits[1].offset, insertionOffset);
+    expect(edits[1].length, 0);
+    expect(edits[1].replacement, insertionText);
+  }
+
+  Future<void> test_insertion_deletion_adjacent_right() async {
+    var deletionOffset = 23;
+    var deletionLength = 7;
+    var insertionOffset = 30;
+    var insertionText = 'x';
+    await builder.addGenericFileEdit(path, (builder) {
+      builder.addSimpleInsertion(insertionOffset, insertionText);
+      builder.addDeletion(SourceRange(deletionOffset, deletionLength));
+    });
+    var edits = builder.sourceChange.edits[0].edits;
+    expect(edits, hasLength(2));
+    expect(edits[0].offset, insertionOffset);
+    expect(edits[0].length, 0);
+    expect(edits[0].replacement, insertionText);
+    expect(edits[1].offset, deletionOffset);
+    expect(edits[1].length, deletionLength);
+    expect(edits[1].replacement, isEmpty);
+  }
+
+  Future<void> test_insertion_deletion_overlap() async {
+    var deletionOffset = 23;
+    var deletionLength = 7;
+    var insertionOffset = 26;
+    var insertionText = 'x';
+    await builder.addGenericFileEdit(path, (builder) {
+      builder.addSimpleInsertion(insertionOffset, insertionText);
+      expect(() {
+        builder.addDeletion(SourceRange(deletionOffset, deletionLength));
+      }, hasConflict);
+    });
+    var edits = builder.sourceChange.edits[0].edits;
+    expect(edits, hasLength(1));
+    expect(edits[0].offset, insertionOffset);
+    expect(edits[0].length, 0);
+    expect(edits[0].replacement, insertionText);
+  }
+
+  Future<void> test_replacement_replacement_overlap_left() async {
+    var offset = 23;
+    var length = 7;
+    var text = 'x';
+    await builder.addGenericFileEdit(path, (builder) {
+      builder.addSimpleReplacement(SourceRange(offset, length), text);
+      expect(() {
+        builder.addSimpleReplacement(SourceRange(offset - 2, length), text);
+      }, hasConflict);
+    });
+    var edits = builder.sourceChange.edits[0].edits;
+    expect(edits, hasLength(1));
+    expect(edits[0].offset, offset);
+    expect(edits[0].length, length);
+    expect(edits[0].replacement, text);
+  }
+
+  Future<void> test_replacement_replacement_overlap_right() async {
+    var offset = 23;
+    var length = 7;
+    var text = 'x';
+    await builder.addGenericFileEdit(path, (builder) {
+      builder.addSimpleReplacement(SourceRange(offset, length), text);
+      expect(() {
+        builder.addSimpleReplacement(SourceRange(offset + 2, length), text);
+      }, hasConflict);
+    });
+    var edits = builder.sourceChange.edits[0].edits;
+    expect(edits, hasLength(1));
+    expect(edits[0].offset, offset);
+    expect(edits[0].length, length);
+    expect(edits[0].replacement, text);
+  }
+}
+
 @reflectiveTest
 class FileEditBuilderImplTest extends AbstractChangeBuilderTest {
   String path = '/test.dart';
@@ -314,68 +538,6 @@
     expect(edits[0].replacement, isEmpty);
   }
 
-  Future<void> test_addDeletion_adjacent_lowerOffsetFirst() async {
-    // TODO(brianwilkerson) This should also merge the deletions, but is written
-    //  to ensure that existing uses of FileEditBuilder continue to work even
-    //  without that change.
-    var firstOffset = 23;
-    var firstLength = 7;
-    var secondOffset = 30;
-    var secondLength = 5;
-    await builder.addGenericFileEdit(path, (builder) {
-      builder.addDeletion(SourceRange(firstOffset, firstLength));
-      builder.addDeletion(SourceRange(secondOffset, secondLength));
-    });
-    var edits = builder.sourceChange.edits[0].edits;
-    expect(edits, hasLength(2));
-    expect(edits[0].offset, secondOffset);
-    expect(edits[0].length, secondLength);
-    expect(edits[0].replacement, isEmpty);
-    expect(edits[1].offset, firstOffset);
-    expect(edits[1].length, firstLength);
-    expect(edits[1].replacement, isEmpty);
-  }
-
-  Future<void> test_addDeletion_adjacent_lowerOffsetSecond() async {
-    // TODO(brianwilkerson) This should also merge the deletions, but is written
-    //  to ensure that existing uses of FileEditBuilder continue to work even
-    //  without that change.
-    var firstOffset = 23;
-    var firstLength = 7;
-    var secondOffset = 30;
-    var secondLength = 5;
-    await builder.addGenericFileEdit(path, (builder) {
-      builder.addDeletion(SourceRange(secondOffset, secondLength));
-      builder.addDeletion(SourceRange(firstOffset, firstLength));
-    });
-    var edits = builder.sourceChange.edits[0].edits;
-    expect(edits, hasLength(2));
-    expect(edits[0].offset, secondOffset);
-    expect(edits[0].length, secondLength);
-    expect(edits[0].replacement, isEmpty);
-    expect(edits[1].offset, firstOffset);
-    expect(edits[1].length, firstLength);
-    expect(edits[1].replacement, isEmpty);
-  }
-
-  @failingTest
-  Future<void> test_addDeletion_overlapping() async {
-    // This support is not yet implemented.
-    var firstOffset = 23;
-    var firstLength = 7;
-    var secondOffset = 27;
-    var secondLength = 8;
-    await builder.addGenericFileEdit(path, (builder) {
-      builder.addDeletion(SourceRange(firstOffset, firstLength));
-      builder.addDeletion(SourceRange(secondOffset, secondLength));
-    });
-    var edits = builder.sourceChange.edits[0].edits;
-    expect(edits, hasLength(1));
-    expect(edits[0].offset, firstOffset);
-    expect(edits[0].length, secondOffset + secondLength - firstOffset);
-    expect(edits[0].replacement, isEmpty);
-  }
-
   Future<void> test_addInsertion() async {
     await builder.addGenericFileEdit(path, (builder) {
       builder.addInsertion(10, (builder) {
@@ -472,40 +634,6 @@
     expect(edits[1].replacement, text);
   }
 
-  Future<void> test_addSimpleReplacement_overlapsHead() async {
-    var offset = 23;
-    var length = 7;
-    var text = 'xyz';
-    await builder.addGenericFileEdit(path, (builder) {
-      builder.addSimpleReplacement(SourceRange(offset, length), text);
-      expect(() {
-        builder.addSimpleReplacement(SourceRange(offset - 2, length), text);
-      }, throwsA(isA<ConflictingEditException>()));
-    });
-    var edits = builder.sourceChange.edits[0].edits;
-    expect(edits, hasLength(1));
-    expect(edits[0].offset, offset);
-    expect(edits[0].length, length);
-    expect(edits[0].replacement, text);
-  }
-
-  Future<void> test_addSimpleReplacement_overlapsTail() async {
-    var offset = 23;
-    var length = 7;
-    var text = 'xyz';
-    await builder.addGenericFileEdit(path, (builder) {
-      builder.addSimpleReplacement(SourceRange(offset, length), text);
-      expect(() {
-        builder.addSimpleReplacement(SourceRange(offset + 2, length), text);
-      }, throwsA(isA<ConflictingEditException>()));
-    });
-    var edits = builder.sourceChange.edits[0].edits;
-    expect(edits, hasLength(1));
-    expect(edits[0].offset, offset);
-    expect(edits[0].length, length);
-    expect(edits[0].replacement, text);
-  }
-
   Future<void> test_createEditBuilder() async {
     await builder.addGenericFileEdit(path, (builder) {
       var offset = 4;
diff --git a/runtime/lib/isolate.cc b/runtime/lib/isolate.cc
index 3569eb9..f3bb2bd 100644
--- a/runtime/lib/isolate.cc
+++ b/runtime/lib/isolate.cc
@@ -111,9 +111,8 @@
   // We have to check whether the reciever has the same isolate group (e.g.
   // native message handlers such as an IOService handler does not but does
   // share the same origin port).
-  const bool same_group =
-      FLAG_enable_isolate_groups && PortMap::IsReceiverInThisIsolateGroup(
-                                        destination_port_id, isolate->group());
+  const bool same_group = PortMap::IsReceiverInThisIsolateGroup(
+      destination_port_id, isolate->group());
   // TODO(turnidge): Throw an exception when the return value is false?
   PortMap::PostMessage(WriteMessage(can_send_any_object, same_group, obj,
                                     destination_port_id,
@@ -639,7 +638,7 @@
     ASSERT(name != nullptr);
 
     auto group = state_->isolate_group();
-    if (!FLAG_enable_isolate_groups || group == nullptr) {
+    if (group == nullptr) {
       RunHeavyweight(name);
     } else {
       RunLightweight(name);
@@ -676,8 +675,7 @@
   }
 
   void RunLightweight(const char* name) {
-    // The create isolate initialize callback is mandatory if
-    // --enable-isolate-groups was passed.
+    // The create isolate initialize callback is mandatory.
     auto initialize_callback = Isolate::InitializeCallback();
     if (initialize_callback == nullptr) {
       FailedSpawn(
@@ -934,22 +932,16 @@
   const auto& func = Function::Handle(zone, GetTopLevelFunction(zone, closure));
   PersistentHandle* closure_tuple_handle = nullptr;
   if (func.IsNull()) {
-    if (!FLAG_enable_isolate_groups) {
-      const String& msg = String::Handle(String::New(
-          "Isolate.spawn expects to be passed a static or top-level function"));
-      Exceptions::ThrowArgumentError(msg);
-    } else {
-      // We have a non-toplevel closure that we might need to copy.
-      // Result will be [<closure-copy>, <objects-in-msg-to-rehash>]
-      const auto& closure_copy_tuple = Object::Handle(
-          zone, CopyMutableObjectGraph(closure));  // Throws if it fails.
-      ASSERT(closure_copy_tuple.IsArray());
-      ASSERT(Object::Handle(zone, Array::Cast(closure_copy_tuple).At(0))
-                 .IsClosure());
-      closure_tuple_handle =
-          isolate->group()->api_state()->AllocatePersistentHandle();
-      closure_tuple_handle->set_ptr(closure_copy_tuple.ptr());
-    }
+    // We have a non-toplevel closure that we might need to copy.
+    // Result will be [<closure-copy>, <objects-in-msg-to-rehash>]
+    const auto& closure_copy_tuple = Object::Handle(
+        zone, CopyMutableObjectGraph(closure));  // Throws if it fails.
+    ASSERT(closure_copy_tuple.IsArray());
+    ASSERT(Object::Handle(zone, Array::Cast(closure_copy_tuple).At(0))
+               .IsClosure());
+    closure_tuple_handle =
+        isolate->group()->api_state()->AllocatePersistentHandle();
+    closure_tuple_handle->set_ptr(closure_copy_tuple.ptr());
   }
 
   bool fatal_errors = fatalErrors.IsNull() ? true : fatalErrors.value();
@@ -960,9 +952,8 @@
   // serializable this will throw an exception.
   SerializedObjectBuffer message_buffer;
   message_buffer.set_message(WriteMessage(
-      /* can_send_any_object */ true,
-      /* same_group */ FLAG_enable_isolate_groups, message, ILLEGAL_PORT,
-      Message::kNormalPriority));
+      /*can_send_any_object=*/true,
+      /*same_group=*/true, message, ILLEGAL_PORT, Message::kNormalPriority));
 
   const char* utf8_package_config =
       packageConfig.IsNull() ? NULL : String2UTF8(packageConfig);
diff --git a/runtime/vm/compiler/frontend/kernel_binary_flowgraph.cc b/runtime/vm/compiler/frontend/kernel_binary_flowgraph.cc
index 798174d..6bf2d87 100644
--- a/runtime/vm/compiler/frontend/kernel_binary_flowgraph.cc
+++ b/runtime/vm/compiler/frontend/kernel_binary_flowgraph.cc
@@ -4869,17 +4869,28 @@
 
   if (instructions.is_open()) {
     if (inside_try_finally) {
-      ASSERT(scopes()->finally_return_variable != NULL);
+      LocalVariable* const finally_return_variable =
+          scopes()->finally_return_variable;
+      ASSERT(finally_return_variable != nullptr);
       const Function& function = parsed_function()->function();
       if (NeedsDebugStepCheck(function, position)) {
         instructions += DebugStepCheck(position);
       }
-      instructions += StoreLocal(position, scopes()->finally_return_variable);
+      instructions += StoreLocal(position, finally_return_variable);
       instructions += Drop();
-      instructions += TranslateFinallyFinalizers(NULL, -1);
+      const intptr_t target_context_depth =
+          finally_return_variable->is_captured()
+              ? finally_return_variable->owner()->context_level()
+              : -1;
+      instructions += TranslateFinallyFinalizers(nullptr, target_context_depth);
       if (instructions.is_open()) {
-        instructions += LoadLocal(scopes()->finally_return_variable);
+        const intptr_t saved_context_depth = B->context_depth_;
+        if (finally_return_variable->is_captured()) {
+          B->context_depth_ = target_context_depth;
+        }
+        instructions += LoadLocal(finally_return_variable);
         instructions += Return(TokenPosition::kNoSource);
+        B->context_depth_ = saved_context_depth;
       }
     } else {
       instructions += Return(position);
diff --git a/runtime/vm/compiler/jit/compiler.cc b/runtime/vm/compiler/jit/compiler.cc
index 1ec2adc..5b08899 100644
--- a/runtime/vm/compiler/jit/compiler.cc
+++ b/runtime/vm/compiler/jit/compiler.cc
@@ -211,7 +211,7 @@
   ASSERT(thread->IsMutatorThread());
   const Function& function = Function::CheckedHandle(zone, arguments.ArgAt(0));
 
-  if (FLAG_enable_isolate_groups) {
+  {
     // Another isolate's mutator thread may have created [function] and
     // published it via an ICData, MegamorphicCache etc. Entering the lock below
     // is an acquire operation that pairs with the release operation when the
@@ -220,12 +220,6 @@
     SafepointReadRwLocker ml(thread, thread->isolate_group()->program_lock());
   }
 
-  // In single-isolate scenarios the lazy compile stub is only invoked if
-  // there's no existing code. In multi-isolate scenarios with shared JITed code
-  // we can end up in the lazy compile runtime entry here with code being
-  // installed.
-  ASSERT(!function.HasCode() || FLAG_enable_isolate_groups);
-
   // Will throw if compilation failed (e.g. with compile-time error).
   function.EnsureHasCode();
 }
diff --git a/runtime/vm/dart_api_impl.cc b/runtime/vm/dart_api_impl.cc
index 1c7026a..88dc441 100644
--- a/runtime/vm/dart_api_impl.cc
+++ b/runtime/vm/dart_api_impl.cc
@@ -1463,13 +1463,6 @@
 
   *error = nullptr;
 
-  if (!FLAG_enable_isolate_groups) {
-    *error = Utils::StrDup(
-        "Lightweight isolates need to be explicitly enabled by passing "
-        "--enable-isolate-groups.");
-    return nullptr;
-  }
-
   Isolate* isolate;
   isolate = CreateWithinExistingIsolateGroup(member->group(), name, error);
   if (isolate != nullptr) {
diff --git a/runtime/vm/flag_list.h b/runtime/vm/flag_list.h
index 224a6b1..eccc2ac 100644
--- a/runtime/vm/flag_list.h
+++ b/runtime/vm/flag_list.h
@@ -194,7 +194,6 @@
   P(retain_code_objects, bool, true,                                           \
     "Serialize all code objects even if not otherwise "                        \
     "needed in the precompiled runtime.")                                      \
-  P(enable_isolate_groups, bool, true, "Enable isolate group support.")        \
   P(show_invisible_frames, bool, false,                                        \
     "Show invisible frames in stack traces.")                                  \
   D(trace_cha, bool, false, "Trace CHA operations")                            \
diff --git a/runtime/vm/heap/heap_test.cc b/runtime/vm/heap/heap_test.cc
index b30bf66..c45765c 100644
--- a/runtime/vm/heap/heap_test.cc
+++ b/runtime/vm/heap/heap_test.cc
@@ -574,9 +574,6 @@
 };
 
 VM_UNIT_TEST_CASE(CleanupBequestNeverReceived) {
-  // This test uses features from isolate groups
-  FLAG_enable_isolate_groups = true;
-
   const char* TEST_MESSAGE = "hello, world";
   Dart_Isolate parent = TestCase::CreateTestIsolate("parent");
   EXPECT_EQ(parent, Dart_CurrentIsolate());
@@ -608,9 +605,6 @@
 }
 
 VM_UNIT_TEST_CASE(ReceivesSendAndExitMessage) {
-  // This test uses features from isolate groups
-  FLAG_enable_isolate_groups = true;
-
   const char* TEST_MESSAGE = "hello, world";
   Dart_Isolate parent = TestCase::CreateTestIsolate("parent");
   EXPECT_EQ(parent, Dart_CurrentIsolate());
diff --git a/runtime/vm/isolate.cc b/runtime/vm/isolate.cc
index c1e6cbe..3f425e4 100644
--- a/runtime/vm/isolate.cc
+++ b/runtime/vm/isolate.cc
@@ -2638,11 +2638,9 @@
       Dart::thread_pool()->Run<ShutdownGroupTask>(isolate_group);
     }
   } else {
-    if (FLAG_enable_isolate_groups) {
-      // TODO(dartbug.com/36097): An isolate just died. A significant amount of
-      // memory might have become unreachable. We should evaluate how to best
-      // inform the GC about this situation.
-    }
+    // TODO(dartbug.com/36097): An isolate just died. A significant amount of
+    // memory might have become unreachable. We should evaluate how to best
+    // inform the GC about this situation.
   }
 }  // namespace dart
 
@@ -2807,11 +2805,6 @@
   auto thread = Thread::Current();
   StoppedMutatorsScope stopped_mutators_scope(thread);
 
-  if (thread->IsMutatorThread() && !FLAG_enable_isolate_groups) {
-    single_current_mutator->Call();
-    return;
-  }
-
   if (thread->IsAtSafepoint()) {
     RELEASE_ASSERT(safepoint_handler()->IsOwnedByTheThread(thread));
     single_current_mutator->Call();
diff --git a/runtime/vm/runtime_entry.cc b/runtime/vm/runtime_entry.cc
index 549dd9b..c693c02 100644
--- a/runtime/vm/runtime_entry.cc
+++ b/runtime/vm/runtime_entry.cc
@@ -869,9 +869,6 @@
         new_cache.WriteEntryToBuffer(zone, &buffer, colliding_index, "      ");
         THR_Print("%s\n", buffer.buffer());
       }
-      if (!FLAG_enable_isolate_groups) {
-        FATAL("Duplicate subtype test cache entry");
-      }
       if (old_result.ptr() != result.ptr()) {
         FATAL("Existing subtype test cache entry has result %s, not %s",
               old_result.ToCString(), result.ToCString());
@@ -1246,9 +1243,6 @@
   const Code& target_code = Code::Handle(zone, target_function.EnsureHasCode());
   // Before patching verify that we are not repeatedly patching to the same
   // target.
-  ASSERT(FLAG_enable_isolate_groups ||
-         target_code.ptr() != CodePatcher::GetStaticCallTargetAt(
-                                  caller_frame->pc(), caller_code));
   if (target_code.ptr() !=
       CodePatcher::GetStaticCallTargetAt(caller_frame->pc(), caller_code)) {
     GcSafepointOperationScope safepoint(thread);
@@ -3018,10 +3012,6 @@
         current_target_code.EntryPoint(),
         current_target_code.is_optimized() ? "optimized" : "unoptimized");
   }
-  // With isolate groups enabled, it is possible that the target code
-  // has been deactivated just now(as a result of re-optimizatin for example),
-  // which will result in another run through FixCallersTarget.
-  ASSERT(!current_target_code.IsDisabled() || FLAG_enable_isolate_groups);
   arguments.SetReturn(current_target_code);
 #else
   UNREACHABLE();
diff --git a/runtime/vm/symbols.cc b/runtime/vm/symbols.cc
index 673f26e..a60edfa 100644
--- a/runtime/vm/symbols.cc
+++ b/runtime/vm/symbols.cc
@@ -401,11 +401,6 @@
     // cases.
     if (thread->IsAtSafepoint()) {
       RELEASE_ASSERT(group->safepoint_handler()->IsOwnedByTheThread(thread));
-      // In DEBUG mode the snapshot writer also calls this method inside a
-      // safepoint.
-#if !defined(DEBUG)
-      RELEASE_ASSERT(FLAG_enable_isolate_groups || !USING_PRODUCT);
-#endif
       data = object_store->symbol_table();
       CanonicalStringSet table(&key, &value, &data);
       symbol ^= table.GetOrNull(str);
diff --git a/tests/language/async_star/regression_47610_test.dart b/tests/language/async_star/regression_47610_test.dart
new file mode 100644
index 0000000..9bb4ce2
--- /dev/null
+++ b/tests/language/async_star/regression_47610_test.dart
@@ -0,0 +1,32 @@
+// Copyright (c) 2021, 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.
+
+// Regression test for https://github.com/dart-lang/sdk/issues/47610.
+// Tests returning value from a deep context depth along with
+// breaking from 'await for'.
+
+import "dart:async";
+import "package:expect/expect.dart";
+import "package:async_helper/async_helper.dart";
+
+Stream<int> foo() async* {
+  for (int i = 0; i < 2; ++i) {
+    for (int j = 0; j < 2; ++j) {
+      for (int k = 0; k < 2; ++k) {
+        yield i + j + k;
+      }
+    }
+  }
+}
+
+void test() async {
+  await for (var x in foo()) {
+    Expect.equals(0, x);
+    break;
+  }
+}
+
+void main() {
+  asyncTest(test);
+}
diff --git a/tests/language_2/async_star/regression_47610_test.dart b/tests/language_2/async_star/regression_47610_test.dart
new file mode 100644
index 0000000..4fd7815
--- /dev/null
+++ b/tests/language_2/async_star/regression_47610_test.dart
@@ -0,0 +1,34 @@
+// Copyright (c) 2021, 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.
+
+// @dart = 2.9
+
+// Regression test for https://github.com/dart-lang/sdk/issues/47610.
+// Tests returning value from a deep context depth along with
+// breaking from 'await for'.
+
+import "dart:async";
+import "package:expect/expect.dart";
+import "package:async_helper/async_helper.dart";
+
+Stream<int> foo() async* {
+  for (int i = 0; i < 2; ++i) {
+    for (int j = 0; j < 2; ++j) {
+      for (int k = 0; k < 2; ++k) {
+        yield i + j + k;
+      }
+    }
+  }
+}
+
+void test() async {
+  await for (var x in foo()) {
+    Expect.equals(0, x);
+    break;
+  }
+}
+
+void main() {
+  asyncTest(test);
+}
diff --git a/tools/VERSION b/tools/VERSION
index 41c3212..0754943 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 15
 PATCH 0
-PRERELEASE 290
+PRERELEASE 291
 PRERELEASE_PATCH 0
\ No newline at end of file