Version 2.15.0-47.0.dev

Merge commit '419d70afb2ac23987a9b5638dfc7befa240b157f' into 'dev'
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/add_return_type.dart b/pkg/analysis_server/lib/src/services/correction/dart/add_return_type.dart
index 7e49c67..b5aed2b 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/add_return_type.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/add_return_type.dart
@@ -65,14 +65,16 @@
 
     final insertBeforeEntity_final = insertBeforeEntity;
     await builder.addDartFileEdit(file, (builder) {
-      builder.addInsertion(insertBeforeEntity_final.offset, (builder) {
-        if (returnType.isDynamic) {
-          builder.write('dynamic');
-        } else {
-          builder.writeType(returnType);
-        }
-        builder.write(' ');
-      });
+      if (returnType.isDynamic || builder.canWriteType(returnType)) {
+        builder.addInsertion(insertBeforeEntity_final.offset, (builder) {
+          if (returnType.isDynamic) {
+            builder.write('dynamic');
+          } else {
+            builder.writeType(returnType);
+          }
+          builder.write(' ');
+        });
+      }
     });
   }
 
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/change_type_annotation.dart b/pkg/analysis_server/lib/src/services/correction/dart/change_type_annotation.dart
index f1e0afd..0d13f8c 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/change_type_annotation.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/change_type_annotation.dart
@@ -44,9 +44,11 @@
           _oldAnnotation = displayStringForType(typeNode.typeOrThrow);
           _newAnnotation = displayStringForType(newType);
           await builder.addDartFileEdit(file, (builder) {
-            builder.addReplacement(range.node(typeNode), (builder) {
-              builder.writeType(newType);
-            });
+            if (builder.canWriteType(newType)) {
+              builder.addReplacement(range.node(typeNode), (builder) {
+                builder.writeType(newType);
+              });
+            }
           });
         }
       }
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/replace_return_type.dart b/pkg/analysis_server/lib/src/services/correction/dart/replace_return_type.dart
new file mode 100644
index 0000000..89adc99
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/correction/dart/replace_return_type.dart
@@ -0,0 +1,111 @@
+// 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:analysis_server/src/services/correction/dart/abstract_producer.dart';
+import 'package:analysis_server/src/services/correction/fix.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/syntactic_entity.dart';
+import 'package:analyzer/dart/element/type.dart';
+import 'package:analyzer/src/dart/element/inheritance_manager3.dart';
+import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
+import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
+import 'package:analyzer_plugin/utilities/range_factory.dart';
+
+class ReplaceReturnType extends CorrectionProducer {
+  String _newType = '';
+
+  @override
+  List<Object> get fixArguments => [_newType];
+
+  @override
+  FixKind get fixKind => DartFixKind.REPLACE_RETURN_TYPE;
+
+  @override
+  Future<void> compute(ChangeBuilder builder) async {
+    final node = this.node;
+    if (node is Expression) {
+      final typeSystem = libraryElement.typeSystem;
+
+      var newType = node.staticType;
+
+      void updateNewType(SyntacticEntity entity) {
+        if (entity is FunctionExpression) {
+          return;
+        } else if (entity is ReturnStatement) {
+          var type = entity.expression?.staticType;
+          if (type != null) {
+            if (newType == null) {
+              newType = type;
+            } else {
+              newType = typeSystem.leastUpperBound(newType!, type);
+            }
+          }
+        } else if (entity is AstNode) {
+          entity.childEntities.forEach(updateNewType);
+        }
+      }
+
+      var functionBody = node.thisOrAncestorOfType<FunctionBody>();
+      var parent = functionBody?.parent;
+      var grandParent = parent?.parent;
+
+      TypeAnnotation? returnType;
+      if (grandParent is FunctionDeclaration) {
+        updateNewType(grandParent.functionExpression.body);
+        returnType = grandParent.returnType;
+      } else if (parent is MethodDeclaration) {
+        updateNewType(parent.body);
+        if (_isCompatibleWithReturnType(parent, newType)) {
+          returnType = parent.returnType;
+        }
+      }
+
+      if (returnType != null && newType != null) {
+        if (functionBody!.isAsynchronous) {
+          newType = typeProvider.futureType(newType!);
+        }
+
+        _newType = newType!.getDisplayString(withNullability: true);
+
+        await builder.addDartFileEdit(file, (builder) {
+          if (builder.canWriteType(newType)) {
+            builder.addReplacement(range.node(returnType!), (builder) {
+              builder.writeType(newType);
+            });
+          }
+        });
+      }
+    }
+  }
+
+  bool _isCompatibleWithReturnType(
+      MethodDeclaration method, DartType? newType) {
+    if (newType != null) {
+      var clazz = method.thisOrAncestorOfType<ClassDeclaration>();
+      if (clazz != null) {
+        var classElement = clazz.declaredElement!;
+        var overriddenList = InheritanceManager3().getOverridden2(
+            classElement,
+            Name(
+              classElement.library.source.uri,
+              method.declaredElement!.name,
+            ));
+
+        if (overriddenList != null) {
+          var notSubtype = overriddenList.any((element) => !libraryElement
+              .typeSystem
+              .isSubtypeOf(newType, element.returnType));
+          if (notSubtype) {
+            return false;
+          }
+        }
+      }
+      return true;
+    }
+    return false;
+  }
+
+  /// Return an instance of this class. Used as a tear-off in `FixProcessor`.
+  static ReplaceReturnType newInstance() => ReplaceReturnType();
+}
diff --git a/pkg/analysis_server/lib/src/services/correction/fix.dart b/pkg/analysis_server/lib/src/services/correction/fix.dart
index 73c3779..83b6c8b 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix.dart
@@ -745,6 +745,10 @@
       'dart.fix.replace.nullWithVoid.multi',
       DartFixKindPriority.DEFAULT,
       "Replace 'Null' with 'void' everywhere in file");
+  static const REPLACE_RETURN_TYPE = FixKind(
+      'dart.fix.replace.returnType',
+      DartFixKindPriority.DEFAULT,
+      "Replace the return type with '{0}'");
   static const REPLACE_RETURN_TYPE_FUTURE = FixKind(
       'dart.fix.replace.returnTypeFuture',
       DartFixKindPriority.DEFAULT,
diff --git a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
index e0ef0b5..a67305f 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
@@ -140,6 +140,7 @@
 import 'package:analysis_server/src/services/correction/dart/replace_final_with_var.dart';
 import 'package:analysis_server/src/services/correction/dart/replace_new_with_const.dart';
 import 'package:analysis_server/src/services/correction/dart/replace_null_with_closure.dart';
+import 'package:analysis_server/src/services/correction/dart/replace_return_type.dart';
 import 'package:analysis_server/src/services/correction/dart/replace_return_type_future.dart';
 import 'package:analysis_server/src/services/correction/dart/replace_var_with_dynamic.dart';
 import 'package:analysis_server/src/services/correction/dart/replace_with_brackets.dart';
@@ -919,9 +920,11 @@
     ],
     CompileTimeErrorCode.RETURN_OF_INVALID_TYPE_FROM_FUNCTION: [
       MakeReturnTypeNullable.newInstance,
+      ReplaceReturnType.newInstance,
     ],
     CompileTimeErrorCode.RETURN_OF_INVALID_TYPE_FROM_METHOD: [
       MakeReturnTypeNullable.newInstance,
+      ReplaceReturnType.newInstance,
     ],
     CompileTimeErrorCode.TYPE_TEST_WITH_UNDEFINED_NAME: [
       ChangeTo.classOrMixin,
diff --git a/pkg/analysis_server/test/src/services/correction/fix/add_return_type_test.dart b/pkg/analysis_server/test/src/services/correction/fix/add_return_type_test.dart
index 75a8383..cfcabd6 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/add_return_type_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/add_return_type_test.dart
@@ -152,6 +152,22 @@
 ''');
   }
 
+  Future<void> test_privateType() async {
+    addSource('/home/test/lib/a.dart', '''
+class A {
+  _B b => _B();
+}
+class _B {}
+''');
+
+    await resolveTestCode('''
+import 'package:test/a.dart';
+
+f(A a) => a.b();
+''');
+    await assertNoFix();
+  }
+
   Future<void> test_topLevelFunction_block() async {
     await resolveTestCode('''
 f() {
diff --git a/pkg/analysis_server/test/src/services/correction/fix/change_type_annotation_test.dart b/pkg/analysis_server/test/src/services/correction/fix/change_type_annotation_test.dart
index 80d36d6..ca26deb 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/change_type_annotation_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/change_type_annotation_test.dart
@@ -22,13 +22,13 @@
 
   Future<void> test_generic() async {
     await resolveTestCode('''
-main() {
+f() {
   String v = <int>[];
   print(v);
 }
 ''');
     await assertHasFix('''
-main() {
+f() {
   List<int> v = <int>[];
   print(v);
 }
@@ -37,7 +37,7 @@
 
   Future<void> test_multipleVariables() async {
     await resolveTestCode('''
-main() {
+f() {
   String a, b = '';
   print('\$a \$b');
 }
@@ -47,7 +47,7 @@
 
   Future<void> test_notVariableDeclaration() async {
     await resolveTestCode('''
-main() {
+f() {
   String v;
   v = 42;
   print(v);
@@ -56,15 +56,34 @@
     await assertNoFix();
   }
 
+  Future<void> test_privateType() async {
+    addSource('/home/test/lib/a.dart', '''
+class A {
+  _B b => _B();
+}
+class _B {}
+''');
+
+    await resolveTestCode('''
+import 'package:test/a.dart';
+
+f(A a) {
+  String v = a.b();
+  print(v);
+}
+''');
+    await assertNoFix();
+  }
+
   Future<void> test_simple() async {
     await resolveTestCode('''
-main() {
+f() {
   String v = 'abc'.length;
   print(v);
 }
 ''');
     await assertHasFix('''
-main() {
+f() {
   int v = 'abc'.length;
   print(v);
 }
diff --git a/pkg/analysis_server/test/src/services/correction/fix/replace_return_type_test.dart b/pkg/analysis_server/test/src/services/correction/fix/replace_return_type_test.dart
new file mode 100644
index 0000000..d3c41f7
--- /dev/null
+++ b/pkg/analysis_server/test/src/services/correction/fix/replace_return_type_test.dart
@@ -0,0 +1,235 @@
+// 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:analysis_server/src/services/correction/fix.dart';
+import 'package:analyzer/src/error/codes.dart';
+import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'fix_processor.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(ReplaceReturnTypeTest);
+  });
+}
+
+@reflectiveTest
+class ReplaceReturnTypeTest extends FixProcessorTest {
+  @override
+  FixKind get kind => DartFixKind.REPLACE_RETURN_TYPE;
+
+  Future<void> test_async_method() async {
+    await resolveTestCode('''
+class A {
+  Future<int> m() async {
+    return '';
+  }
+}
+''');
+    await assertHasFix('''
+class A {
+  Future<String> m() async {
+    return '';
+  }
+}
+''');
+  }
+
+  Future<void> test_closure() async {
+    await resolveTestCode('''
+class A {
+  int m() {
+    var list = <String>[];
+    list.map((e) {
+      return 0;
+    });
+    return 2.4;
+  }
+}
+''');
+    await assertHasFix('''
+class A {
+  double m() {
+    var list = <String>[];
+    list.map((e) {
+      return 0;
+    });
+    return 2.4;
+  }
+}
+''');
+  }
+
+  Future<void> test_function() async {
+    await resolveTestCode('''
+int f() {
+  return '';
+}
+''');
+    await assertHasFix('''
+String f() {
+  return '';
+}
+''');
+  }
+
+  Future<void> test_function_local() async {
+    await resolveTestCode('''
+void top() {
+  int f() {
+    return '';
+  }
+}
+''');
+    await assertHasFix('''
+void top() {
+  String f() {
+    return '';
+  }
+}
+''', errorFilter: (error) {
+      return error.errorCode == CompileTimeErrorCode.RETURN_OF_INVALID_TYPE_FROM_FUNCTION;
+    });
+  }
+
+  Future<void> test_method() async {
+    await resolveTestCode('''
+class A {
+  int m() {
+    return '';
+  }
+}
+''');
+    await assertHasFix('''
+class A {
+  String m() {
+    return '';
+  }
+}
+''');
+  }
+
+  Future<void> test_methodOverride() async {
+    await resolveTestCode('''
+class A {
+  A m() => this;
+}
+class B extends A {
+  @override
+  int m() => this;
+}
+''');
+    await assertHasFix('''
+class A {
+  A m() => this;
+}
+class B extends A {
+  @override
+  B m() => this;
+}
+''', errorFilter: (error) {
+      return error.errorCode == CompileTimeErrorCode.RETURN_OF_INVALID_TYPE_FROM_METHOD;
+    });
+  }
+
+  Future<void> test_methodOverride_multiple_subtype() async {
+    await resolveTestCode('''
+class A {}
+class B extends A {}
+
+class Parent {
+  A m() => A();
+}
+
+class I {
+  B m() => B();
+}
+
+class D extends Parent implements I {
+  @override
+  B m() => A();
+}
+''');
+
+    await assertNoFix();
+  }
+
+  Future<void> test_methodOverride_subtype() async {
+    await resolveTestCode('''
+class A {
+  B m() => B();
+}
+class B extends A {
+  @override
+  B m() => A();
+}
+''');
+    await assertNoFix();
+  }
+
+  Future<void> test_privateType() async {
+    addSource('/home/test/lib/a.dart', '''
+class A {
+  _B b => _B();
+}
+class _B {}
+''');
+
+    await resolveTestCode('''
+import 'package:test/a.dart';
+
+int f(A a) {
+  return a.b();
+}
+''');
+    await assertNoFix();
+  }
+
+  Future<void> test_upperBound_function() async {
+    await resolveTestCode('''
+int f() {
+  if (true) {
+    return 3;
+  }
+  return 2.4;
+}
+''');
+    await assertHasFix('''
+num f() {
+  if (true) {
+    return 3;
+  }
+  return 2.4;
+}
+''', errorFilter: (error) {
+      return error.errorCode == CompileTimeErrorCode.RETURN_OF_INVALID_TYPE_FROM_FUNCTION;
+    });
+  }
+
+  Future<void> test_upperBound_method() async {
+    await resolveTestCode('''
+class A {
+  int m() {
+    if (true) {
+      return 3;
+    }
+    return 2.4;
+  }
+}
+''');
+    await assertHasFix('''
+class A {
+  num m() {
+    if (true) {
+      return 3;
+    }
+    return 2.4;
+  }
+}
+''', errorFilter: (error) {
+      return error.errorCode == CompileTimeErrorCode.RETURN_OF_INVALID_TYPE_FROM_METHOD;
+    });
+  }
+}
diff --git a/pkg/analysis_server/test/src/services/correction/fix/test_all.dart b/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
index 6178199..5ff6f0d 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
@@ -167,6 +167,7 @@
 import 'replace_new_with_const_test.dart' as replace_new_with_const;
 import 'replace_null_with_closure_test.dart' as replace_null_with_closure;
 import 'replace_return_type_future_test.dart' as replace_return_type_future;
+import 'replace_return_type_test.dart' as replace_return_type;
 import 'replace_var_with_dynamic_test.dart' as replace_var_with_dynamic;
 import 'replace_with_brackets_test.dart' as replace_with_brackets;
 import 'replace_with_conditional_assignment_test.dart'
@@ -339,6 +340,7 @@
     replace_new_with_const.main();
     replace_null_with_closure.main();
     replace_null_with_void.main();
+    replace_return_type.main();
     replace_return_type_future.main();
     replace_var_with_dynamic.main();
     replace_with_brackets.main();
diff --git a/pkg/analyzer/lib/src/error/codes.dart b/pkg/analyzer/lib/src/error/codes.dart
index f48af7c..0e3c1e3 100644
--- a/pkg/analyzer/lib/src/error/codes.dart
+++ b/pkg/analyzer/lib/src/error/codes.dart
@@ -12011,8 +12011,7 @@
           hasPublishedDocs: true);
 
   /**
-   * Parameters:
-   * 0: the modifier that makes the function a generator
+   * No parameters.
    */
   // #### Description
   //
@@ -12053,10 +12052,13 @@
   // ```
   static const CompileTimeErrorCode RETURN_IN_GENERATOR = CompileTimeErrorCode(
       'RETURN_IN_GENERATOR',
-      "Can't return a value from a generator function (using the '{0}' "
-          "modifier).",
-      correction: "Try removing the value, replacing 'return' with 'yield' or "
-          "changing the method body modifier.",
+      "Can't return a value from a generator function that uses the 'async*' "
+          "or 'sync*' modifier.",
+      // TODO(srawlins): Splitting this code into two cases, one for block-
+      // bodied, and one for expression-bodied, would improve each correction
+      // message. This split would have to be done in the parser.
+      correction: "Try replacing 'return' with 'yield', using a block function "
+          "body, or changing the method body modifier.",
       hasPublishedDocs: true);
 
   /**
diff --git a/pkg/analyzer/lib/src/error/return_type_verifier.dart b/pkg/analyzer/lib/src/error/return_type_verifier.dart
index f2a619b..b222820 100644
--- a/pkg/analyzer/lib/src/error/return_type_verifier.dart
+++ b/pkg/analyzer/lib/src/error/return_type_verifier.dart
@@ -62,13 +62,6 @@
     }
 
     if (enclosingExecutable.isGenerator) {
-      if (expression != null) {
-        _errorReporter.reportErrorForNode(
-          CompileTimeErrorCode.RETURN_IN_GENERATOR,
-          statement,
-          [enclosingExecutable.isAsynchronous ? 'async*' : 'sync*'],
-        );
-      }
       return;
     }
 
diff --git a/pkg/analyzer/lib/src/fasta/error_converter.dart b/pkg/analyzer/lib/src/fasta/error_converter.dart
index 145508c..f5e935f 100644
--- a/pkg/analyzer/lib/src/fasta/error_converter.dart
+++ b/pkg/analyzer/lib/src/fasta/error_converter.dart
@@ -267,10 +267,7 @@
         return;
       case "RETURN_IN_GENERATOR":
         errorReporter?.reportErrorForOffset(
-            CompileTimeErrorCode.RETURN_IN_GENERATOR, offset, length,
-            // TODO(danrubel): Update the parser to report the modifier
-            // involved in this error... either async* or sync*
-            ['async*']);
+            CompileTimeErrorCode.RETURN_IN_GENERATOR, offset, length);
         return;
       case "SUPER_IN_REDIRECTING_CONSTRUCTOR":
         errorReporter?.reportErrorForOffset(
diff --git a/pkg/analyzer/test/src/diagnostics/return_in_generator_test.dart b/pkg/analyzer/test/src/diagnostics/return_in_generator_test.dart
index 748f963..06be63d 100644
--- a/pkg/analyzer/test/src/diagnostics/return_in_generator_test.dart
+++ b/pkg/analyzer/test/src/diagnostics/return_in_generator_test.dart
@@ -29,7 +29,6 @@
   return 0;
 }
 ''', [
-      error(CompileTimeErrorCode.RETURN_IN_GENERATOR, 15, 9),
       error(CompileTimeErrorCode.RETURN_IN_GENERATOR, 15, 6),
     ]);
   }
@@ -64,7 +63,6 @@
   return 0;
 }
 ''', [
-      error(CompileTimeErrorCode.RETURN_IN_GENERATOR, 14, 9),
       error(CompileTimeErrorCode.RETURN_IN_GENERATOR, 14, 6),
     ]);
   }
diff --git a/pkg/analyzer/tool/diagnostics/diagnostics.md b/pkg/analyzer/tool/diagnostics/diagnostics.md
index dbd9c55..d900b81 100644
--- a/pkg/analyzer/tool/diagnostics/diagnostics.md
+++ b/pkg/analyzer/tool/diagnostics/diagnostics.md
@@ -11393,7 +11393,8 @@
 
 ### return_in_generator
 
-_Can't return a value from a generator function (using the '{0}' modifier)._
+_Can't return a value from a generator function that uses the 'async*' or
+'sync*' modifier._
 
 #### Description
 
diff --git a/pkg/dds/lib/src/dap/adapters/dart.dart b/pkg/dds/lib/src/dap/adapters/dart.dart
index ed80060..bc70642 100644
--- a/pkg/dds/lib/src/dap/adapters/dart.dart
+++ b/pkg/dds/lib/src/dap/adapters/dart.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:convert';
 import 'dart:io';
 
 import 'package:collection/collection.dart';
@@ -54,9 +55,173 @@
 /// Pattern for extracting useful error messages from an unhandled exception.
 final _exceptionMessagePattern = RegExp('Unhandled exception:\n(.*)');
 
+/// Whether to subscribe to stdout/stderr through the VM Service.
+///
+/// This is set by [attachRequest] so that any output will still be captured and
+/// sent to the client without needing to access the process.
+///
+/// [launchRequest] reads the stdout/stderr streams directly and does not need
+/// to have them sent via the VM Service.
+var _subscribeToOutputStreams = false;
+
 /// Pattern for a trailing semicolon.
 final _trailingSemicolonPattern = RegExp(r';$');
 
+/// An implementation of [LaunchRequestArguments] that includes all fields used
+/// by the base Dart debug adapter.
+///
+/// This class represents the data passed from the client editor to the debug
+/// adapter in launchRequest, which is a request to start debugging an
+/// application.
+///
+/// Specialised adapters (such as Flutter) will likely extend this class with
+/// their own additional fields.
+class DartAttachRequestArguments extends DartCommonLaunchAttachRequestArguments
+    implements AttachRequestArguments {
+  /// Optional data from the previous, restarted session.
+  /// The data is sent as the 'restart' attribute of the 'terminated' event.
+  /// The client should leave the data intact.
+  final Object? restart;
+
+  final String vmServiceUri;
+
+  DartAttachRequestArguments({
+    this.restart,
+    required this.vmServiceUri,
+    String? name,
+    String? cwd,
+    String? vmServiceInfoFile,
+    List<String>? additionalProjectPaths,
+    bool? debugSdkLibraries,
+    bool? debugExternalPackageLibraries,
+    bool? evaluateGettersInDebugViews,
+    bool? evaluateToStringInDebugViews,
+    bool? sendLogsToClient,
+  }) : super(
+          name: name,
+          cwd: cwd,
+          vmServiceInfoFile: vmServiceInfoFile,
+          additionalProjectPaths: additionalProjectPaths,
+          debugSdkLibraries: debugSdkLibraries,
+          debugExternalPackageLibraries: debugExternalPackageLibraries,
+          evaluateGettersInDebugViews: evaluateGettersInDebugViews,
+          evaluateToStringInDebugViews: evaluateToStringInDebugViews,
+          sendLogsToClient: sendLogsToClient,
+        );
+
+  DartAttachRequestArguments.fromMap(Map<String, Object?> obj)
+      : restart = obj['restart'],
+        vmServiceUri = obj['vmServiceUri'] as String,
+        super.fromMap(obj);
+
+  @override
+  Map<String, Object?> toJson() => {
+        ...super.toJson(),
+        if (restart != null) 'restart': restart,
+        'vmServiceUri': vmServiceUri,
+      };
+
+  static DartAttachRequestArguments fromJson(Map<String, Object?> obj) =>
+      DartAttachRequestArguments.fromMap(obj);
+}
+
+/// A common base for [DartLaunchRequestArguments] and
+/// [DartAttachRequestArguments] for fields that are common to both.
+class DartCommonLaunchAttachRequestArguments extends RequestArguments {
+  final String? name;
+  final String? cwd;
+  final String? vmServiceInfoFile;
+
+  /// Paths that should be considered the users local code.
+  ///
+  /// These paths will generally be all of the open folders in the users editor
+  /// and are used to determine whether a library is "external" or not to
+  /// support debugging "just my code" where SDK/Pub package code will be marked
+  /// as not-debuggable.
+  final List<String>? additionalProjectPaths;
+
+  /// Whether SDK libraries should be marked as debuggable.
+  ///
+  /// Treated as `false` if null, which means "step in" will not step into SDK
+  /// libraries.
+  final bool? debugSdkLibraries;
+
+  /// Whether external package libraries should be marked as debuggable.
+  ///
+  /// Treated as `false` if null, which means "step in" will not step into
+  /// libraries in packages that are not either the local package or a path
+  /// dependency. This allows users to debug "just their code" and treat Pub
+  /// packages as block boxes.
+  final bool? debugExternalPackageLibraries;
+
+  /// Whether to evaluate getters in debug views like hovers and the variables
+  /// list.
+  ///
+  /// Invoking getters has a performance cost and may introduce side-effects,
+  /// although users may expected this functionality. null is treated like false
+  /// although clients may have their own defaults (for example Dart-Code sends
+  /// true by default at the time of writing).
+  final bool? evaluateGettersInDebugViews;
+
+  /// Whether to call toString() on objects in debug views like hovers and the
+  /// variables list.
+  ///
+  /// Invoking toString() has a performance cost and may introduce side-effects,
+  /// although users may expected this functionality. null is treated like false
+  /// although clients may have their own defaults (for example Dart-Code sends
+  /// true by default at the time of writing).
+  final bool? evaluateToStringInDebugViews;
+
+  /// Whether to send debug logging to clients in a custom `dart.log` event. This
+  /// is used both by the out-of-process tests to ensure the logs contain enough
+  /// information to track down issues, but also by Dart-Code to capture VM
+  /// service traffic in a unified log file.
+  final bool? sendLogsToClient;
+
+  DartCommonLaunchAttachRequestArguments({
+    required this.name,
+    required this.cwd,
+    required this.vmServiceInfoFile,
+    required this.additionalProjectPaths,
+    required this.debugSdkLibraries,
+    required this.debugExternalPackageLibraries,
+    required this.evaluateGettersInDebugViews,
+    required this.evaluateToStringInDebugViews,
+    required this.sendLogsToClient,
+  });
+
+  DartCommonLaunchAttachRequestArguments.fromMap(Map<String, Object?> obj)
+      : name = obj['name'] as String?,
+        cwd = obj['cwd'] as String?,
+        vmServiceInfoFile = obj['vmServiceInfoFile'] as String?,
+        additionalProjectPaths =
+            (obj['additionalProjectPaths'] as List?)?.cast<String>(),
+        debugSdkLibraries = obj['debugSdkLibraries'] as bool?,
+        debugExternalPackageLibraries =
+            obj['debugExternalPackageLibraries'] as bool?,
+        evaluateGettersInDebugViews =
+            obj['evaluateGettersInDebugViews'] as bool?,
+        evaluateToStringInDebugViews =
+            obj['evaluateToStringInDebugViews'] as bool?,
+        sendLogsToClient = obj['sendLogsToClient'] as bool?;
+
+  Map<String, Object?> toJson() => {
+        if (name != null) 'name': name,
+        if (cwd != null) 'cwd': cwd,
+        if (vmServiceInfoFile != null) 'vmServiceInfoFile': vmServiceInfoFile,
+        if (additionalProjectPaths != null)
+          'additionalProjectPaths': additionalProjectPaths,
+        if (debugSdkLibraries != null) 'debugSdkLibraries': debugSdkLibraries,
+        if (debugExternalPackageLibraries != null)
+          'debugExternalPackageLibraries': debugExternalPackageLibraries,
+        if (evaluateGettersInDebugViews != null)
+          'evaluateGettersInDebugViews': evaluateGettersInDebugViews,
+        if (evaluateToStringInDebugViews != null)
+          'evaluateToStringInDebugViews': evaluateToStringInDebugViews,
+        if (sendLogsToClient != null) 'sendLogsToClient': sendLogsToClient,
+      };
+}
+
 /// A base DAP Debug Adapter implementation for running and debugging Dart-based
 /// applications (including Flutter and Tests).
 ///
@@ -93,9 +258,9 @@
 /// an expression into an evaluation console) or to events sent by the server
 /// (for example when the server sends a `StoppedEvent` it may cause the client
 /// to then send a `stackTraceRequest` or `scopesRequest` to get variables).
-abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
-    extends BaseDebugAdapter<T> {
-  late final T args;
+abstract class DartDebugAdapter<TL extends DartLaunchRequestArguments,
+    TA extends DartAttachRequestArguments> extends BaseDebugAdapter<TL, TA> {
+  late final DartCommonLaunchAttachRequestArguments args;
   final _debuggerInitializedCompleter = Completer<void>();
   final _configurationDoneCompleter = Completer<void>();
 
@@ -168,7 +333,8 @@
   /// 'cwd' and any 'additionalProjectPaths' from the launch arguments.
   late final List<String> projectPaths = [
     args.cwd,
-    path.dirname(args.program),
+    if (args is DartLaunchRequestArguments)
+      path.dirname((args as DartLaunchRequestArguments).program),
     ...?args.additionalProjectPaths,
   ].whereNotNull().toList();
 
@@ -220,28 +386,33 @@
   /// termination.
   bool get terminateOnVmServiceClose;
 
+  /// Overridden by sub-classes to handle when the client sends an
+  /// `attachRequest` (a request to attach to a running app).
+  ///
+  /// Sub-classes can use the [args] field to access the arguments provided
+  /// to this request.
+  Future<void> attachImpl();
+
   /// [attachRequest] is called by the client when it wants us to to attach to
   /// an existing app. This will only be called once (and only one of this or
   /// launchRequest will be called).
   @override
   Future<void> attachRequest(
     Request request,
-    T args,
+    TA args,
     void Function() sendResponse,
   ) async {
     this.args = args;
     isAttach = true;
+    _subscribeToOutputStreams = true;
 
     // Common setup.
-    await _prepareForLaunchOrAttach();
-
-    // TODO(dantup): Implement attach support.
-    throw UnimplementedError();
+    await _prepareForLaunchOrAttach(null);
 
     // Delegate to the sub-class to attach to the process.
-    // await attachImpl();
-    //
-    // sendResponse();
+    await attachImpl();
+
+    sendResponse();
   }
 
   /// Builds an evaluateName given a parent VM InstanceRef ID and a suffix.
@@ -297,13 +468,25 @@
       // TODO(dantup): Do we need to worry about there already being one connected
       //   if this URL came from another service that may have started one?
       logger?.call('Starting a DDS instance for $uri');
-      final dds = await DartDevelopmentService.startDartDevelopmentService(
-        uri,
-        enableAuthCodes: enableAuthCodes,
-        ipv6: ipv6,
-      );
-      _dds = dds;
-      uri = dds.wsUri!;
+      try {
+        final dds = await DartDevelopmentService.startDartDevelopmentService(
+          uri,
+          enableAuthCodes: enableAuthCodes,
+          ipv6: ipv6,
+        );
+        _dds = dds;
+        uri = dds.wsUri!;
+      } on DartDevelopmentServiceException catch (e) {
+        // If there's already a DDS instance, then just continue. This is common
+        // when attaching, as the program may have already been run with a DDS
+        // instance.
+        if (e.errorCode ==
+            DartDevelopmentServiceException.existingDdsInstanceError) {
+          uri = _cleanVmServiceUri(uri);
+        } else {
+          rethrow;
+        }
+      }
     } else {
       uri = _cleanVmServiceUri(uri);
     }
@@ -328,8 +511,10 @@
       // TODO(dantup): Implement these.
       // vmService.onExtensionEvent.listen(_handleExtensionEvent),
       // vmService.onServiceEvent.listen(_handleServiceEvent),
-      // vmService.onStdoutEvent.listen(_handleStdoutEvent),
-      // vmService.onStderrEvent.listen(_handleStderrEvent),
+      if (_subscribeToOutputStreams) ...[
+        vmService.onStdoutEvent.listen(_handleStdoutEvent),
+        vmService.onStderrEvent.listen(_handleStderrEvent),
+      ],
     ]);
     await Future.wait([
       vmService.streamListen(vm.EventStreams.kIsolate),
@@ -337,8 +522,8 @@
       vmService.streamListen(vm.EventStreams.kLogging),
       // vmService.streamListen(vm.EventStreams.kExtension),
       // vmService.streamListen(vm.EventStreams.kService),
-      // vmService.streamListen(vm.EventStreams.kStdout),
-      // vmService.streamListen(vm.EventStreams.kStderr),
+      vmService.streamListen(vm.EventStreams.kStdout),
+      vmService.streamListen(vm.EventStreams.kStderr),
     ]);
 
     final vmInfo = await vmService.getVM();
@@ -595,12 +780,15 @@
   }
 
   /// Sends a [TerminatedEvent] if one has not already been sent.
-  void handleSessionTerminate() {
+  void handleSessionTerminate([String exitSuffix = '']) {
     if (_hasSentTerminatedEvent) {
       return;
     }
 
     _hasSentTerminatedEvent = true;
+    // Always add a leading newline since the last written text might not have
+    // had one.
+    sendOutput('console', '\nExited$exitSuffix.');
     sendEvent(TerminatedEventBody());
   }
 
@@ -688,18 +876,18 @@
 
   /// [launchRequest] is called by the client when it wants us to to start the app
   /// to be run/debug. This will only be called once (and only one of this or
-  /// attachRequest will be called).
+  /// [attachRequest] will be called).
   @override
   Future<void> launchRequest(
     Request request,
-    T args,
+    TL args,
     void Function() sendResponse,
   ) async {
     this.args = args;
     isAttach = false;
 
     // Common setup.
-    await _prepareForLaunchOrAttach();
+    await _prepareForLaunchOrAttach(args.noDebug);
 
     // Delegate to the sub-class to launch the process.
     await launchImpl();
@@ -1326,6 +1514,14 @@
     }
   }
 
+  void _handleStderrEvent(vm.Event event) {
+    _sendOutputStreamEvent('stderr', event);
+  }
+
+  void _handleStdoutEvent(vm.Event event) {
+    _sendOutputStreamEvent('stdout', event);
+  }
+
   Future<void> _handleVmServiceClosed() async {
     if (terminateOnVmServiceClose) {
       handleSessionTerminate();
@@ -1341,7 +1537,7 @@
 
   /// Performs some setup that is common to both [launchRequest] and
   /// [attachRequest].
-  Future<void> _prepareForLaunchOrAttach() async {
+  Future<void> _prepareForLaunchOrAttach(bool? noDebug) async {
     // Don't start launching until configurationDone.
     if (!_configurationDoneCompleter.isCompleted) {
       logger?.call('Waiting for configurationDone request...');
@@ -1350,13 +1546,25 @@
 
     // Notify IsolateManager if we'll be debugging so it knows whether to set
     // up breakpoints etc. when isolates are registered.
-    final debug = !(args.noDebug ?? false);
+    final debug = !(noDebug ?? false);
     _isolateManager.debug = debug;
     _isolateManager.debugSdkLibraries = args.debugSdkLibraries ?? true;
     _isolateManager.debugExternalPackageLibraries =
         args.debugExternalPackageLibraries ?? true;
   }
 
+  /// Sends output for a VM WriteEvent to the client.
+  ///
+  /// Used to pass stdout/stderr when there's no access to the streams directly.
+  void _sendOutputStreamEvent(String type, vm.Event event) {
+    final data = event.bytes;
+    if (data == null) {
+      return;
+    }
+    final message = utf8.decode(base64Decode(data));
+    sendOutput('stdout', message);
+  }
+
   /// Updates the current debug options for the session.
   ///
   /// Clients may not know about all debug options, so anything not included
@@ -1415,24 +1623,23 @@
 ///
 /// Specialised adapters (such as Flutter) will likely extend this class with
 /// their own additional fields.
-class DartLaunchRequestArguments extends LaunchRequestArguments {
-  final String? name;
+class DartLaunchRequestArguments extends DartCommonLaunchAttachRequestArguments
+    implements LaunchRequestArguments {
+  /// Optional data from the previous, restarted session.
+  /// The data is sent as the 'restart' attribute of the 'terminated' event.
+  /// The client should leave the data intact.
+  final Object? restart;
+
+  /// If noDebug is true the launch request should launch the program without
+  /// enabling debugging.
+  final bool? noDebug;
+
   final String program;
   final List<String>? args;
-  final String? cwd;
-  final String? vmServiceInfoFile;
   final int? vmServicePort;
   final List<String>? vmAdditionalArgs;
   final bool? enableAsserts;
 
-  /// Paths that should be considered the users local code.
-  ///
-  /// These paths will generally be all of the open folders in the users editor
-  /// and are used to determine whether a library is "external" or not to
-  /// support debugging "just my code" where SDK/Pub package code will be marked
-  /// as not-debuggable.
-  final List<String>? additionalProjectPaths;
-
   /// Which console to run the program in.
   ///
   /// If "terminal" or "externalTerminal" will cause the program to be run by
@@ -1445,108 +1652,58 @@
   /// simplest) way, but prevents the user from being able to type into `stdin`.
   final String? console;
 
-  /// Whether SDK libraries should be marked as debuggable.
-  ///
-  /// Treated as `false` if null, which means "step in" will not step into SDK
-  /// libraries.
-  final bool? debugSdkLibraries;
-
-  /// Whether external package libraries should be marked as debuggable.
-  ///
-  /// Treated as `false` if null, which means "step in" will not step into
-  /// libraries in packages that are not either the local package or a path
-  /// dependency. This allows users to debug "just their code" and treat Pub
-  /// packages as block boxes.
-  final bool? debugExternalPackageLibraries;
-
-  /// Whether to evaluate getters in debug views like hovers and the variables
-  /// list.
-  ///
-  /// Invoking getters has a performance cost and may introduce side-effects,
-  /// although users may expected this functionality. null is treated like false
-  /// although clients may have their own defaults (for example Dart-Code sends
-  /// true by default at the time of writing).
-  final bool? evaluateGettersInDebugViews;
-
-  /// Whether to call toString() on objects in debug views like hovers and the
-  /// variables list.
-  ///
-  /// Invoking toString() has a performance cost and may introduce side-effects,
-  /// although users may expected this functionality. null is treated like false
-  /// although clients may have their own defaults (for example Dart-Code sends
-  /// true by default at the time of writing).
-  final bool? evaluateToStringInDebugViews;
-
-  /// Whether to send debug logging to clients in a custom `dart.log` event. This
-  /// is used both by the out-of-process tests to ensure the logs contain enough
-  /// information to track down issues, but also by Dart-Code to capture VM
-  /// service traffic in a unified log file.
-  final bool? sendLogsToClient;
-
   DartLaunchRequestArguments({
-    Object? restart,
-    bool? noDebug,
-    this.name,
+    this.restart,
+    this.noDebug,
     required this.program,
     this.args,
-    this.cwd,
-    this.vmServiceInfoFile,
     this.vmServicePort,
     this.vmAdditionalArgs,
     this.console,
     this.enableAsserts,
-    this.additionalProjectPaths,
-    this.debugSdkLibraries,
-    this.debugExternalPackageLibraries,
-    this.evaluateGettersInDebugViews,
-    this.evaluateToStringInDebugViews,
-    this.sendLogsToClient,
-  }) : super(restart: restart, noDebug: noDebug);
+    String? name,
+    String? cwd,
+    String? vmServiceInfoFile,
+    List<String>? additionalProjectPaths,
+    bool? debugSdkLibraries,
+    bool? debugExternalPackageLibraries,
+    bool? evaluateGettersInDebugViews,
+    bool? evaluateToStringInDebugViews,
+    bool? sendLogsToClient,
+  }) : super(
+          name: name,
+          cwd: cwd,
+          vmServiceInfoFile: vmServiceInfoFile,
+          additionalProjectPaths: additionalProjectPaths,
+          debugSdkLibraries: debugSdkLibraries,
+          debugExternalPackageLibraries: debugExternalPackageLibraries,
+          evaluateGettersInDebugViews: evaluateGettersInDebugViews,
+          evaluateToStringInDebugViews: evaluateToStringInDebugViews,
+          sendLogsToClient: sendLogsToClient,
+        );
 
   DartLaunchRequestArguments.fromMap(Map<String, Object?> obj)
-      : name = obj['name'] as String?,
+      : restart = obj['restart'],
+        noDebug = obj['noDebug'] as bool?,
         program = obj['program'] as String,
         args = (obj['args'] as List?)?.cast<String>(),
-        cwd = obj['cwd'] as String?,
-        vmServiceInfoFile = obj['vmServiceInfoFile'] as String?,
-        vmServicePort = obj['vmServicePort'] as int?,
         vmAdditionalArgs = (obj['vmAdditionalArgs'] as List?)?.cast<String>(),
+        vmServicePort = obj['vmServicePort'] as int?,
         console = obj['console'] as String?,
         enableAsserts = obj['enableAsserts'] as bool?,
-        additionalProjectPaths =
-            (obj['additionalProjectPaths'] as List?)?.cast<String>(),
-        debugSdkLibraries = obj['debugSdkLibraries'] as bool?,
-        debugExternalPackageLibraries =
-            obj['debugExternalPackageLibraries'] as bool?,
-        evaluateGettersInDebugViews =
-            obj['evaluateGettersInDebugViews'] as bool?,
-        evaluateToStringInDebugViews =
-            obj['evaluateToStringInDebugViews'] as bool?,
-        sendLogsToClient = obj['sendLogsToClient'] as bool?,
         super.fromMap(obj);
 
   @override
   Map<String, Object?> toJson() => {
         ...super.toJson(),
-        if (name != null) 'name': name,
+        if (restart != null) 'restart': restart,
+        if (noDebug != null) 'noDebug': noDebug,
         'program': program,
         if (args != null) 'args': args,
-        if (cwd != null) 'cwd': cwd,
-        if (vmServiceInfoFile != null) 'vmServiceInfoFile': vmServiceInfoFile,
-        if (vmServicePort != null) 'vmServicePort': vmServicePort,
         if (vmAdditionalArgs != null) 'vmAdditionalArgs': vmAdditionalArgs,
+        if (vmServicePort != null) 'vmServicePort': vmServicePort,
         if (console != null) 'console': console,
         if (enableAsserts != null) 'enableAsserts': enableAsserts,
-        if (additionalProjectPaths != null)
-          'additionalProjectPaths': additionalProjectPaths,
-        if (debugSdkLibraries != null) 'debugSdkLibraries': debugSdkLibraries,
-        if (debugExternalPackageLibraries != null)
-          'debugExternalPackageLibraries': debugExternalPackageLibraries,
-        if (evaluateGettersInDebugViews != null)
-          'evaluateGettersInDebugViews': evaluateGettersInDebugViews,
-        if (evaluateToStringInDebugViews != null)
-          'evaluateToStringInDebugViews': evaluateToStringInDebugViews,
-        if (sendLogsToClient != null) 'sendLogsToClient': sendLogsToClient,
       };
 
   static DartLaunchRequestArguments fromJson(Map<String, Object?> obj) =>
diff --git a/pkg/dds/lib/src/dap/adapters/dart_cli.dart b/pkg/dds/lib/src/dap/adapters/dart_cli.dart
index a44f520..837313a 100644
--- a/pkg/dds/lib/src/dap/adapters/dart_cli.dart
+++ b/pkg/dds/lib/src/dap/adapters/dart_cli.dart
@@ -16,7 +16,8 @@
 import 'dart.dart';
 
 /// A DAP Debug Adapter for running and debugging Dart CLI scripts.
-class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments> {
+class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments,
+    DartAttachRequestArguments> {
   Process? _process;
 
   /// The location of the vm-service-info file (if debugging).
@@ -41,6 +42,9 @@
   @override
   final parseLaunchArgs = DartLaunchRequestArguments.fromJson;
 
+  @override
+  final parseAttachArgs = DartAttachRequestArguments.fromJson;
+
   DartCliDebugAdapter(
     ByteStreamServerChannel channel, {
     bool ipv6 = false,
@@ -94,6 +98,7 @@
   /// For debugging, this should start paused, connect to the VM Service, set
   /// breakpoints, and resume.
   Future<void> launchImpl() async {
+    final args = this.args as DartLaunchRequestArguments;
     final vmPath = Platform.resolvedExecutable;
 
     final debug = !(args.noDebug ?? false);
@@ -143,7 +148,10 @@
     // TODO(dantup): Remove this once
     //   https://github.com/dart-lang/sdk/issues/45530 is done as it will not be
     //   necessary.
-    final packageConfig = _findPackageConfigFile();
+    var possibleRoot = path.isAbsolute(args.program)
+        ? path.dirname(args.program)
+        : path.dirname(path.normalize(path.join(args.cwd ?? '', args.program)));
+    final packageConfig = _findPackageConfigFile(possibleRoot);
     if (packageConfig != null) {
       this.usePackageConfigFile(packageConfig);
     }
@@ -173,6 +181,26 @@
     }
   }
 
+  /// Called by [attachRequest] to request that we actually connect to the app
+  /// to be debugged.
+  Future<void> attachImpl() async {
+    final args = this.args as DartAttachRequestArguments;
+
+    // Find the package_config file for this script.
+    // TODO(dantup): Remove this once
+    //   https://github.com/dart-lang/sdk/issues/45530 is done as it will not be
+    //   necessary.
+    final cwd = args.cwd;
+    if (cwd != null) {
+      final packageConfig = _findPackageConfigFile(cwd);
+      if (packageConfig != null) {
+        this.usePackageConfigFile(packageConfig);
+      }
+    }
+
+    unawaited(connectDebugger(Uri.parse(args.vmServiceUri)));
+  }
+
   /// Calls the client (via a `runInTerminal` request) to spawn the process so
   /// that it can run in a local terminal that the user can interact with.
   Future<void> launchInEditorTerminal(
@@ -181,6 +209,7 @@
     String vmPath,
     List<String> processArgs,
   ) async {
+    final args = this.args as DartLaunchRequestArguments;
     logger?.call('Spawning $vmPath with $processArgs in ${args.cwd}'
         ' via client ${terminalKind} terminal');
 
@@ -241,11 +270,7 @@
   /// TODO(dantup): Remove this once
   ///   https://github.com/dart-lang/sdk/issues/45530 is done as it will not be
   ///   necessary.
-  File? _findPackageConfigFile() {
-    var possibleRoot = path.isAbsolute(args.program)
-        ? path.dirname(args.program)
-        : path.dirname(path.normalize(path.join(args.cwd ?? '', args.program)));
-
+  File? _findPackageConfigFile(String possibleRoot) {
     File? packageConfig;
     while (true) {
       packageConfig =
@@ -282,10 +307,7 @@
   void _handleExitCode(int code) {
     final codeSuffix = code == 0 ? '' : ' ($code)';
     logger?.call('Process exited ($code)');
-    // Always add a leading newline since the last written text might not have
-    // had one.
-    sendOutput('console', '\nExited$codeSuffix.');
-    handleSessionTerminate();
+    handleSessionTerminate(codeSuffix);
   }
 
   void _handleStderr(List<int> data) {
diff --git a/pkg/dds/lib/src/dap/base_debug_adapter.dart b/pkg/dds/lib/src/dap/base_debug_adapter.dart
index 1071fee..3f64d98 100644
--- a/pkg/dds/lib/src/dap/base_debug_adapter.dart
+++ b/pkg/dds/lib/src/dap/base_debug_adapter.dart
@@ -26,7 +26,8 @@
 /// appropriate method calls/events.
 ///
 /// This class does not implement any DA functionality, only message handling.
-abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments> {
+abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments,
+    TAttachArgs extends AttachRequestArguments> {
   int _sequence = 1;
   final ByteStreamServerChannel _channel;
 
@@ -38,6 +39,13 @@
     _channel.listen(_handleIncomingMessage);
   }
 
+  /// Parses arguments for [attachRequest] into a type of [TAttachArgs].
+  ///
+  /// This method must be implemented by the implementing class using a class
+  /// that corresponds to the arguments it expects (these may differ between
+  /// Dart CLI, Dart tests, Flutter, Flutter tests).
+  TAttachArgs Function(Map<String, Object?>) get parseAttachArgs;
+
   /// Parses arguments for [launchRequest] into a type of [TLaunchArgs].
   ///
   /// This method must be implemented by the implementing class using a class
@@ -47,7 +55,7 @@
 
   Future<void> attachRequest(
     Request request,
-    TLaunchArgs args,
+    TAttachArgs args,
     void Function() sendResponse,
   );
 
@@ -271,7 +279,7 @@
     } else if (request.command == 'launch') {
       handle(request, _withVoidResponse(launchRequest), parseLaunchArgs);
     } else if (request.command == 'attach') {
-      handle(request, _withVoidResponse(attachRequest), parseLaunchArgs);
+      handle(request, _withVoidResponse(attachRequest), parseAttachArgs);
     } else if (request.command == 'terminate') {
       handle(
         request,
diff --git a/pkg/dds/test/dap/integration/debug_attach_test.dart b/pkg/dds/test/dap/integration/debug_attach_test.dart
new file mode 100644
index 0000000..b986621
--- /dev/null
+++ b/pkg/dds/test/dap/integration/debug_attach_test.dart
@@ -0,0 +1,66 @@
+// 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/test.dart';
+
+import 'test_client.dart';
+import 'test_scripts.dart';
+import 'test_support.dart';
+
+main() {
+  group('debug mode', () {
+    late DapTestSession dap;
+    setUp(() async {
+      dap = await DapTestSession.setUp();
+    });
+    tearDown(() => dap.tearDown());
+
+    test('can attach to a simple script using vmServiceUri', () async {
+      final testFile = dap.createTestFile(simpleArgPrintingProgram);
+
+      final args = ['one', 'two'];
+      final proc = await startDartProcessPaused(
+        testFile.path,
+        args,
+        cwd: dap.testAppDir.path,
+      );
+      final vmServiceUri = await waitForStdoutVmServiceBanner(proc);
+
+      final outputEvents = await dap.client.collectOutput(
+        launch: () => dap.client.attach(
+          vmServiceUri: vmServiceUri.toString(),
+          cwd: dap.testAppDir.path,
+        ),
+      );
+
+      // Expect a "console" output event that prints the URI of the VM Service
+      // the debugger connects to.
+      final vmConnection = outputEvents.first;
+      expect(vmConnection.output,
+          startsWith('Connecting to VM Service at ws://127.0.0.1:'));
+      expect(vmConnection.category, equals('console'));
+
+      // Expect the normal applications output.
+      final output = outputEvents
+          .skip(1)
+          .map((e) => e.output)
+          // The stdout also contains the Observatory+DevTools banners.
+          .where(
+            (line) =>
+                !line.startsWith('Observatory listening on') &&
+                !line.startsWith(
+                    'The Dart DevTools debugger and profiler is available at'),
+          )
+          .join();
+      expectLines(output, [
+        'Hello!',
+        'World!',
+        'args: [one, two]',
+        '',
+        'Exited.',
+      ]);
+    });
+    // These tests can be slow due to starting up the external server process.
+  }, timeout: Timeout.none);
+}
diff --git a/pkg/dds/test/dap/integration/debug_test.dart b/pkg/dds/test/dap/integration/debug_test.dart
index 8c6a42a..6e7565b 100644
--- a/pkg/dds/test/dap/integration/debug_test.dart
+++ b/pkg/dds/test/dap/integration/debug_test.dart
@@ -20,13 +20,7 @@
     tearDown(() => dap.tearDown());
 
     test('runs a simple script', () async {
-      final testFile = dap.createTestFile(r'''
-void main(List<String> args) async {
-  print('Hello!');
-  print('World!');
-  print('args: $args');
-}
-    ''');
+      final testFile = dap.createTestFile(simpleArgPrintingProgram);
 
       final outputEvents = await dap.client.collectOutput(
         launch: () => dap.client.launch(
@@ -212,6 +206,6 @@
 Uri _extractVmServiceUri(OutputEventBody vmConnectionBanner) {
   // TODO(dantup): Change this to use the dart.debuggerUris custom event
   //   if implemented (whch VS Code also needs).
-  final match = vmServiceUriPattern.firstMatch(vmConnectionBanner.output);
+  final match = dapVmServiceBannerPattern.firstMatch(vmConnectionBanner.output);
   return Uri.parse(match!.group(1)!);
 }
diff --git a/pkg/dds/test/dap/integration/no_debug_test.dart b/pkg/dds/test/dap/integration/no_debug_test.dart
index dec3316..c5e5e54 100644
--- a/pkg/dds/test/dap/integration/no_debug_test.dart
+++ b/pkg/dds/test/dap/integration/no_debug_test.dart
@@ -20,13 +20,7 @@
 
   group('noDebug mode', () {
     test('runs a simple script', () async {
-      final testFile = dap.createTestFile(r'''
-void main(List<String> args) async {
-  print('Hello!');
-  print('World!');
-  print('args: $args');
-}
-    ''');
+      final testFile = dap.createTestFile(simpleArgPrintingProgram);
 
       final outputEvents = await dap.client.collectOutput(
         launch: () => dap.client.launch(
diff --git a/pkg/dds/test/dap/integration/test_client.dart b/pkg/dds/test/dap/integration/test_client.dart
index 150050e..e08e094 100644
--- a/pkg/dds/test/dap/integration/test_client.dart
+++ b/pkg/dds/test/dap/integration/test_client.dart
@@ -59,6 +59,37 @@
   Stream<OutputEventBody> get outputEvents => events('output')
       .map((e) => OutputEventBody.fromJson(e.body as Map<String, Object?>));
 
+  /// Send an attachRequest to the server, asking it to attach to an existing
+  /// Dart program.
+  Future<Response> attach({
+    required String vmServiceUri,
+    String? cwd,
+    List<String>? additionalProjectPaths,
+    bool? debugSdkLibraries,
+    bool? debugExternalPackageLibraries,
+    bool? evaluateGettersInDebugViews,
+    bool? evaluateToStringInDebugViews,
+  }) {
+    return sendRequest(
+      DartAttachRequestArguments(
+        vmServiceUri: vmServiceUri,
+        cwd: cwd,
+        additionalProjectPaths: additionalProjectPaths,
+        debugSdkLibraries: debugSdkLibraries,
+        debugExternalPackageLibraries: debugExternalPackageLibraries,
+        evaluateGettersInDebugViews: evaluateGettersInDebugViews,
+        evaluateToStringInDebugViews: evaluateToStringInDebugViews,
+        // When running out of process, VM Service traffic won't be available
+        // to the client-side logger, so force logging on which sends VM Service
+        // traffic in a custom event.
+        sendLogsToClient: captureVmServiceTraffic,
+      ),
+      // We can't automatically pick the command when using a custom type
+      // (DartAttachRequestArguments).
+      overrideCommand: 'attach',
+    );
+  }
+
   /// Sends a continue request for the given thread.
   ///
   /// Returns a Future that completes when the server returns a corresponding
diff --git a/pkg/dds/test/dap/integration/test_scripts.dart b/pkg/dds/test/dap/integration/test_scripts.dart
index df3ed5d..08b5390 100644
--- a/pkg/dds/test/dap/integration/test_scripts.dart
+++ b/pkg/dds/test/dap/integration/test_scripts.dart
@@ -20,6 +20,15 @@
   }
 ''';
 
+/// A simple Dart script that prints its arguments.
+const simpleArgPrintingProgram = r'''
+  void main(List<String> args) async {
+    print('Hello!');
+    print('World!');
+    print('args: $args');
+  }
+''';
+
 /// A simple async Dart script that when stopped at the line of '// BREAKPOINT'
 /// will contain multiple stack frames across some async boundaries.
 const simpleAsyncProgram = '''
diff --git a/pkg/dds/test/dap/integration/test_support.dart b/pkg/dds/test/dap/integration/test_support.dart
index 9fd5a88..a8cbd0f 100644
--- a/pkg/dds/test/dap/integration/test_support.dart
+++ b/pkg/dds/test/dap/integration/test_support.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:convert';
 import 'dart:io';
 
 import 'package:dds/src/dap/logging.dart';
@@ -14,6 +15,11 @@
 import 'test_client.dart';
 import 'test_server.dart';
 
+/// A [RegExp] that matches the "Connecting to VM Service" banner that is sent
+/// by the DAP adapter as the first output event for a debug session.
+final dapVmServiceBannerPattern =
+    RegExp(r'Connecting to VM Service at ([^\s]+)\s');
+
 /// Whether to run the DAP server in-process with the tests, or externally in
 /// another process.
 ///
@@ -34,9 +40,9 @@
 /// an authentication token.
 final vmServiceAuthCodePathPattern = RegExp(r'^/[\w_\-=]{5,15}/ws$');
 
-/// A [RegExp] that matches the "Connecting to VM Service" banner that is sent
-/// as the first output event for a debug session.
-final vmServiceUriPattern = RegExp(r'Connecting to VM Service at ([^\s]+)\s');
+/// A [RegExp] that matches the "Observatory listening on" banner that is sent
+/// by the VM when not using --write-service-info.
+final vmServiceBannerPattern = RegExp(r'Observatory listening on ([^\s]+)\s');
 
 /// Expects [actual] to equal the lines [expected], ignoring differences in line
 /// endings and trailing whitespace.
@@ -72,6 +78,55 @@
 int lineWith(File file, String searchText) =>
     file.readAsLinesSync().indexWhere((line) => line.contains(searchText)) + 1;
 
+Future<Process> startDartProcessPaused(
+  String script,
+  List<String> args, {
+  required String cwd,
+  List<String>? vmArgs,
+}) async {
+  final vmPath = Platform.resolvedExecutable;
+  vmArgs ??= [];
+  vmArgs.addAll([
+    '--enable-vm-service=0',
+    '--pause_isolates_on_start',
+  ]);
+  final processArgs = [
+    ...vmArgs,
+    script,
+    ...args,
+  ];
+
+  return Process.start(
+    vmPath,
+    processArgs,
+    workingDirectory: cwd,
+  );
+}
+
+/// Monitors [process] for the Observatory/VM Service banner and extracts the
+/// VM Service URI.
+Future<Uri> waitForStdoutVmServiceBanner(Process process) {
+  final _vmServiceUriCompleter = Completer<Uri>();
+
+  late StreamSubscription<String> vmServiceBannerSub;
+  vmServiceBannerSub = process.stdout.transform(utf8.decoder).listen(
+    (line) {
+      final match = vmServiceBannerPattern.firstMatch(line);
+      if (match != null) {
+        _vmServiceUriCompleter.complete(Uri.parse(match.group(1)!));
+        vmServiceBannerSub.cancel();
+      }
+    },
+    onDone: () {
+      if (!_vmServiceUriCompleter.isCompleted) {
+        _vmServiceUriCompleter.completeError('Stream ended');
+      }
+    },
+  );
+
+  return _vmServiceUriCompleter.future;
+}
+
 /// A helper class containing the DAP server/client for DAP integration tests.
 class DapTestSession {
   DapTestServer server;
diff --git a/tests/language/async/return_types_test.dart b/tests/language/async/return_types_test.dart
index 3de5491..c28aeae 100644
--- a/tests/language/async/return_types_test.dart
+++ b/tests/language/async/return_types_test.dart
@@ -56,8 +56,6 @@
 //^^^^^^
 // [analyzer] COMPILE_TIME_ERROR.RETURN_IN_GENERATOR
 // [cfe] 'sync*' and 'async*' can't return a value.
-//^^^^^^^^^
-// [analyzer] COMPILE_TIME_ERROR.RETURN_IN_GENERATOR
 }
 
 Stream<int> foo9() async* {
@@ -67,8 +65,6 @@
 //^^^^^^
 // [analyzer] COMPILE_TIME_ERROR.RETURN_IN_GENERATOR
 // [cfe] 'sync*' and 'async*' can't return a value.
-//^^^^^^^^^
-// [analyzer] COMPILE_TIME_ERROR.RETURN_IN_GENERATOR
 }
 
 test() async {
diff --git a/tests/language_2/async/return_types_test.dart b/tests/language_2/async/return_types_test.dart
index d976b5bc..0e97782 100644
--- a/tests/language_2/async/return_types_test.dart
+++ b/tests/language_2/async/return_types_test.dart
@@ -60,7 +60,6 @@
 //^^^^^^
 // [analyzer] COMPILE_TIME_ERROR.RETURN_IN_GENERATOR
 //^^^^^^^^^
-// [analyzer] COMPILE_TIME_ERROR.RETURN_IN_GENERATOR
 // [cfe] 'sync*' and 'async*' can't return a value.
 }
 
@@ -71,7 +70,6 @@
 //^^^^^^
 // [analyzer] COMPILE_TIME_ERROR.RETURN_IN_GENERATOR
 //^^^^^^^^^
-// [analyzer] COMPILE_TIME_ERROR.RETURN_IN_GENERATOR
 // [cfe] 'sync*' and 'async*' can't return a value.
 }
 
diff --git a/tools/VERSION b/tools/VERSION
index b711534..03e9d5b 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 15
 PATCH 0
-PRERELEASE 46
+PRERELEASE 47
 PRERELEASE_PATCH 0
\ No newline at end of file