Version 2.12.0-33.0.dev
Merge commit '546fdf55bccaa02105c3cb0645f2325f5145f5ef' into 'dev'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 23f93f2..139cd68 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -56,8 +56,9 @@
#### Linter
-Updated the Linter to `0.1.124`, which includes:
+Updated the Linter to `0.1.125`, which includes:
+* (Internal): test updates to the new `PhysicalResourceProvider` API.
* Fixed false positives in `prefer_constructors_over_static_methods`.
* Updates to `package_names` to allow leading underscores.
* Fixed NPEs in `unnecessary_null_checks`.
diff --git a/DEPS b/DEPS
index a93ac4f..bb39333 100644
--- a/DEPS
+++ b/DEPS
@@ -117,7 +117,7 @@
"intl_tag": "0.17.0-nullsafety",
"jinja2_rev": "2222b31554f03e62600cd7e383376a7c187967a1",
"json_rpc_2_rev": "b8dfe403fd8528fd14399dee3a6527b55802dd4d",
- "linter_tag": "0.1.124",
+ "linter_tag": "0.1.125",
"logging_rev": "e2f633b543ef89c54688554b15ca3d7e425b86a2",
"markupsafe_rev": "8f45f5cfa0009d2a70589bcda0349b8cb2b72783",
"markdown_rev": "6f89681d59541ddb1cf3a58efbdaa2304ffc3f51",
@@ -148,7 +148,7 @@
"source_span_rev": "49ff31eabebed0da0ae6634124f8ba5c6fbf57f1",
"sse_tag": "e5cf68975e8e87171a3dc297577aa073454a91dc",
"stack_trace_tag": "6788afc61875079b71b3d1c3e65aeaa6a25cbc2f",
- "stagehand_tag": "v3.3.9",
+ "stagehand_tag": "v3.3.11",
"stream_channel_tag": "d7251e61253ec389ee6e045ee1042311bced8f1d",
"string_scanner_rev": "1b63e6e5db5933d7be0a45da6e1129fe00262734",
"sync_http_rev": "a85d7ec764ea485cbbc49f3f3e7f1b43f87a1c74",
diff --git a/pkg/analysis_server/lib/src/lsp/client_configuration.dart b/pkg/analysis_server/lib/src/lsp/client_configuration.dart
index 55bfaf5..8af5a85 100644
--- a/pkg/analysis_server/lib/src/lsp/client_configuration.dart
+++ b/pkg/analysis_server/lib/src/lsp/client_configuration.dart
@@ -20,6 +20,7 @@
}
}
+ bool get completeFunctionCalls => _settings['completeFunctionCalls'] ?? false;
bool get enableSdkFormatter => _settings['enableSdkFormatter'] ?? true;
int get lineLength => _settings['lineLength'];
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
index 430489a..e9d0fcd 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
@@ -239,6 +239,8 @@
// https://github.com/microsoft/vscode-languageserver-node/issues/673
includeCommitCharacters:
server.clientConfiguration.previewCommitCharacters,
+ completeFunctionCalls:
+ server.clientConfiguration.completeFunctionCalls,
),
)
.toList();
@@ -372,6 +374,7 @@
// not assume that the Dart ones would be correct for all of their
// completions.
includeCommitCharacters: false,
+ completeFunctionCalls: false,
),
);
});
diff --git a/pkg/analysis_server/lib/src/lsp/mapping.dart b/pkg/analysis_server/lib/src/lsp/mapping.dart
index 039b503..e425b2d 100644
--- a/pkg/analysis_server/lib/src/lsp/mapping.dart
+++ b/pkg/analysis_server/lib/src/lsp/mapping.dart
@@ -62,13 +62,14 @@
_asMarkup(preferredFormats, content));
}
-/// Builds an LSP snippet string that uses a $1 tabstop to set the selected text
-/// after insertion.
-String buildSnippetStringWithSelection(
+/// Builds an LSP snippet string with supplied ranges as tabstops.
+String buildSnippetStringWithTabStops(
String text,
- int selectionOffset,
- int selectionLength,
+ List<int> offsetLengthPairs,
) {
+ text ??= '';
+ offsetLengthPairs ??= const [];
+
String escape(String input) => input.replaceAllMapped(
RegExp(r'[$}\\]'), // Replace any of $ } \
(c) => '\\${c[0]}', // Prefix with a backslash
@@ -77,17 +78,29 @@
// https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#snippet-syntax
//
// $1, $2, etc. are used for tab stops and ${1:foo} inserts a placeholder of foo.
- // Since we only need to support a single tab stop, our string is constructed of three parts:
- // - Anything before the selection
- // - The selection (which may or may not include text, depending on selectionLength)
- // - Anything after the selection
- final prefix = escape(text.substring(0, selectionOffset));
- final selectionText = escape(
- text.substring(selectionOffset, selectionOffset + selectionLength));
- final selection = '\${1:$selectionText}';
- final suffix = escape(text.substring(selectionOffset + selectionLength));
- return '$prefix$selection$suffix';
+ final output = [];
+ var offset = 0;
+ var tabStopNumber = 1;
+ for (var i = 0; i < offsetLengthPairs.length; i += 2) {
+ final pairOffset = offsetLengthPairs[i];
+ final pairLength = offsetLengthPairs[i + 1];
+
+ // Add any text that came before this tabstop to the result.
+ output.add(escape(text.substring(offset, pairOffset)));
+
+ // Add this tabstop
+ final tabStopText =
+ escape(text.substring(pairOffset, pairOffset + pairLength));
+ output.add('\${${tabStopNumber++}:$tabStopText}');
+
+ offset = pairOffset + pairLength;
+ }
+
+ // Add any remaining text that was after the last tabstop.
+ output.add(escape(text.substring(offset)));
+
+ return output.join('');
}
/// Note: This code will fetch the version of each document being modified so
@@ -769,6 +782,7 @@
int replacementOffset,
int replacementLength, {
@required bool includeCommitCharacters,
+ @required bool completeFunctionCalls,
}) {
// Build display labels and text to insert. insertText and filterText may
// differ from label (for ex. if the label includes things like (…)). If
@@ -782,14 +796,24 @@
label = label.substring(0, label.length - 1);
}
- if (suggestion.displayText == null) {
- switch (suggestion.element?.kind) {
- case server.ElementKind.CONSTRUCTOR:
- case server.ElementKind.FUNCTION:
- case server.ElementKind.METHOD:
- label += suggestion.parameterNames?.isNotEmpty ?? false ? '(…)' : '()';
- break;
- }
+ // isCallable is used to suffix the label with parens so it's clear the item
+ // is callable.
+ //
+ // isInvocation means the location at which it's used is an invocation (and
+ // therefore it is appropriate to include the parens/parameters in the
+ // inserted text).
+ //
+ // In the case of show combinators, the parens will still be shown to indicate
+ // functions but they should not be included in the completions.
+ final elementKind = suggestion.element?.kind;
+ final isCallable = elementKind == server.ElementKind.CONSTRUCTOR ||
+ elementKind == server.ElementKind.FUNCTION ||
+ elementKind == server.ElementKind.METHOD;
+ final isInvocation =
+ suggestion.kind == server.CompletionSuggestionKind.INVOCATION;
+
+ if (suggestion.displayText == null && isCallable) {
+ label += suggestion.parameterNames?.isNotEmpty ?? false ? '(…)' : '()';
}
final supportsDeprecatedFlag =
@@ -809,13 +833,33 @@
supportedCompletionItemKinds, suggestion.kind, label);
var insertTextFormat = lsp.InsertTextFormat.PlainText;
- if (supportsSnippets && suggestion.selectionOffset != 0) {
- insertTextFormat = lsp.InsertTextFormat.Snippet;
- insertText = buildSnippetStringWithSelection(
- suggestion.completion,
- suggestion.selectionOffset,
- suggestion.selectionLength,
- );
+
+ // If the client supports snippets, we can support completeFunctionCalls or
+ // setting a selection.
+ if (supportsSnippets) {
+ // completeFunctionCalls should only work if commit characters are disabled
+ // otherwise the editor may insert parens that we're also inserting.
+ if (!includeCommitCharacters &&
+ completeFunctionCalls &&
+ isCallable &&
+ isInvocation) {
+ insertTextFormat = lsp.InsertTextFormat.Snippet;
+ final hasRequiredParameters =
+ (suggestion.defaultArgumentListTextRanges?.length ?? 0) > 0;
+ final functionCallSuffix = hasRequiredParameters
+ ? buildSnippetStringWithTabStops(
+ suggestion.defaultArgumentListString,
+ suggestion.defaultArgumentListTextRanges,
+ )
+ : '\${1:}'; // No required params still gets a tabstop in the parens.
+ insertText += '($functionCallSuffix)';
+ } else if (suggestion.selectionOffset != 0) {
+ insertTextFormat = lsp.InsertTextFormat.Snippet;
+ insertText = buildSnippetStringWithTabStops(
+ suggestion.completion,
+ [suggestion.selectionOffset, suggestion.selectionLength],
+ );
+ }
}
// Because we potentially send thousands of these items, we should minimise
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/replace_with_var.dart b/pkg/analysis_server/lib/src/services/correction/dart/replace_with_var.dart
index cd226a8..8ac0b18 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/replace_with_var.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/replace_with_var.dart
@@ -42,6 +42,9 @@
String typeArgumentsText;
int typeArgumentsOffset;
if (type is NamedType && type.typeArguments != null) {
+ if (initializer is CascadeExpression) {
+ initializer = (initializer as CascadeExpression).target;
+ }
if (initializer is TypedLiteral) {
if (initializer.typeArguments == null) {
typeArgumentsText = utils.getNodeText(type.typeArguments);
diff --git a/pkg/analysis_server/test/lsp/completion_test.dart b/pkg/analysis_server/test/lsp/completion_test.dart
index cdecc6f..94ff72b 100644
--- a/pkg/analysis_server/test/lsp/completion_test.dart
+++ b/pkg/analysis_server/test/lsp/completion_test.dart
@@ -15,6 +15,7 @@
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(CompletionTest);
+ defineReflectiveTests(CompletionTestWithNullSafetyTest);
});
}
@@ -89,6 +90,93 @@
expect(options.allCommitCharacters, equals(dartCompletionCommitCharacters));
}
+ Future<void> test_completeFunctionCalls() async {
+ final content = '''
+ void myFunction(String a, int b, {String c}) {}
+
+ main() {
+ [[myFu^]]
+ }
+ ''';
+
+ await provideConfig(
+ () => initialize(
+ textDocumentCapabilities: withCompletionItemSnippetSupport(
+ emptyTextDocumentClientCapabilities),
+ workspaceCapabilities:
+ withConfigurationSupport(emptyWorkspaceClientCapabilities),
+ ),
+ {'completeFunctionCalls': true},
+ );
+ await openFile(mainFileUri, withoutMarkers(content));
+ final res = await getCompletion(mainFileUri, positionFromMarker(content));
+ final item = res.singleWhere((c) => c.label == 'myFunction(…)');
+ // Ensure the snippet comes through in the expected format with the expected
+ // placeholders.
+ expect(item.insertTextFormat, equals(InsertTextFormat.Snippet));
+ expect(item.insertText, equals(r'myFunction(${1:a}, ${2:b})'));
+ expect(item.textEdit.newText, equals(item.insertText));
+ expect(
+ item.textEdit.range,
+ equals(rangeFromMarkers(content)),
+ );
+ }
+
+ Future<void> test_completeFunctionCalls_noRequiredParameters() async {
+ final content = '''
+ void myFunction({int a}) {}
+
+ main() {
+ [[myFu^]]
+ }
+ ''';
+
+ await provideConfig(
+ () => initialize(
+ textDocumentCapabilities: withCompletionItemSnippetSupport(
+ emptyTextDocumentClientCapabilities),
+ workspaceCapabilities:
+ withConfigurationSupport(emptyWorkspaceClientCapabilities),
+ ),
+ {'completeFunctionCalls': true},
+ );
+ await openFile(mainFileUri, withoutMarkers(content));
+ final res = await getCompletion(mainFileUri, positionFromMarker(content));
+ final item = res.singleWhere((c) => c.label == 'myFunction(…)');
+ // With no required params, there should still be parens and a tabstop inside.
+ expect(item.insertTextFormat, equals(InsertTextFormat.Snippet));
+ expect(item.insertText, equals(r'myFunction(${1:})'));
+ expect(item.textEdit.newText, equals(item.insertText));
+ expect(
+ item.textEdit.range,
+ equals(rangeFromMarkers(content)),
+ );
+ }
+
+ Future<void> test_completeFunctionCalls_show() async {
+ final content = '''
+ import 'dart:math' show mi^
+ ''';
+
+ await provideConfig(
+ () => initialize(
+ textDocumentCapabilities: withCompletionItemSnippetSupport(
+ emptyTextDocumentClientCapabilities),
+ workspaceCapabilities:
+ withConfigurationSupport(emptyWorkspaceClientCapabilities),
+ ),
+ {'completeFunctionCalls': true},
+ );
+ await openFile(mainFileUri, withoutMarkers(content));
+ final res = await getCompletion(mainFileUri, positionFromMarker(content));
+ final item = res.singleWhere((c) => c.label == 'min(…)');
+ // Ensure the snippet does not include the parens/args as it doesn't
+ // make sense in the show clause. There will still be a trailing tabstop.
+ expect(item.insertTextFormat, equals(InsertTextFormat.Snippet));
+ expect(item.insertText, equals(r'min${1:}'));
+ expect(item.textEdit.newText, equals(item.insertText));
+ }
+
Future<void> test_completionKinds_default() async {
newFile(join(projectFolderPath, 'file.dart'));
newFolder(join(projectFolderPath, 'folder'));
@@ -1237,3 +1325,44 @@
expect(updated, contains('a.abcdefghij'));
}
}
+
+@reflectiveTest
+class CompletionTestWithNullSafetyTest extends AbstractLspAnalysisServerTest {
+ @override
+ String get testPackageLanguageVersion => latestLanguageVersion;
+
+ @failingTest
+ Future<void> test_completeFunctionCalls_requiredNamed() async {
+ // TODO(dantup): Find out how we can tell this parameter is required
+ // (in the completion mapping).
+ final content = '''
+ void myFunction(String a, int b, {required String c, String d = ''}) {}
+
+ main() {
+ [[myFu^]]
+ }
+ ''';
+
+ await provideConfig(
+ () => initialize(
+ textDocumentCapabilities: withCompletionItemSnippetSupport(
+ emptyTextDocumentClientCapabilities),
+ workspaceCapabilities:
+ withConfigurationSupport(emptyWorkspaceClientCapabilities),
+ ),
+ {'completeFunctionCalls': true},
+ );
+ await openFile(mainFileUri, withoutMarkers(content));
+ final res = await getCompletion(mainFileUri, positionFromMarker(content));
+ final item = res.singleWhere((c) => c.label == 'myFunction(…)');
+ // Ensure the snippet comes through in the expected format with the expected
+ // placeholders.
+ expect(item.insertTextFormat, equals(InsertTextFormat.Snippet));
+ expect(item.insertText, equals(r'myFunction(${1:a}, ${2:b}, ${2:c})'));
+ expect(item.textEdit.newText, equals(item.insertText));
+ expect(
+ item.textEdit.range,
+ equals(rangeFromMarkers(content)),
+ );
+ }
+}
diff --git a/pkg/analysis_server/test/lsp/mapping_test.dart b/pkg/analysis_server/test/lsp/mapping_test.dart
index 2ae7ac1..72bf3f6 100644
--- a/pkg/analysis_server/test/lsp/mapping_test.dart
+++ b/pkg/analysis_server/test/lsp/mapping_test.dart
@@ -74,18 +74,35 @@
expect(result, isNull);
}
- Future<void> test_selectionsInSnippets_empty() async {
- var result = lsp.buildSnippetStringWithSelection('teststring', 4, 0);
- expect(result, equals(r'test${1:}string'));
+ Future<void> test_tabStopsInSnippets_contains() async {
+ var result = lsp.buildSnippetStringWithTabStops('a, b, c', [3, 1]);
+ expect(result, equals(r'a, ${1:b}, c'));
}
- Future<void> test_selectionsInSnippets_escaping() async {
- var result = lsp.buildSnippetStringWithSelection(r'te$tstri}ng', 4, 3);
- expect(result, equals(r'te\$t${1:str}i\}ng'));
+ Future<void> test_tabStopsInSnippets_empty() async {
+ var result = lsp.buildSnippetStringWithTabStops('a, b', []);
+ expect(result, equals(r'a, b'));
}
- Future<void> test_selectionsInSnippets_selection() async {
- var result = lsp.buildSnippetStringWithSelection('teststring', 4, 3);
- expect(result, equals(r'test${1:str}ing'));
+ Future<void> test_tabStopsInSnippets_endsWith() async {
+ var result = lsp.buildSnippetStringWithTabStops('a, b', [3, 1]);
+ expect(result, equals(r'a, ${1:b}'));
+ }
+
+ Future<void> test_tabStopsInSnippets_escape() async {
+ var result = lsp.buildSnippetStringWithTabStops(
+ r'te$tstri}ng, te$tstri}ng, te$tstri}ng', [13, 11]);
+ expect(result, equals(r'te\$tstri\}ng, ${1:te\$tstri\}ng}, te\$tstri\}ng'));
+ }
+
+ Future<void> test_tabStopsInSnippets_multiple() async {
+ var result =
+ lsp.buildSnippetStringWithTabStops('a, b, c', [0, 1, 3, 1, 6, 1]);
+ expect(result, equals(r'${1:a}, ${2:b}, ${3:c}'));
+ }
+
+ Future<void> test_tabStopsInSnippets_startsWith() async {
+ var result = lsp.buildSnippetStringWithTabStops('a, b', [0, 1]);
+ expect(result, equals(r'${1:a}, b'));
}
}
diff --git a/pkg/analysis_server/test/src/services/correction/fix/replace_with_var_test.dart b/pkg/analysis_server/test/src/services/correction/fix/replace_with_var_test.dart
index d069876..422034e 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/replace_with_var_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/replace_with_var_test.dart
@@ -74,6 +74,21 @@
''');
}
+ Future<void> test_generic_instanceCreation_cascade() async {
+ await resolveTestCode('''
+Set f() {
+ Set<String> s = Set<String>()..addAll([]);
+ return s;
+}
+''');
+ await assertHasFix('''
+Set f() {
+ var s = Set<String>()..addAll([]);
+ return s;
+}
+''');
+ }
+
Future<void> test_generic_instanceCreation_withArguments() async {
await resolveTestCode('''
C<int> f() {
@@ -193,6 +208,21 @@
await assertNoFix();
}
+ Future<void> test_generic_setLiteral_cascade() async {
+ await resolveTestCode('''
+Set f() {
+ Set<String> s = {}..addAll([]);
+ return s;
+}
+''');
+ await assertHasFix('''
+Set f() {
+ var s = <String>{}..addAll([]);
+ return s;
+}
+''');
+ }
+
Future<void> test_generic_setLiteral_const() async {
await resolveTestCode('''
String f() {
diff --git a/pkg/analysis_server/tool/lsp_spec/README.md b/pkg/analysis_server/tool/lsp_spec/README.md
index 051abb3..e95c70a 100644
--- a/pkg/analysis_server/tool/lsp_spec/README.md
+++ b/pkg/analysis_server/tool/lsp_spec/README.md
@@ -31,6 +31,7 @@
- `dart.enableSdkFormatter`: When set to `false`, prevents registration (or unregisters) the SDK formatter. When set to `true` or not supplied, will register/reregister the SDK formatter.
- `dart.lineLength`: The number of characters the formatter should wrap code at. If unspecified, code will be wrapped at `80` characters.
+- `dart.completeFunctionCalls`: Completes functions/methods with their required parameters.
## Method Status
diff --git a/pkg/dartdev/pubspec.yaml b/pkg/dartdev/pubspec.yaml
index e5b9295..aea30a3 100644
--- a/pkg/dartdev/pubspec.yaml
+++ b/pkg/dartdev/pubspec.yaml
@@ -23,7 +23,7 @@
pedantic: ^1.9.0
pub:
path: ../../third_party/pkg/pub
- stagehand: 3.3.7
+ stagehand: any
telemetry:
path: ../telemetry
usage: ^3.4.0
diff --git a/pkg/nnbd_migration/lib/src/messages.dart b/pkg/nnbd_migration/lib/src/messages.dart
index f4796c1..3f9b048 100644
--- a/pkg/nnbd_migration/lib/src/messages.dart
+++ b/pkg/nnbd_migration/lib/src/messages.dart
@@ -34,7 +34,7 @@
Please upgrade the packages containing these libraries to null safe versions
before continuing. To see what null safe package versions are available, run
-the following command: `dart pub outdated --mode=null-safety --prereleases`.
+the following command: `dart pub outdated --mode=null-safety`.
To skip this check and try to migrate anyway, re-run with the flag
`$_skipImportCheckFlag`.
diff --git a/runtime/lib/isolate.cc b/runtime/lib/isolate.cc
index 1ff4a38..94de7aa 100644
--- a/runtime/lib/isolate.cc
+++ b/runtime/lib/isolate.cc
@@ -76,6 +76,15 @@
return Integer::New(id);
}
+DEFINE_NATIVE_ENTRY(RawReceivePortImpl_setActive, 0, 2) {
+ GET_NON_NULL_NATIVE_ARGUMENT(ReceivePort, port, arguments->NativeArgAt(0));
+ GET_NON_NULL_NATIVE_ARGUMENT(Bool, active, arguments->NativeArgAt(1));
+ Dart_Port id = port.Id();
+ PortMap::SetPortState(
+ id, active.value() ? PortMap::kLivePort : PortMap::kInactivePort);
+ return Object::null();
+}
+
DEFINE_NATIVE_ENTRY(SendPortImpl_get_id, 0, 1) {
GET_NON_NULL_NATIVE_ARGUMENT(SendPort, port, arguments->NativeArgAt(0));
return Integer::New(port.Id());
diff --git a/runtime/observatory/tests/service/validate_timer_port_behavior_test.dart b/runtime/observatory/tests/service/validate_timer_port_behavior_test.dart
new file mode 100644
index 0000000..ae40cb2
--- /dev/null
+++ b/runtime/observatory/tests/service/validate_timer_port_behavior_test.dart
@@ -0,0 +1,74 @@
+// Copyright (c) 2020, 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 'dart:async';
+import 'dart:developer';
+import 'dart:isolate' hide Isolate;
+import 'package:observatory/service_io.dart';
+import 'package:test/test.dart';
+
+import 'service_test_common.dart';
+import 'test_helper.dart';
+
+void warmup() {
+ Timer timer = Timer(const Duration(days: 30), () => null);
+ debugger();
+ timer.cancel();
+ debugger();
+ timer = Timer(const Duration(days: 30), () => null);
+ debugger();
+ timer.cancel();
+}
+
+late Set<int> originalPortIds;
+late int timerPortId;
+
+final tests = <IsolateTest>[
+ hasPausedAtStart,
+ (Isolate isolate) async {
+ final originalPorts =
+ (await isolate.invokeRpcNoUpgrade('getPorts', {}))['ports'];
+ originalPortIds = {
+ for (int i = 0; i < originalPorts.length; ++i) originalPorts[i]['portId'],
+ };
+ },
+ resumeIsolate,
+ hasStoppedAtBreakpoint,
+ (Isolate isolate) async {
+ // Determine the ID of the timer port.
+ final ports = (await isolate.invokeRpcNoUpgrade('getPorts', {}))['ports'];
+ timerPortId = ports
+ .firstWhere((p) => !originalPortIds.contains(p['portId']))['portId'];
+ },
+ resumeIsolate,
+ hasStoppedAtBreakpoint,
+ (Isolate isolate) async {
+ // After cancelling the timer, there should be no active timers left.
+ // The timer port should be inactive and not reported.
+ final ports = (await isolate.invokeRpcNoUpgrade('getPorts', {}))['ports'];
+ for (final port in ports) {
+ if (port['portId'] == timerPortId) {
+ fail('Timer port should no longer be active');
+ }
+ }
+ },
+ resumeIsolate,
+ hasStoppedAtBreakpoint,
+ (Isolate isolate) async {
+ // After setting a new timer, the timer port should be active and have the same
+ // port ID as before as the original port is still being used.
+ final ports = (await isolate.invokeRpcNoUpgrade('getPorts', {}))['ports'];
+ bool foundTimerPort = false;
+ for (final port in ports) {
+ if (port['portId'] == timerPortId) {
+ foundTimerPort = true;
+ break;
+ }
+ }
+ expect(foundTimerPort, true);
+ },
+];
+
+main(args) async => runIsolateTests(args, tests,
+ pause_on_start: true, testeeConcurrent: warmup);
diff --git a/runtime/observatory_2/tests/service_2/validate_timer_port_behavior_test.dart b/runtime/observatory_2/tests/service_2/validate_timer_port_behavior_test.dart
new file mode 100644
index 0000000..fd91428
--- /dev/null
+++ b/runtime/observatory_2/tests/service_2/validate_timer_port_behavior_test.dart
@@ -0,0 +1,74 @@
+// Copyright (c) 2020, 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 'dart:async';
+import 'dart:developer';
+import 'dart:isolate' hide Isolate;
+import 'package:observatory_2/service_io.dart';
+import 'package:test/test.dart';
+
+import 'service_test_common.dart';
+import 'test_helper.dart';
+
+void warmup() {
+ Timer timer = Timer(const Duration(days: 30), () => null);
+ debugger();
+ timer.cancel();
+ debugger();
+ timer = Timer(const Duration(days: 30), () => null);
+ debugger();
+ timer.cancel();
+}
+
+Set<int> originalPortIds;
+int timerPortId;
+
+final tests = <IsolateTest>[
+ hasPausedAtStart,
+ (Isolate isolate) async {
+ final originalPorts =
+ (await isolate.invokeRpcNoUpgrade('getPorts', {}))['ports'];
+ originalPortIds = {
+ for (int i = 0; i < originalPorts.length; ++i) originalPorts[i]['portId'],
+ };
+ },
+ resumeIsolate,
+ hasStoppedAtBreakpoint,
+ (Isolate isolate) async {
+ // Determine the ID of the timer port.
+ final ports = (await isolate.invokeRpcNoUpgrade('getPorts', {}))['ports'];
+ timerPortId = ports
+ .firstWhere((p) => !originalPortIds.contains(p['portId']))['portId'];
+ },
+ resumeIsolate,
+ hasStoppedAtBreakpoint,
+ (Isolate isolate) async {
+ // After cancelling the timer, there should be no active timers left.
+ // The timer port should be inactive and not reported.
+ final ports = (await isolate.invokeRpcNoUpgrade('getPorts', {}))['ports'];
+ for (final port in ports) {
+ if (port['portId'] == timerPortId) {
+ fail('Timer port should no longer be active');
+ }
+ }
+ },
+ resumeIsolate,
+ hasStoppedAtBreakpoint,
+ (Isolate isolate) async {
+ // After setting a new timer, the timer port should be active and have the same
+ // port ID as before as the original port is still being used.
+ final ports = (await isolate.invokeRpcNoUpgrade('getPorts', {}))['ports'];
+ bool foundTimerPort = false;
+ for (final port in ports) {
+ if (port['portId'] == timerPortId) {
+ foundTimerPort = true;
+ break;
+ }
+ }
+ expect(foundTimerPort, true);
+ },
+];
+
+main(args) async => runIsolateTests(args, tests,
+ pause_on_start: true, testeeConcurrent: warmup);
diff --git a/runtime/vm/bootstrap_natives.h b/runtime/vm/bootstrap_natives.h
index 8b15f7b..c8ac080 100644
--- a/runtime/vm/bootstrap_natives.h
+++ b/runtime/vm/bootstrap_natives.h
@@ -58,6 +58,7 @@
V(RawReceivePortImpl_get_id, 1) \
V(RawReceivePortImpl_get_sendport, 1) \
V(RawReceivePortImpl_closeInternal, 1) \
+ V(RawReceivePortImpl_setActive, 2) \
V(SendPortImpl_get_id, 1) \
V(SendPortImpl_get_hashcode, 1) \
V(SendPortImpl_sendInternal_, 2) \
diff --git a/runtime/vm/heap/scavenger.cc b/runtime/vm/heap/scavenger.cc
index 0de6a9f..897bf85 100644
--- a/runtime/vm/heap/scavenger.cc
+++ b/runtime/vm/heap/scavenger.cc
@@ -762,6 +762,7 @@
Scavenger::~Scavenger() {
ASSERT(!scavenging_);
delete to_;
+ ASSERT(blocks_ == nullptr);
}
intptr_t Scavenger::NewSizeInWords(intptr_t old_size_in_words) const {
@@ -1024,9 +1025,8 @@
// Grab the deduplication sets out of the isolate's consolidated store buffer.
StoreBuffer* store_buffer = heap_->isolate_group()->store_buffer();
StoreBufferBlock* pending = blocks_;
- blocks_ = nullptr;
intptr_t total_count = 0;
- while (pending != NULL) {
+ while (pending != nullptr) {
StoreBufferBlock* next = pending->next();
// Generated code appends to store buffers; tell MemorySanitizer.
MSAN_UNPOISON(pending, sizeof(*pending));
@@ -1045,10 +1045,10 @@
pending->Reset();
// Return the emptied block for recycling (no need to check threshold).
store_buffer->PushBlock(pending, StoreBuffer::kIgnoreThreshold);
- pending = next;
+ blocks_ = pending = next;
}
// Done iterating through old objects remembered in the store buffers.
- visitor->VisitingOldObject(NULL);
+ visitor->VisitingOldObject(nullptr);
heap_->RecordData(kStoreBufferEntries, total_count);
heap_->RecordData(kDataUnused1, 0);
@@ -1645,9 +1645,23 @@
to_ = *from;
*from = temp;
+ // Release any remaining part of the promotion worklist that wasn't completed.
promotion_stack_.Reset();
- // This also rebuilds the remembered set.
+ // Release any remaining part of the rememebred set that wasn't completed.
+ StoreBuffer* store_buffer = heap_->isolate_group()->store_buffer();
+ StoreBufferBlock* pending = blocks_;
+ while (pending != nullptr) {
+ StoreBufferBlock* next = pending->next();
+ pending->Reset();
+ // Return the emptied block for recycling (no need to check threshold).
+ store_buffer->PushBlock(pending, StoreBuffer::kIgnoreThreshold);
+ pending = next;
+ }
+ blocks_ = nullptr;
+
+ // Reverse the partial forwarding from the aborted scavenge. This also
+ // rebuilds the remembered set.
Become::FollowForwardingPointers(thread);
// Don't scavenge again until the next old-space GC has occurred. Prevents
diff --git a/runtime/vm/heap/scavenger.h b/runtime/vm/heap/scavenger.h
index 9f84dcd..5e7486b 100644
--- a/runtime/vm/heap/scavenger.h
+++ b/runtime/vm/heap/scavenger.h
@@ -429,7 +429,7 @@
bool scavenging_;
bool early_tenure_ = false;
RelaxedAtomic<intptr_t> root_slices_started_;
- StoreBufferBlock* blocks_;
+ StoreBufferBlock* blocks_ = nullptr;
int64_t gc_time_micros_;
intptr_t collections_;
diff --git a/runtime/vm/isolate_reload.cc b/runtime/vm/isolate_reload.cc
index f9e26a0..4df27eb 100644
--- a/runtime/vm/isolate_reload.cc
+++ b/runtime/vm/isolate_reload.cc
@@ -1927,7 +1927,7 @@
visitor->VisitPointers(class_table, saved_num_cids_);
}
ClassPtr* saved_tlc_class_table =
- saved_class_table_.load(std::memory_order_relaxed);
+ saved_tlc_class_table_.load(std::memory_order_relaxed);
if (saved_tlc_class_table != NULL) {
auto class_table =
reinterpret_cast<ObjectPtr*>(&(saved_tlc_class_table[0]));
diff --git a/runtime/vm/port.cc b/runtime/vm/port.cc
index 9d5c331..c7312de 100644
--- a/runtime/vm/port.cc
+++ b/runtime/vm/port.cc
@@ -29,6 +29,8 @@
return "live";
case kControlPort:
return "control";
+ case kInactivePort:
+ return "inactive";
default:
UNREACHABLE();
return "UNKNOWN";
@@ -72,10 +74,11 @@
Entry& entry = *it;
PortState old_state = entry.state;
- ASSERT(old_state == kNewPort);
entry.state = state;
if (state == kLivePort) {
entry.handler->increment_live_ports();
+ } else if (state == kInactivePort && old_state == kLivePort) {
+ entry.handler->decrement_live_ports();
}
if (FLAG_trace_isolates) {
OS::PrintErr(
@@ -211,6 +214,18 @@
return handler->IsCurrentIsolate();
}
+bool PortMap::IsLivePort(Dart_Port id) {
+ MutexLocker ml(mutex_);
+ auto it = ports_->TryLookup(id);
+ if (it == ports_->end()) {
+ // Port does not exist.
+ return false;
+ }
+
+ PortState state = (*it).state;
+ return (state == kLivePort || state == kControlPort);
+}
+
Isolate* PortMap::GetIsolate(Dart_Port id) {
MutexLocker ml(mutex_);
auto it = ports_->TryLookup(id);
diff --git a/runtime/vm/port.h b/runtime/vm/port.h
index 0297f1d..1405362 100644
--- a/runtime/vm/port.h
+++ b/runtime/vm/port.h
@@ -28,6 +28,8 @@
kNewPort = 0, // a newly allocated port
kLivePort = 1, // a regular port (has a ReceivePort)
kControlPort = 2, // a special control port (has a ReceivePort)
+ kInactivePort =
+ 3, // an inactive port (has a ReceivePort) not considered live.
};
// Allocate a port for the provided handler and return its VM-global id.
@@ -55,6 +57,9 @@
// Returns whether a port is local to the current isolate.
static bool IsLocalPort(Dart_Port id);
+ // Returns whether a port is live (e.g., is not new or inactive).
+ static bool IsLivePort(Dart_Port id);
+
// Returns the owning Isolate for port 'id'.
static Isolate* GetIsolate(Dart_Port id);
@@ -84,9 +89,6 @@
// Allocate a new unique port.
static Dart_Port AllocatePort();
- static bool IsActivePort(Dart_Port id);
- static bool IsLivePort(Dart_Port id);
-
// Lock protecting access to the port map.
static Mutex* mutex_;
diff --git a/runtime/vm/port_test.cc b/runtime/vm/port_test.cc
index 2fb63d7..afcbb7b 100644
--- a/runtime/vm/port_test.cc
+++ b/runtime/vm/port_test.cc
@@ -106,6 +106,11 @@
EXPECT(PortMapTestPeer::IsActivePort(port));
EXPECT(PortMapTestPeer::IsLivePort(port));
+ // Inactive port.
+ PortMap::SetPortState(port, PortMap::kInactivePort);
+ EXPECT(PortMapTestPeer::IsActivePort(port));
+ EXPECT(!PortMapTestPeer::IsLivePort(port));
+
PortMap::ClosePort(port);
EXPECT(!PortMapTestPeer::IsActivePort(port));
EXPECT(!PortMapTestPeer::IsLivePort(port));
diff --git a/runtime/vm/service.cc b/runtime/vm/service.cc
index b207114..fee9fbb 100644
--- a/runtime/vm/service.cc
+++ b/runtime/vm/service.cc
@@ -3178,11 +3178,14 @@
JSONObject jsobj(js);
jsobj.AddProperty("type", "PortList");
{
- Instance& port = Instance::Handle(zone.GetZone());
+ ReceivePort& port = ReceivePort::Handle(zone.GetZone());
JSONArray arr(&jsobj, "ports");
for (int i = 0; i < ports.Length(); ++i) {
port ^= ports.At(i);
- arr.AddValue(port);
+ // Don't report inactive ports.
+ if (PortMap::IsLivePort(port.Id())) {
+ arr.AddValue(port);
+ }
}
}
return true;
diff --git a/sdk/lib/_internal/vm/lib/isolate_patch.dart b/sdk/lib/_internal/vm/lib/isolate_patch.dart
index 97e90ce..187efdb 100644
--- a/sdk/lib/_internal/vm/lib/isolate_patch.dart
+++ b/sdk/lib/_internal/vm/lib/isolate_patch.dart
@@ -188,6 +188,12 @@
// Call into the VM to close the VM maintained mappings.
_closeInternal() native "RawReceivePortImpl_closeInternal";
+ // Set this port as active or inactive in the VM. If inactive, this port
+ // will not be considered live even if it hasn't been explicitly closed.
+ // TODO(bkonyi): determine if we want to expose this as an option through
+ // RawReceivePort.
+ _setActive(bool active) native "RawReceivePortImpl_setActive";
+
void set handler(Function? value) {
final id = this._get_id();
if (!_portMap.containsKey(id)) {
diff --git a/sdk/lib/_internal/vm/lib/timer_impl.dart b/sdk/lib/_internal/vm/lib/timer_impl.dart
index db626c2..a348d19 100644
--- a/sdk/lib/_internal/vm/lib/timer_impl.dart
+++ b/sdk/lib/_internal/vm/lib/timer_impl.dart
@@ -138,6 +138,7 @@
static RawReceivePort? _receivePort;
static SendPort? _sendPort;
+ static bool _receivePortActive = false;
static int _scheduledWakeupTime = 0;
static bool _handlingCallbacks = false;
@@ -262,12 +263,10 @@
// Enqueue one message for each zero timer. To be able to distinguish from
// EventHandler messages we send a _ZERO_EVENT instead of a _TIMEOUT_EVENT.
static void _notifyZeroHandler() {
- var port = _sendPort;
- if (port == null) {
- port = _createTimerHandler();
- _sendPort = port;
+ if (!_receivePortActive) {
+ _createTimerHandler();
}
- port.send(_ZERO_EVENT);
+ _sendPort!.send(_ZERO_EVENT);
}
// Handle the notification of a zero timer. Make sure to also execute non-zero
@@ -314,7 +313,6 @@
_cancelWakeup();
return;
}
-
// Only send a message if the requested wakeup time differs from the
// already scheduled wakeup time.
var wakeupTime = _heap.first._wakeupTime;
@@ -433,12 +431,10 @@
// Tell the event handler to wake this isolate at a specific time.
static void _scheduleWakeup(int wakeupTime) {
- var port = _sendPort;
- if (port == null) {
- port = _createTimerHandler();
- _sendPort = port;
+ if (!_receivePortActive) {
+ _createTimerHandler();
}
- VMLibraryHooks.eventHandlerSendData(null, port, wakeupTime);
+ VMLibraryHooks.eventHandlerSendData(null, _sendPort, wakeupTime);
_scheduledWakeupTime = wakeupTime;
}
@@ -452,22 +448,23 @@
// Create a receive port and register a message handler for the timer
// events.
- static SendPort _createTimerHandler() {
- assert(_receivePort == null);
- assert(_sendPort == null);
- final port = new RawReceivePort(_handleMessage);
- final sendPort = port.sendPort;
- _receivePort = port;
- _sendPort = sendPort;
- _scheduledWakeupTime = 0;
- return sendPort;
+ static void _createTimerHandler() {
+ if (_receivePort == null) {
+ assert(_receivePort == null);
+ assert(_sendPort == null);
+ _receivePort = RawReceivePort(_handleMessage);
+ _sendPort = _receivePort!.sendPort;
+ _scheduledWakeupTime = 0;
+ } else {
+ (_receivePort as _RawReceivePortImpl)._setActive(true);
+ }
+ _receivePortActive = true;
}
static void _shutdownTimerHandler() {
- _sendPort = null;
_scheduledWakeupTime = 0;
- _receivePort!.close();
- _receivePort = null;
+ (_receivePort as _RawReceivePortImpl)._setActive(false);
+ _receivePortActive = false;
}
// The Timer factory registered with the dart:async library by the embedder.
diff --git a/sdk/lib/html/doc/WORKAROUNDS.md b/sdk/lib/html/doc/WORKAROUNDS.md
new file mode 100644
index 0000000..d3d8456
--- /dev/null
+++ b/sdk/lib/html/doc/WORKAROUNDS.md
@@ -0,0 +1,128 @@
+Dart web platform libraries e.g. `dart:html` is partially hand-written and
+partially generated, with the code generation using the Chrome IDL as the source
+of truth for many browser interfaces. This introduces a dependency on the
+version of the IDL and doesn’t always match up with other browser interfaces.
+
+Currently, we do not intend on updating our scripts to use a newer version of
+the IDL, so APIs and classes in these libraries may be inaccurate.
+
+In order to work around this, we ask users to leverage JS interop. Longer term,
+we intend to revamp our web library offerings to be more robust and reliable.
+
+The following are workarounds to common issues you might see with using the web
+platform libraries.
+
+## Common Issues
+
+### Missing/broken APIs
+
+As mentioned above, there exists stale interfaces. While some of these may be
+fixed in the source code, many might not.
+
+In order to circumvent this, you can use the `js_util` library, like
+`getProperty`, `setProperty`, `callMethod`, and `callConstructor`.
+
+Let’s look at an example. `FileReader` is a `dart:html` interface that is
+missing the API `readAsBinaryString` ([#42834][]). We can work around this by
+doing something like the following:
+
+```
+import 'dart:html';
+import 'dart:js_util' as js_util;
+
+import 'package:async_helper/async_minitest.dart';
+import 'package:expect/expect.dart';
+
+void main() async {
+ var reader = new FileReader();
+ reader.onLoad.listen(expectAsync((event) {
+ String result = reader.result as String;
+ Expect.equals(result, '00000000');
+ }));
+ js_util.callMethod(reader, 'readAsBinaryString', [new Blob(['00000000'])]);
+ // We can manipulate properties as well.
+ js_util.setProperty(reader, 'foo', 'bar'); // reader.foo is now ‘bar’
+ Expect.equals(js_util.getProperty(reader, 'foo'), 'bar');
+}
+```
+
+In the case where the API is missing a constructor, we can use
+`callConstructor`. For example, instead of using the factory constructor for
+`KeyboardEvent`, we can do the following:
+
+```
+import 'dart:html';
+import 'dart:js_util' as js_util;
+
+import 'package:expect/expect.dart';
+
+void main() {
+ List<dynamic> eventArgs = <dynamic>[
+ 'KeyboardEvent',
+ <String, dynamic>{'key': 'A'}
+ ];
+ KeyboardEvent event = js_util.callConstructor(
+ js_util.getProperty(window, 'KeyboardEvent'), js_util.jsify(eventArgs));
+ Expect.equals(event.key, 'A');
+}
+```
+
+### Private/unimplemented native types
+
+There are several native interfaces that are suppressed e.g.
+`USBDevice` ([#42200][]) due to historical reasons. These native interfaces are
+marked with `@Native`, are private, and have no attributes associated with them.
+Therefore, unlike other `@Native` objects, we can’t access any of the APIs or
+attributes associated with this interface. We can use the `js_util` library
+again to circumvent this issue. For example, we can manipulate a
+`_SubtleCrypto` object:
+
+```
+import 'dart:html';
+import 'dart:js_util' as js_util;
+import 'dart:typed_data';
+
+import 'package:js/js.dart';
+
+@JS()
+external Crypto get crypto;
+
+void main() async {
+ var subtle = crypto.subtle!;
+ var array = Uint8List(16);
+ var promise = js_util.promiseToFuture<ByteBuffer>(js_util
+ .callMethod(subtle, 'digest', ['SHA-256', array])); // SubtleCrypto.digest
+ var digest = await promise;
+}
+```
+
+What you shouldn’t do is attempt to cast these native objects using your own JS
+interop types, e.g.
+
+```
+import 'dart:html';
+
+import 'package:js/js.dart';
+
+@JS()
+external Crypto get crypto;
+
+@JS()
+class SubtleCrypto {}
+
+void main() {
+ SubtleCrypto subtle = crypto.subtle! as SubtleCrypto;
+}
+```
+
+With the above, you’ll see a type error:
+
+`Uncaught TypeError: Instance of 'SubtleCrypto': type 'Interceptor' is not a subtype of type 'SubtleCrypto'`
+
+This is because the types in the `@Native` annotation are reserved and the above
+leads to namespace conflicts between the `@Native` type and the user JS interop
+type in the compiler. These `@Native` types inherit the `Interceptor` class,
+which is why you see the message above.
+
+[#42834]: https://github.com/dart-lang/sdk/issues/42834
+[#42200]: https://github.com/dart-lang/sdk/issues/42200
diff --git a/tools/VERSION b/tools/VERSION
index 31011dd..648028b 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
MAJOR 2
MINOR 12
PATCH 0
-PRERELEASE 32
+PRERELEASE 33
PRERELEASE_PATCH 0
\ No newline at end of file