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