Version 2.13.0-69.0.dev
Merge commit 'a7bf05f64dd7e82deb22b24a7d23610f9916770b' into 'dev'
diff --git a/.dart_tool/package_config.json b/.dart_tool/package_config.json
index d3231e9..8400a1c 100644
--- a/.dart_tool/package_config.json
+++ b/.dart_tool/package_config.json
@@ -11,7 +11,7 @@
"constraint, update this by running tools/generate_package_config.dart."
],
"configVersion": 2,
- "generated": "2021-02-23T10:05:38.675159",
+ "generated": "2021-02-23T11:38:53.873726",
"generator": "tools/generate_package_config.dart",
"packages": [
{
@@ -458,7 +458,7 @@
"name": "oauth2",
"rootUri": "../third_party/pkg/oauth2",
"packageUri": "lib/",
- "languageVersion": "2.0"
+ "languageVersion": "2.12"
},
{
"name": "observatory",
diff --git a/DEPS b/DEPS
index 022463e..6c57713 100644
--- a/DEPS
+++ b/DEPS
@@ -126,7 +126,7 @@
"mime_rev": "c931f4bed87221beaece356494b43731445ce7b8",
"mockito_rev": "d39ac507483b9891165e422ec98d9fb480037c8b",
"mustache_rev": "664737ecad027e6b96d0d1e627257efa0e46fcb1",
- "oauth2_rev": "95b6c8d96dc37a1723480961665d3477a46dd303",
+ "oauth2_rev": "9b8ee16d0dd4538173422a01532f33fc8be2548f",
"package_config_rev": "249af482de9ebabfc781bf10d6152c938e5ce45e",
"path_rev": "407ab76187fade41c31e39c745b39661b710106c",
"pedantic_rev": "df177f6ae531426aaf7bbf0121c90dc89d9c57bf",
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/for_in_loop_type_not_iterable_nullability_error.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/for_in_loop_type_not_iterable_nullability_error.dart
new file mode 100644
index 0000000..4c0d40f
--- /dev/null
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/for_in_loop_type_not_iterable_nullability_error.dart
@@ -0,0 +1,21 @@
+// 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.
+
+// This test contains a test case for each condition that can lead to the front
+// end's `ForInLoopTypeNotIterableNullability` or
+// `ForInLoopTypeNotIterablePartNullability` errors, for which we wish to report
+// "why not promoted" context information.
+
+// TODO(paulberry): get this to work with the CFE and add additional test cases
+// if needed.
+
+class C1 {
+ List<int>? bad;
+}
+
+test(C1 c) {
+ if (c.bad == null) return;
+ for (var x
+ in /*analyzer.notPromoted(propertyNotPromoted(member:C1.bad))*/ c.bad) {}
+}
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/invalid_assignment_error_nullability_error.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/invalid_assignment_error_nullability_error.dart
new file mode 100644
index 0000000..b8e021a
--- /dev/null
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/invalid_assignment_error_nullability_error.dart
@@ -0,0 +1,20 @@
+// 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.
+
+// This test contains a test case for each condition that can lead to the front
+// end's `InvalidAssignmentErrorNullability` or
+// `InvalidAssignmentErrorPartNullability` errors, for which we wish to report
+// "why not promoted" context information.
+
+// TODO(paulberry): get this to work with the CFE and add additional test cases
+// if needed.
+
+class C1 {
+ List<int>? bad;
+}
+
+test(C1 c) sync* {
+ if (c.bad == null) return;
+ yield* /*analyzer.notPromoted(propertyNotPromoted(member:C1.bad))*/ c.bad;
+}
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/nullable_expression_call_error.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/nullable_expression_call_error.dart
index c603def..3e33316 100644
--- a/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/nullable_expression_call_error.dart
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/nullable_expression_call_error.dart
@@ -16,7 +16,7 @@
instance_method_invocation(C1 c) {
if (c.bad == null) return;
- c.bad
+ /*analyzer.notPromoted(propertyNotPromoted(member:C1.bad))*/ c.bad
/*cfe.invoke: notPromoted(propertyNotPromoted(member:C1.bad))*/
();
}
@@ -42,7 +42,7 @@
if (c.ok == null) return;
c.ok();
if (c.bad == null) return;
- c.bad
+ /*analyzer.notPromoted(propertyNotPromoted(member:C3.bad))*/ c.bad
/*cfe.invoke: notPromoted(propertyNotPromoted(member:C3.bad))*/
();
}
@@ -57,7 +57,7 @@
instance_getter_invocation(C6 c) {
if (c.bad == null) return;
- c.bad
+ /*analyzer.notPromoted(propertyNotPromoted(member:C6.bad))*/ c.bad
/*cfe.invoke: notPromoted(propertyNotPromoted(member:C6.bad))*/
();
}
@@ -83,7 +83,7 @@
if (c.ok == null) return;
c.ok();
if (c.bad == null) return;
- c.bad
+ /*analyzer.notPromoted(propertyNotPromoted(member:C8.bad))*/ c.bad
/*cfe.invoke: notPromoted(propertyNotPromoted(member:C8.bad))*/
();
}
@@ -94,7 +94,7 @@
function_invocation(C11 c) {
if (c.bad == null) return;
- c.bad
+ /*analyzer.notPromoted(propertyNotPromoted(member:C11.bad))*/ c.bad
/*cfe.invoke: notPromoted(propertyNotPromoted(member:C11.bad))*/
();
}
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/nullable_spread_error.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/nullable_spread_error.dart
new file mode 100644
index 0000000..d3f6451
--- /dev/null
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/nullable_spread_error.dart
@@ -0,0 +1,21 @@
+// 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.
+
+// This test contains a test case for each condition that can lead to the front
+// end's `NullableSpreadError` error, for which we wish to report "why not
+// promoted" context information.
+
+// TODO(paulberry): get this to work with the CFE and add additional test cases
+// if needed.
+
+class C1 {
+ List<int>? bad;
+}
+
+test(C1 c) {
+ if (c.bad == null) return;
+ return [
+ ... /*analyzer.notPromoted(propertyNotPromoted(member:C1.bad))*/ c.bad
+ ];
+}
diff --git a/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart b/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart
index 16daa2d..6a1ba57 100644
--- a/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart
+++ b/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart
@@ -293,6 +293,7 @@
{@required this.libId,
@required this.displayUri,
@required this.rOffset,
+ @required this.iLength,
@required this.rLength,
@required this.file,
@required this.offset}) {
@@ -305,6 +306,9 @@
if (rOffset == null) {
throw 'rOffset is required but was not provided';
}
+ if (iLength == null) {
+ throw 'iLength is required but was not provided';
+ }
if (rLength == null) {
throw 'rLength is required but was not provided';
}
@@ -319,6 +323,7 @@
final libId = json['libId'];
final displayUri = json['displayUri'];
final rOffset = json['rOffset'];
+ final iLength = json['iLength'];
final rLength = json['rLength'];
final file = json['file'];
final offset = json['offset'];
@@ -326,6 +331,7 @@
libId: libId,
displayUri: displayUri,
rOffset: rOffset,
+ iLength: iLength,
rLength: rLength,
file: file,
offset: offset);
@@ -333,6 +339,7 @@
final String displayUri;
final String file;
+ final num iLength;
final num libId;
final num offset;
final num rLength;
@@ -345,6 +352,8 @@
displayUri ?? (throw 'displayUri is required but was not set');
__result['rOffset'] =
rOffset ?? (throw 'rOffset is required but was not set');
+ __result['iLength'] =
+ iLength ?? (throw 'iLength is required but was not set');
__result['rLength'] =
rLength ?? (throw 'rLength is required but was not set');
__result['file'] = file ?? (throw 'file is required but was not set');
@@ -405,6 +414,23 @@
} finally {
reporter.pop();
}
+ reporter.push('iLength');
+ try {
+ if (!obj.containsKey('iLength')) {
+ reporter.reportError('must not be undefined');
+ return false;
+ }
+ if (obj['iLength'] == null) {
+ reporter.reportError('must not be null');
+ return false;
+ }
+ if (!(obj['iLength'] is num)) {
+ reporter.reportError('must be of type num');
+ return false;
+ }
+ } finally {
+ reporter.pop();
+ }
reporter.push('rLength');
try {
if (!obj.containsKey('rLength')) {
@@ -470,6 +496,7 @@
return libId == other.libId &&
displayUri == other.displayUri &&
rOffset == other.rOffset &&
+ iLength == other.iLength &&
rLength == other.rLength &&
file == other.file &&
offset == other.offset &&
@@ -484,6 +511,7 @@
hash = JenkinsSmiHash.combine(hash, libId.hashCode);
hash = JenkinsSmiHash.combine(hash, displayUri.hashCode);
hash = JenkinsSmiHash.combine(hash, rOffset.hashCode);
+ hash = JenkinsSmiHash.combine(hash, iLength.hashCode);
hash = JenkinsSmiHash.combine(hash, rLength.hashCode);
hash = JenkinsSmiHash.combine(hash, file.hashCode);
hash = JenkinsSmiHash.combine(hash, offset.hashCode);
diff --git a/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart b/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart
index 251eafe..3438d4b 100644
--- a/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart
+++ b/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart
@@ -6144,8 +6144,17 @@
final insertTextMode = json['insertTextMode'] != null
? InsertTextMode.fromJson(json['insertTextMode'])
: null;
- final textEdit =
- json['textEdit'] != null ? TextEdit.fromJson(json['textEdit']) : null;
+ final textEdit = TextEdit.canParse(json['textEdit'], nullLspJsonReporter)
+ ? Either2<TextEdit, InsertReplaceEdit>.t1(json['textEdit'] != null
+ ? TextEdit.fromJson(json['textEdit'])
+ : null)
+ : (InsertReplaceEdit.canParse(json['textEdit'], nullLspJsonReporter)
+ ? Either2<TextEdit, InsertReplaceEdit>.t2(json['textEdit'] != null
+ ? InsertReplaceEdit.fromJson(json['textEdit'])
+ : null)
+ : (json['textEdit'] == null
+ ? null
+ : (throw '''${json['textEdit']} was not one of (TextEdit, InsertReplaceEdit)''')));
final additionalTextEdits = json['additionalTextEdits']
?.map((item) => item != null ? TextEdit.fromJson(item) : null)
?.cast<TextEdit>()
@@ -6282,7 +6291,7 @@
/// must be a prefix of the edit's replace range, that means it must be
/// contained and starting at the same position.
/// @since 3.16.0 additional type `InsertReplaceEdit`
- final TextEdit textEdit;
+ final Either2<TextEdit, InsertReplaceEdit> textEdit;
Map<String, dynamic> toJson() {
var __result = <String, dynamic>{};
@@ -6321,7 +6330,7 @@
__result['insertTextMode'] = insertTextMode.toJson();
}
if (textEdit != null) {
- __result['textEdit'] = textEdit.toJson();
+ __result['textEdit'] = textEdit;
}
if (additionalTextEdits != null) {
__result['additionalTextEdits'] = additionalTextEdits;
@@ -6468,8 +6477,10 @@
reporter.push('textEdit');
try {
if (obj['textEdit'] != null &&
- !(TextEdit.canParse(obj['textEdit'], reporter))) {
- reporter.reportError('must be of type TextEdit');
+ !((TextEdit.canParse(obj['textEdit'], reporter) ||
+ InsertReplaceEdit.canParse(obj['textEdit'], reporter)))) {
+ reporter.reportError(
+ 'must be of type Either2<TextEdit, InsertReplaceEdit>');
return false;
}
} finally {
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 ba9c66d..b84ae47 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
@@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
import 'dart:collection';
+import 'dart:math' as math;
import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
@@ -183,6 +184,17 @@
return alreadyImportedSymbols;
}
+ /// The insert length is the shorter of the replacementLength or the
+ /// difference between the replacementOffset and the caret position.
+ int _computeInsertLength(
+ int offset, int replacementOffset, int replacementLength) {
+ final insertLength =
+ math.min(offset - replacementOffset, replacementLength);
+ assert(insertLength >= 0);
+ assert(insertLength <= replacementLength);
+ return insertLength;
+ }
+
String _createImportedSymbolKey(String name, Uri declaringUri) =>
'$name/$declaringUri';
@@ -205,6 +217,7 @@
completionCapabilities,
clientSupportedCompletionKinds,
lineInfo,
+ offset,
pluginResults,
).toList());
}
@@ -252,6 +265,12 @@
completionRequest,
);
+ final insertLength = _computeInsertLength(
+ offset,
+ completionRequest.replacementOffset,
+ completionRequest.replacementLength,
+ );
+
if (token.isCancellationRequested) {
return cancelled();
}
@@ -264,6 +283,7 @@
unit.lineInfo,
item,
completionRequest.replacementOffset,
+ insertLength,
completionRequest.replacementLength,
// TODO(dantup): Including commit characters in every completion
// increases the payload size. The LSP spec is ambigious
@@ -358,6 +378,7 @@
unit.lineInfo,
item,
completionRequest.replacementOffset,
+ insertLength,
completionRequest.replacementLength,
// TODO(dantup): Including commit characters in every completion
// increases the payload size. The LSP spec is ambigious
@@ -401,6 +422,11 @@
CancellationToken token,
) async {
final suggestions = generator.getSuggestions(path, offset);
+ final insertLength = _computeInsertLength(
+ offset,
+ suggestions.replacementOffset,
+ suggestions.replacementLength,
+ );
final completionItems = suggestions.suggestions
.map(
(item) => toCompletionItem(
@@ -409,6 +435,7 @@
lineInfo,
item,
suggestions.replacementOffset,
+ insertLength,
suggestions.replacementLength,
includeCommitCharacters: false,
completeFunctionCalls: false,
@@ -422,6 +449,7 @@
CompletionClientCapabilities completionCapabilities,
HashSet<CompletionItemKind> clientSupportedCompletionKinds,
LineInfo lineInfo,
+ int offset,
List<plugin.CompletionGetSuggestionsResult> pluginResults,
) {
return pluginResults.expand((result) {
@@ -432,6 +460,11 @@
lineInfo,
item,
result.replacementOffset,
+ _computeInsertLength(
+ offset,
+ result.replacementOffset,
+ result.replacementLength,
+ ),
result.replacementLength,
// Plugins cannot currently contribute commit characters and we should
// not assume that the Dart ones would be correct for all of their
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion_resolve.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion_resolve.dart
index 3157a79..dcf76e2 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion_resolve.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion_resolve.dart
@@ -156,6 +156,9 @@
// Documentation is added on during resolve for LSP.
final formats = server.clientCapabilities?.textDocument?.completion
?.completionItem?.documentationFormat;
+ final supportsInsertReplace = server.clientCapabilities?.textDocument
+ ?.completion?.completionItem?.insertReplaceSupport ==
+ true;
final dartDoc =
analyzer.getDartDocPlainText(requestedElement.documentationComment);
final documentation = asStringOrMarkupContent(formats, dartDoc);
@@ -175,10 +178,20 @@
filterText: item.filterText,
insertText: newInsertText,
insertTextFormat: item.insertTextFormat,
- textEdit: TextEdit(
- range: toRange(lineInfo, data.rOffset, data.rLength),
- newText: newInsertText,
- ),
+ textEdit: supportsInsertReplace
+ ? Either2<TextEdit, InsertReplaceEdit>.t2(
+ InsertReplaceEdit(
+ insert: toRange(lineInfo, data.rOffset, data.iLength),
+ replace: toRange(lineInfo, data.rOffset, data.rLength),
+ newText: newInsertText,
+ ),
+ )
+ : Either2<TextEdit, InsertReplaceEdit>.t1(
+ TextEdit(
+ range: toRange(lineInfo, data.rOffset, data.rLength),
+ newText: newInsertText,
+ ),
+ ),
additionalTextEdits: thisFilesChanges
.expand((change) =>
change.edits.map((edit) => toTextEdit(lineInfo, edit)))
diff --git a/pkg/analysis_server/lib/src/lsp/mapping.dart b/pkg/analysis_server/lib/src/lsp/mapping.dart
index 87231e2..f7500f4 100644
--- a/pkg/analysis_server/lib/src/lsp/mapping.dart
+++ b/pkg/analysis_server/lib/src/lsp/mapping.dart
@@ -220,6 +220,7 @@
server.LineInfo lineInfo,
dec.Declaration declaration,
int replacementOffset,
+ int insertLength,
int replacementLength, {
@required bool includeCommitCharacters,
@required bool completeFunctionCalls,
@@ -341,6 +342,7 @@
libId: includedSuggestionSet.id,
displayUri: includedSuggestionSet.displayUri ?? library.uri?.toString(),
rOffset: replacementOffset,
+ iLength: insertLength,
rLength: replacementLength),
);
}
@@ -816,6 +818,7 @@
server.LineInfo lineInfo,
server.CompletionSuggestion suggestion,
int replacementOffset,
+ int insertLength,
int replacementLength, {
@required bool includeCommitCharacters,
@required bool completeFunctionCalls,
@@ -860,6 +863,8 @@
final formats = completionCapabilities?.completionItem?.documentationFormat;
final supportsSnippets =
completionCapabilities?.completionItem?.snippetSupport == true;
+ final supportsInsertReplace =
+ completionCapabilities?.completionItem?.insertReplaceSupport == true;
final completionKind = suggestion.element != null
? elementKindToCompletionItemKind(
@@ -917,10 +922,20 @@
insertTextFormat: insertTextFormat != lsp.InsertTextFormat.PlainText
? insertTextFormat
: null, // Defaults to PlainText if not supplied
- textEdit: lsp.TextEdit(
- range: toRange(lineInfo, replacementOffset, replacementLength),
- newText: insertText,
- ),
+ textEdit: supportsInsertReplace
+ ? Either2<TextEdit, InsertReplaceEdit>.t2(
+ InsertReplaceEdit(
+ insert: toRange(lineInfo, replacementOffset, insertLength),
+ replace: toRange(lineInfo, replacementOffset, replacementLength),
+ newText: insertText,
+ ),
+ )
+ : Either2<TextEdit, InsertReplaceEdit>.t1(
+ TextEdit(
+ range: toRange(lineInfo, replacementOffset, replacementLength),
+ newText: insertText,
+ ),
+ ),
);
}
diff --git a/pkg/analysis_server/test/lsp/completion_dart_test.dart b/pkg/analysis_server/test/lsp/completion_dart_test.dart
index 2214c75..ffb6cb0 100644
--- a/pkg/analysis_server/test/lsp/completion_dart_test.dart
+++ b/pkg/analysis_server/test/lsp/completion_dart_test.dart
@@ -126,11 +126,9 @@
// 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)),
- );
+ final textEdit = toTextEdit(item.textEdit);
+ expect(textEdit.newText, equals(item.insertText));
+ expect(textEdit.range, equals(rangeFromMarkers(content)));
}
Future<void> test_completeFunctionCalls_flutterSetState() async {
@@ -175,11 +173,9 @@
// placeholders.
expect(item.insertTextFormat, equals(InsertTextFormat.Snippet));
expect(item.insertText, equals('setState(() {\n \${0:}\n \\});'));
- expect(item.textEdit.newText, equals(item.insertText));
- expect(
- item.textEdit.range,
- equals(rangeFromMarkers(content)),
- );
+ final textEdit = toTextEdit(item.textEdit);
+ expect(textEdit.newText, equals(item.insertText));
+ expect(textEdit.range, equals(rangeFromMarkers(content)));
}
Future<void> test_completeFunctionCalls_noRequiredParameters() async {
@@ -206,11 +202,9 @@
// 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(${0:})'));
- expect(item.textEdit.newText, equals(item.insertText));
- expect(
- item.textEdit.range,
- equals(rangeFromMarkers(content)),
- );
+ final textEdit = toTextEdit(item.textEdit);
+ expect(textEdit.newText, equals(item.insertText));
+ expect(textEdit.range, equals(rangeFromMarkers(content)));
}
Future<void> test_completeFunctionCalls_show() async {
@@ -234,7 +228,8 @@
// no need for snippets.
expect(item.insertTextFormat, isNull);
expect(item.insertText, equals(r'min'));
- expect(item.textEdit.newText, equals(item.insertText));
+ final textEdit = toTextEdit(item.textEdit);
+ expect(textEdit.newText, equals(item.insertText));
}
Future<void> test_completeFunctionCalls_suggestionSets() async {
@@ -267,11 +262,9 @@
// Ensure the item can be resolved and gets a proper TextEdit.
final resolved = await resolveCompletion(item);
expect(resolved.textEdit, isNotNull);
- expect(resolved.textEdit.newText, equals(item.insertText));
- expect(
- resolved.textEdit.range,
- equals(rangeFromMarkers(content)),
- );
+ final textEdit = toTextEdit(resolved.textEdit);
+ expect(textEdit.newText, equals(item.insertText));
+ expect(textEdit.range, equals(rangeFromMarkers(content)));
}
Future<void> test_completionKinds_default() async {
@@ -510,6 +503,42 @@
});
}
+ Future<void> test_insertReplaceRanges() async {
+ final content = '''
+ class MyClass {
+ String abcdefghij;
+ }
+
+ main() {
+ MyClass a;
+ a.abc^def
+ }
+ ''';
+
+ await initialize(
+ textDocumentCapabilities: withCompletionItemInsertReplaceSupport(
+ emptyTextDocumentClientCapabilities),
+ );
+ await openFile(mainFileUri, withoutMarkers(content));
+ final res = await getCompletion(mainFileUri, positionFromMarker(content));
+ expect(res.any((c) => c.label == 'abcdefghij'), isTrue);
+ final item = res.singleWhere((c) => c.label == 'abcdefghij');
+ // When using the replacement range, we should get exactly the symbol
+ // we expect.
+ final replaced = applyTextEdits(
+ withoutMarkers(content),
+ [textEditForReplace(item.textEdit)],
+ );
+ expect(replaced, contains('a.abcdefghij\n'));
+ // When using the insert range, we should retain what was after the caret
+ // ("def" in this case).
+ final inserted = applyTextEdits(
+ withoutMarkers(content),
+ [textEditForInsert(item.textEdit)],
+ );
+ expect(inserted, contains('a.abcdefghijdef\n'));
+ }
+
Future<void> test_insideString() async {
final content = '''
var a = "This is ^a test"
@@ -633,7 +662,10 @@
expect(item.insertTextFormat,
anyOf(equals(InsertTextFormat.PlainText), isNull));
expect(item.insertText, anyOf(equals('test'), isNull));
- final updated = applyTextEdits(withoutMarkers(content), [item.textEdit]);
+ final updated = applyTextEdits(
+ withoutMarkers(content),
+ [toTextEdit(item.textEdit)],
+ );
expect(updated, contains('one: '));
}
@@ -656,9 +688,10 @@
// need to be provided.
expect(item.insertTextFormat, isNull);
expect(item.insertText, isNull);
- expect(item.textEdit.newText, equals('one: '));
+ final textEdit = toTextEdit(item.textEdit);
+ expect(textEdit.newText, equals('one: '));
expect(
- item.textEdit.range,
+ textEdit.range,
equals(Range(
start: positionFromMarker(content),
end: positionFromMarker(content))),
@@ -687,9 +720,10 @@
// placeholder.
expect(item.insertTextFormat, equals(InsertTextFormat.Snippet));
expect(item.insertText, equals(r'one: ${0:},'));
- expect(item.textEdit.newText, equals(r'one: ${0:},'));
+ final textEdit = toTextEdit(item.textEdit);
+ expect(textEdit.newText, equals(r'one: ${0:},'));
expect(
- item.textEdit.range,
+ textEdit.range,
equals(Range(
start: positionFromMarker(content),
end: positionFromMarker(content))),
@@ -742,7 +776,10 @@
expect(item.insertTextFormat,
anyOf(equals(InsertTextFormat.PlainText), isNull));
expect(item.insertText, anyOf(equals('abcdefghij'), isNull));
- final updated = applyTextEdits(withoutMarkers(content), [item.textEdit]);
+ final updated = applyTextEdits(
+ withoutMarkers(content),
+ [toTextEdit(item.textEdit)],
+ );
expect(updated, contains('a.abcdefghij'));
}
@@ -864,7 +901,9 @@
// Apply both the main completion edit and the additionalTextEdits atomically.
final newContent = applyTextEdits(
withoutMarkers(content),
- [resolved.textEdit].followedBy(resolved.additionalTextEdits).toList(),
+ [toTextEdit(resolved.textEdit)]
+ .followedBy(resolved.additionalTextEdits)
+ .toList(),
);
// Ensure both edits were made - the completion, and the inserted import.
@@ -968,7 +1007,9 @@
// Apply both the main completion edit and the additionalTextEdits atomically.
final newContent = applyTextEdits(
withoutMarkers(content),
- [resolved.textEdit].followedBy(resolved.additionalTextEdits).toList(),
+ [toTextEdit(resolved.textEdit)]
+ .followedBy(resolved.additionalTextEdits)
+ .toList(),
);
// Ensure both edits were made - the completion, and the inserted import.
@@ -1115,6 +1156,98 @@
expectAutoImportCompletion(resolvedCompletions, '../reexport2.dart');
}
+ Future<void> test_suggestionSets_insertReplaceRanges() async {
+ newFile(
+ join(projectFolderPath, 'other_file.dart'),
+ content: '''
+ /// This class is in another file.
+ class InOtherFile {}
+ ''',
+ );
+
+ final content = '''
+main() {
+ InOtherF^il
+}
+ ''';
+
+ final initialAnalysis = waitForAnalysisComplete();
+ await initialize(
+ textDocumentCapabilities: withCompletionItemInsertReplaceSupport(
+ emptyTextDocumentClientCapabilities),
+ workspaceCapabilities:
+ withApplyEditSupport(emptyWorkspaceClientCapabilities),
+ );
+ await openFile(mainFileUri, withoutMarkers(content));
+ await initialAnalysis;
+ final res = await getCompletion(mainFileUri, positionFromMarker(content));
+
+ // Find the completion for the class in the other file.
+ final completion = res.singleWhere((c) => c.label == 'InOtherFile');
+ expect(completion, isNotNull);
+
+ // Expect no docs or text edit, since these are added during resolve.
+ expect(completion.documentation, isNull);
+ expect(completion.textEdit, isNull);
+
+ // Resolve the completion item (via server) to get its edits. This is the
+ // LSP's equiv of getSuggestionDetails() and is invoked by LSP clients to
+ // populate additional info (in our case, the additional edits for inserting
+ // the import).
+ final resolved = await resolveCompletion(completion);
+ expect(resolved, isNotNull);
+
+ // Ensure the detail field was update to show this will auto-import.
+ expect(
+ resolved.detail, startsWith("Auto import from '../other_file.dart'"));
+
+ // Ensure the doc comment was added.
+ expect(
+ resolved.documentation.valueEquals('This class is in another file.'),
+ isTrue,
+ );
+
+ // Ensure the edit was added on.
+ expect(resolved.textEdit, isNotNull);
+
+ // There should be no command for this item because it doesn't need imports
+ // in other files. Same-file completions are in additionalEdits.
+ expect(resolved.command, isNull);
+
+ // Apply both the main completion edit and the additionalTextEdits atomically
+ // then check the contents.
+
+ final newContentReplaceMode = applyTextEdits(
+ withoutMarkers(content),
+ [textEditForReplace(resolved.textEdit)]
+ .followedBy(resolved.additionalTextEdits)
+ .toList(),
+ );
+ final newContentInsertMode = applyTextEdits(
+ withoutMarkers(content),
+ [textEditForInsert(resolved.textEdit)]
+ .followedBy(resolved.additionalTextEdits)
+ .toList(),
+ );
+
+ // Ensure both edits were made - the completion, and the inserted import.
+ expect(newContentReplaceMode, equals('''
+import '../other_file.dart';
+
+main() {
+ InOtherFile
+}
+ '''));
+ // In insert mode, we'd have the trailing "il" still after the caret.
+ expect(newContentInsertMode, equals('''
+import '../other_file.dart';
+
+main() {
+ InOtherFileil
+}
+ '''));
+ }
+
Future<void> test_suggestionSets_insertsIntoPartFiles() async {
// File we'll be adding an import for.
newFile(
@@ -1160,7 +1293,9 @@
// Apply all current-document edits.
final newContent = applyTextEdits(
withoutMarkers(content),
- [resolved.textEdit].followedBy(resolved.additionalTextEdits).toList(),
+ [toTextEdit(resolved.textEdit)]
+ .followedBy(resolved.additionalTextEdits)
+ .toList(),
);
expect(newContent, equals('''
part of 'parent.dart';
@@ -1262,7 +1397,9 @@
// Apply both the main completion edit and the additionalTextEdits atomically.
final newContent = applyTextEdits(
withoutMarkers(content),
- [resolved.textEdit].followedBy(resolved.additionalTextEdits).toList(),
+ [toTextEdit(resolved.textEdit)]
+ .followedBy(resolved.additionalTextEdits)
+ .toList(),
);
// Ensure both edits were made - the completion, and the inserted import.
@@ -1354,7 +1491,9 @@
// Apply both the main completion edit and the additionalTextEdits atomically.
final newContent = applyTextEdits(
withoutMarkers(content),
- [resolved.textEdit].followedBy(resolved.additionalTextEdits).toList(),
+ [toTextEdit(resolved.textEdit)]
+ .followedBy(resolved.additionalTextEdits)
+ .toList(),
);
// Ensure both edits were made - the completion, and the inserted import.
@@ -1448,7 +1587,10 @@
expect(item.insertTextFormat,
anyOf(equals(InsertTextFormat.PlainText), isNull));
expect(item.insertText, anyOf(equals('abcdefghij'), isNull));
- final updated = applyTextEdits(withoutMarkers(content), [item.textEdit]);
+ final updated = applyTextEdits(
+ withoutMarkers(content),
+ [toTextEdit(item.textEdit)],
+ );
expect(updated, contains('a.abcdefghij'));
}
}
@@ -1483,11 +1625,9 @@
// placeholders.
expect(item.insertTextFormat, equals(InsertTextFormat.Snippet));
expect(item.insertText, equals(r'myFunction(${1:a}, ${2:b}, c: ${3:c})'));
- expect(item.textEdit.newText, equals(item.insertText));
- expect(
- item.textEdit.range,
- equals(rangeFromMarkers(content)),
- );
+ final textEdit = toTextEdit(item.textEdit);
+ expect(textEdit.newText, equals(item.insertText));
+ expect(textEdit.range, equals(rangeFromMarkers(content)));
}
Future<void> test_completeFunctionCalls_requiredNamed_suggestionSet() async {
@@ -1527,10 +1667,8 @@
// Ensure the item can be resolved and gets a proper TextEdit.
final resolved = await resolveCompletion(item);
expect(resolved.textEdit, isNotNull);
- expect(resolved.textEdit.newText, equals(item.insertText));
- expect(
- resolved.textEdit.range,
- equals(rangeFromMarkers(content)),
- );
+ final textEdit = toTextEdit(resolved.textEdit);
+ expect(textEdit.newText, equals(item.insertText));
+ expect(textEdit.range, equals(rangeFromMarkers(content)));
}
}
diff --git a/pkg/analysis_server/test/lsp/completion_yaml_test.dart b/pkg/analysis_server/test/lsp/completion_yaml_test.dart
index 0802d65..90827ef 100644
--- a/pkg/analysis_server/test/lsp/completion_yaml_test.dart
+++ b/pkg/analysis_server/test/lsp/completion_yaml_test.dart
@@ -106,8 +106,19 @@
List<String> expectCompletions,
String verifyEditsFor,
String expectedContent,
+ String expectedContentIfInserting,
+ bool verifyInsertReplaceRanges = false,
}) async {
- await initialize();
+ // If verifyInsertReplaceRanges is true, we need both expected contents.
+ assert(verifyInsertReplaceRanges == false ||
+ (expectedContent != null && expectedContentIfInserting != null));
+
+ final textDocCapabilities = verifyInsertReplaceRanges
+ ? withCompletionItemInsertReplaceSupport(
+ emptyTextDocumentClientCapabilities)
+ : emptyTextDocumentClientCapabilities;
+
+ await initialize(textDocumentCapabilities: textDocCapabilities);
await openFile(fileUri, withoutMarkers(content));
final res = await getCompletion(fileUri, positionFromMarker(content));
@@ -125,8 +136,27 @@
final item = res.singleWhere((c) => c.label == verifyEditsFor);
expect(item.insertTextFormat, isNull);
expect(item.insertText, isNull);
- final updated = applyTextEdits(withoutMarkers(content), [item.textEdit]);
- expect(updated, equals(expectedContent));
+
+ if (verifyInsertReplaceRanges) {
+ // Replacing.
+ final replaced = applyTextEdits(
+ withoutMarkers(content),
+ [textEditForReplace(item.textEdit)],
+ );
+ expect(replaced, equals(expectedContent));
+ // Inserting.
+ final inserted = applyTextEdits(
+ withoutMarkers(content),
+ [textEditForInsert(item.textEdit)],
+ );
+ expect(inserted, equals(expectedContentIfInserting));
+ } else {
+ final updated = applyTextEdits(
+ withoutMarkers(content),
+ [toTextEdit(item.textEdit)],
+ );
+ expect(updated, equals(expectedContent));
+ }
}
}
}
@@ -220,6 +250,40 @@
@reflectiveTest
class PubspecCompletionTest extends AbstractLspAnalysisServerTest
with CompletionTestMixin {
+ Future<void> test_insertReplaceRanges() async {
+ final content = '''
+name: foo
+version: 1.0.0
+
+environment:
+ s^dk
+''';
+ final expectedReplaced = '''
+name: foo
+version: 1.0.0
+
+environment:
+ sdk:
+''';
+ final expectedInserted = '''
+name: foo
+version: 1.0.0
+
+environment:
+ sdk: dk
+''';
+
+ await verifyCompletions(
+ pubspecFileUri,
+ content,
+ expectCompletions: ['sdk: '],
+ verifyEditsFor: 'sdk: ',
+ verifyInsertReplaceRanges: true,
+ expectedContent: expectedReplaced,
+ expectedContentIfInserting: expectedInserted,
+ );
+ }
+
Future<void> test_nested() async {
final content = '''
name: foo
diff --git a/pkg/analysis_server/test/lsp/server_abstract.dart b/pkg/analysis_server/test/lsp/server_abstract.dart
index f9d072c..d2ff935 100644
--- a/pkg/analysis_server/test/lsp/server_abstract.dart
+++ b/pkg/analysis_server/test/lsp/server_abstract.dart
@@ -271,6 +271,16 @@
});
}
+ TextDocumentClientCapabilities withCompletionItemInsertReplaceSupport(
+ TextDocumentClientCapabilities source,
+ ) {
+ return extendTextDocumentCapabilities(source, {
+ 'completion': {
+ 'completionItem': {'insertReplaceSupport': true}
+ }
+ });
+ }
+
TextDocumentClientCapabilities withCompletionItemKinds(
TextDocumentClientCapabilities source,
List<CompletionItemKind> kinds,
@@ -1496,6 +1506,25 @@
return expectSuccessfulResponseTo(request, (result) => result);
}
+ /// Creates a [TextEdit] using the `insert` range of a [InsertReplaceEdit].
+ TextEdit textEditForInsert(Either2<TextEdit, InsertReplaceEdit> edit) =>
+ edit.map(
+ (_) => throw 'Expected InsertReplaceEdit, got TextEdit',
+ (e) => TextEdit(range: e.insert, newText: e.newText),
+ );
+
+ /// Creates a [TextEdit] using the `replace` range of a [InsertReplaceEdit].
+ TextEdit textEditForReplace(Either2<TextEdit, InsertReplaceEdit> edit) =>
+ edit.map(
+ (_) => throw 'Expected InsertReplaceEdit, got TextEdit',
+ (e) => TextEdit(range: e.replace, newText: e.newText),
+ );
+
+ TextEdit toTextEdit(Either2<TextEdit, InsertReplaceEdit> edit) => edit.map(
+ (e) => e,
+ (_) => throw 'Expected TextEdit, got InsertReplaceEdit',
+ );
+
WorkspaceFolder toWorkspaceFolder(Uri uri) {
return WorkspaceFolder(
uri: uri.toString(),
diff --git a/pkg/analysis_server/tool/code_completion/relevance_table_generator.dart b/pkg/analysis_server/tool/code_completion/relevance_table_generator.dart
index df9e5d3..53e3fe2 100644
--- a/pkg/analysis_server/tool/code_completion/relevance_table_generator.dart
+++ b/pkg/analysis_server/tool/code_completion/relevance_table_generator.dart
@@ -20,6 +20,7 @@
import 'package:analyzer/dart/element/type_provider.dart';
import 'package:analyzer/dart/element/type_system.dart';
import 'package:analyzer/diagnostic/diagnostic.dart';
+import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/src/dart/element/inheritance_manager3.dart';
import 'package:analyzer/src/generated/engine.dart';
@@ -35,9 +36,6 @@
var result = parser.parse(args);
if (validArguments(parser, result)) {
- var rootPath = result.rest[0];
- print('Analyzing root: "$rootPath"');
-
var provider = PhysicalResourceProvider.INSTANCE;
var packageRoot = provider.pathContext.normalize(package_root.packageRoot);
var generatedFilePath = provider.pathContext.join(
@@ -51,15 +49,61 @@
'relevance_tables.g.dart');
var generatedFile = provider.getFile(generatedFilePath);
+ void writeRelevanceTable(RelevanceData data, File generatedFile) {
+ var buffer = StringBuffer();
+ var writer = RelevanceTableWriter(buffer);
+ writer.write(data);
+ generatedFile.writeAsStringSync(buffer.toString());
+ DartFormat.formatFile(io.File(generatedFile.path));
+ }
+
+ if (result.wasParsed('reduceDir')) {
+ var data = RelevanceData();
+ var dir = provider.getFolder(result['reduceDir']);
+ for (var child in dir.getChildren()) {
+ if (child is File) {
+ var newData = RelevanceData.fromJson(child.readAsStringSync());
+ data.addData(newData);
+ }
+ }
+ writeRelevanceTable(data, generatedFile);
+ return;
+ }
+
+ var rootPath = result.rest[0];
+ print('Analyzing root: "$rootPath"');
+
+ File uniqueDataFile() {
+ var dataDir = result['mapDir'];
+ var baseFileName = provider.pathContext.basename(rootPath);
+ var index = 1;
+ while (index < 10000) {
+ var suffix = (index++).toString();
+ suffix = '0000'.substring(suffix.length) + suffix + '.json';
+ var fileName = baseFileName + suffix;
+ var filePath = provider.pathContext.join(dataDir, fileName);
+ var file = provider.getFile(filePath);
+ if (!file.exists) {
+ return file;
+ }
+ }
+
+ /// If there are more than 10000 directories with the same name, just
+ /// overwrite a previously generated file.
+ var fileName = baseFileName + '9999';
+ var filePath = provider.pathContext.join(dataDir, fileName);
+ return provider.getFile(filePath);
+ }
+
var computer = RelevanceMetricsComputer();
- var stopwatch = Stopwatch();
- stopwatch.start();
+ var stopwatch = Stopwatch()..start();
await computer.compute(rootPath, verbose: result['verbose']);
- var buffer = StringBuffer();
- var writer = RelevanceTableWriter(buffer);
- writer.write(computer.data);
- generatedFile.writeAsStringSync(buffer.toString());
- DartFormat.formatFile(io.File(generatedFile.path));
+ if (result.wasParsed('mapDir')) {
+ var dataFile = uniqueDataFile();
+ dataFile.writeAsStringSync(computer.data.toJson());
+ } else {
+ writeRelevanceTable(computer.data, generatedFile);
+ }
stopwatch.stop();
var duration = Duration(milliseconds: stopwatch.elapsedMilliseconds);
@@ -74,6 +118,18 @@
'help',
abbr: 'h',
help: 'Print this help message.',
+ negatable: false,
+ );
+ parser.addOption(
+ 'mapDir',
+ help: 'The absolute path of the directory to which the relevance data will '
+ 'be written. Using this option will prevent the relevance table from '
+ 'being written.',
+ );
+ parser.addOption(
+ 'reduceDir',
+ help: 'The absolute path of the directory from which the relevance data '
+ 'will be read.',
);
parser.addFlag(
'verbose',
@@ -103,17 +159,32 @@
if (result.wasParsed('help')) {
printUsage(parser);
return false;
+ } else if (result.wasParsed('reduceDir')) {
+ if (result.rest.isNotEmpty) {
+ printUsage(parser,
+ error: 'A package path is not allowed in reduce mode.');
+ return false;
+ }
+ return validateDir(parser, result['reduceDir']);
} else if (result.rest.length != 1) {
printUsage(parser, error: 'No package path specified.');
return false;
}
- var rootPath = result.rest[0];
- if (!PhysicalResourceProvider.INSTANCE.pathContext.isAbsolute(rootPath)) {
- printUsage(parser, error: 'The package path must be an absolute path.');
+ if (result.wasParsed('mapDir')) {
+ return validateDir(parser, result['mapDir']);
+ }
+ return validateDir(parser, result.rest[0]);
+}
+
+/// Return `true` if the [dirPath] is an absolute path to a directory that
+/// exists.
+bool validateDir(ArgParser parser, String dirPath) {
+ if (!PhysicalResourceProvider.INSTANCE.pathContext.isAbsolute(dirPath)) {
+ printUsage(parser, error: 'The path "$dirPath" must be an absolute path.');
return false;
}
- if (!io.Directory(rootPath).existsSync()) {
- printUsage(parser, error: 'The directory "$rootPath" does not exist.');
+ if (!io.Directory(dirPath).existsSync()) {
+ printUsage(parser, error: 'The directory "$dirPath" does not exist.');
return false;
}
return true;
@@ -130,10 +201,11 @@
/// Initialize a newly created set of relevance data based on the content of
/// the JSON encoded string.
RelevanceData.fromJson(String encoded) {
- var map = json.decode(encoded) as Map<String, Map<String, String>>;
+ var map = json.decode(encoded) as Map<String, dynamic>;
for (var contextEntry in map.entries) {
var contextMap = byKind.putIfAbsent(contextEntry.key, () => {});
- for (var kindEntry in contextEntry.value.entries) {
+ for (var kindEntry
+ in (contextEntry.value as Map<String, dynamic>).entries) {
_Kind kind;
var key = kindEntry.key;
if (key.startsWith('e')) {
@@ -143,7 +215,7 @@
} else {
throw StateError('Invalid initial character in unique key "$key"');
}
- contextMap[kind] = int.parse(kindEntry.value);
+ contextMap[kind] = int.parse(kindEntry.value as String);
}
}
}
@@ -160,11 +232,6 @@
}
}
- /// Add the data from the given relevance [data] to this set of data.
- void addDataFrom(RelevanceData data) {
- _addToMap(byKind, data.byKind);
- }
-
/// Record that an element of the given [kind] was found in the given
/// [context].
void recordElementKind(String context, ElementKind kind) {
@@ -192,17 +259,6 @@
}
return json.encode(map);
}
-
- /// Add the data in the [source] map to the [target] map.
- void _addToMap<K>(Map<K, Map<K, int>> target, Map<K, Map<K, int>> source) {
- for (var outerEntry in source.entries) {
- var innerTarget = target.putIfAbsent(outerEntry.key, () => {});
- for (var innerEntry in outerEntry.value.entries) {
- var innerKey = innerEntry.key;
- innerTarget[innerKey] = (innerTarget[innerKey] ?? 0) + innerEntry.value;
- }
- }
- }
}
/// An object that visits a compilation unit in order to record the data used to
diff --git a/pkg/analysis_server/tool/lsp_spec/generate_all.dart b/pkg/analysis_server/tool/lsp_spec/generate_all.dart
index ff160b1..ecc3079 100644
--- a/pkg/analysis_server/tool/lsp_spec/generate_all.dart
+++ b/pkg/analysis_server/tool/lsp_spec/generate_all.dart
@@ -261,10 +261,13 @@
interface(
'DartCompletionItemResolutionInfo',
[
+ // These fields have short-ish names because they're on the payload
+ // for all suggestion-set backed completions.
field('libId', type: 'number'),
field('displayUri', type: 'string'),
- field('rOffset', type: 'number'),
- field('rLength', type: 'number'),
+ field('rOffset', type: 'number'), // replacementOffset
+ field('iLength', type: 'number'), // insertLength
+ field('rLength', type: 'number'), // replacementLength
],
baseType: 'CompletionItemResolutionInfo',
),
diff --git a/pkg/analysis_server/tool/lsp_spec/typescript.dart b/pkg/analysis_server/tool/lsp_spec/typescript.dart
index f0a54e8..575d66e 100644
--- a/pkg/analysis_server/tool/lsp_spec/typescript.dart
+++ b/pkg/analysis_server/tool/lsp_spec/typescript.dart
@@ -90,7 +90,6 @@
'CompletionItem': {
'kind': 'CompletionItemKind',
'data': 'CompletionItemResolutionInfo',
- 'textEdit': 'TextEdit',
},
'CallHierarchyItem': {
'data': 'object',
diff --git a/pkg/analyzer/lib/src/dart/resolver/flow_analysis_visitor.dart b/pkg/analyzer/lib/src/dart/resolver/flow_analysis_visitor.dart
index 1f89eb6..3775ad7 100644
--- a/pkg/analyzer/lib/src/dart/resolver/flow_analysis_visitor.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/flow_analysis_visitor.dart
@@ -240,6 +240,19 @@
flow!.finish();
}
+ /// Transfers any test data that was recorded for [oldNode] so that it is now
+ /// associated with [newNode]. We need to do this when doing AST rewriting,
+ /// so that test data can be found using the rewritten tree.
+ void transferTestData(AstNode oldNode, AstNode newNode) {
+ var dataForTesting = this.dataForTesting;
+ if (dataForTesting != null) {
+ var oldNonPromotionReasons = dataForTesting.nonPromotionReasons[oldNode];
+ if (oldNonPromotionReasons != null) {
+ dataForTesting.nonPromotionReasons[newNode] = oldNonPromotionReasons;
+ }
+ }
+ }
+
void variableDeclarationList(VariableDeclarationList node) {
if (flow != null) {
var variables = node.variables;
diff --git a/pkg/analyzer/lib/src/dart/resolver/function_expression_invocation_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/function_expression_invocation_resolver.dart
index a85d35e..c69fc95 100644
--- a/pkg/analyzer/lib/src/dart/resolver/function_expression_invocation_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/function_expression_invocation_resolver.dart
@@ -42,17 +42,21 @@
return;
}
- _nullableDereferenceVerifier.expression(function,
- errorCode: CompileTimeErrorCode.UNCHECKED_INVOCATION_OF_NULLABLE_VALUE);
-
var receiverType = function.staticType;
- if (receiverType is FunctionType) {
- _resolve(node, receiverType);
+ if (receiverType is InterfaceType) {
+ // Note: in this circumstance it's not necessary to call
+ // `_nullableDereferenceVerifier.expression` because
+ // `_resolveReceiverInterfaceType` calls `TypePropertyResolver.resolve`,
+ // which does the necessary null checking.
+ _resolveReceiverInterfaceType(node, function, receiverType);
return;
}
- if (receiverType is InterfaceType) {
- _resolveReceiverInterfaceType(node, function, receiverType);
+ _nullableDereferenceVerifier.expression(function,
+ errorCode: CompileTimeErrorCode.UNCHECKED_INVOCATION_OF_NULLABLE_VALUE);
+
+ if (receiverType is FunctionType) {
+ _resolve(node, receiverType);
return;
}
diff --git a/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart
index a975540..e05d2ad 100644
--- a/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart
@@ -752,6 +752,8 @@
node.methodName,
);
}
+ _resolver.flowAnalysis?.flow
+ ?.propertyGet(functionExpression, target, node.methodName.name);
functionExpression.staticType = targetType;
}
@@ -763,6 +765,7 @@
NodeReplacer.replace(node, invocation);
node.setProperty(_rewriteResultKey, invocation);
InferenceContext.setTypeFromNode(invocation, node);
+ _resolver.flowAnalysis?.transferTestData(node, invocation);
}
void _setDynamicResolution(MethodInvocation node,
diff --git a/pkg/analyzer/lib/src/dart/resolver/type_property_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/type_property_resolver.dart
index d6204de..99b1277 100644
--- a/pkg/analyzer/lib/src/dart/resolver/type_property_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/type_property_resolver.dart
@@ -2,7 +2,6 @@
// 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:_fe_analyzer_shared/src/flow_analysis/flow_analysis.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/syntactic_entity.dart';
import 'package:analyzer/dart/element/element.dart';
@@ -13,13 +12,9 @@
import 'package:analyzer/src/dart/element/type_provider.dart';
import 'package:analyzer/src/dart/element/type_system.dart';
import 'package:analyzer/src/dart/resolver/extension_member_resolver.dart';
-import 'package:analyzer/src/dart/resolver/flow_analysis_visitor.dart';
import 'package:analyzer/src/dart/resolver/resolution_result.dart';
-import 'package:analyzer/src/diagnostic/diagnostic.dart';
import 'package:analyzer/src/error/codes.dart';
import 'package:analyzer/src/generated/resolver.dart';
-import 'package:analyzer/src/generated/source.dart';
-import 'package:analyzer/src/util/ast_data_extractor.dart';
/// Helper for resolving properties (getters, setters, or methods).
class TypePropertyResolver {
@@ -120,35 +115,8 @@
}
}
- List<DiagnosticMessage> messages = [];
- if (receiver != null) {
- var whyNotPromoted =
- _resolver.flowAnalysis?.flow?.whyNotPromoted(receiver);
- if (whyNotPromoted != null) {
- for (var entry in whyNotPromoted.entries) {
- var whyNotPromotedVisitor = _WhyNotPromotedVisitor(_resolver.source,
- receiver, _resolver.flowAnalysis!.dataForTesting);
- if (_typeSystem.isPotentiallyNullable(entry.key)) continue;
- var message = entry.value.accept(whyNotPromotedVisitor);
- if (message != null) {
- if (_resolver.flowAnalysis!.dataForTesting != null) {
- var nonPromotionReasonText = entry.value.shortName;
- if (whyNotPromotedVisitor.propertyReference != null) {
- var id =
- computeMemberId(whyNotPromotedVisitor.propertyReference!);
- nonPromotionReasonText += '($id)';
- }
- _resolver.flowAnalysis!.dataForTesting!
- .nonPromotionReasons[nameErrorEntity] =
- nonPromotionReasonText;
- }
- messages = [message];
- }
- break;
- }
- }
- }
-
+ List<DiagnosticMessage> messages =
+ _resolver.computeWhyNotPromotedMessages(receiver, nameErrorEntity);
_resolver.nullableDereferenceVerifier.report(
receiverErrorNode, receiverType,
errorCode: errorCode, arguments: [name], messages: messages);
@@ -299,102 +267,3 @@
);
}
}
-
-class _WhyNotPromotedVisitor
- implements
- NonPromotionReasonVisitor<DiagnosticMessage?, AstNode, Expression,
- PromotableElement> {
- final Source source;
-
- final Expression _receiver;
-
- final FlowAnalysisDataForTesting? _dataForTesting;
-
- PropertyAccessorElement? propertyReference;
-
- _WhyNotPromotedVisitor(this.source, this._receiver, this._dataForTesting);
-
- @override
- DiagnosticMessage? visitDemoteViaExplicitWrite(
- DemoteViaExplicitWrite<PromotableElement, Expression> reason) {
- var writeExpression = reason.writeExpression;
- if (_dataForTesting != null) {
- _dataForTesting!.nonPromotionReasonTargets[writeExpression] =
- reason.shortName;
- }
- var variableName = reason.variable.name;
- if (variableName == null) return null;
- return _contextMessageForWrite(variableName, writeExpression);
- }
-
- @override
- DiagnosticMessage? visitDemoteViaForEachVariableWrite(
- DemoteViaForEachVariableWrite<PromotableElement, AstNode> reason) {
- var node = reason.node;
- var variableName = reason.variable.name;
- if (variableName == null) return null;
- ForLoopParts parts;
- if (node is ForStatement) {
- parts = node.forLoopParts;
- } else if (node is ForElement) {
- parts = node.forLoopParts;
- } else {
- assert(false, 'Unexpected node type');
- return null;
- }
- if (parts is ForEachPartsWithIdentifier) {
- var identifier = parts.identifier;
- if (_dataForTesting != null) {
- _dataForTesting!.nonPromotionReasonTargets[identifier] =
- reason.shortName;
- }
- return _contextMessageForWrite(variableName, identifier);
- } else {
- assert(false, 'Unexpected parts type');
- return null;
- }
- }
-
- @override
- DiagnosticMessage? visitPropertyNotPromoted(PropertyNotPromoted reason) {
- var receiver = _receiver;
- Element? receiverElement;
- if (receiver is SimpleIdentifier) {
- receiverElement = receiver.staticElement;
- } else if (receiver is PropertyAccess) {
- receiverElement = receiver.propertyName.staticElement;
- } else if (receiver is PrefixedIdentifier) {
- receiverElement = receiver.identifier.staticElement;
- } else {
- assert(false, 'Unrecognized receiver: ${receiver.runtimeType}');
- }
- if (receiverElement is PropertyAccessorElement) {
- propertyReference = receiverElement;
- return _contextMessageForProperty(receiverElement, reason.propertyName);
- } else {
- assert(receiverElement == null,
- 'Unrecognized receiver element: ${receiverElement.runtimeType}');
- return null;
- }
- }
-
- DiagnosticMessageImpl _contextMessageForProperty(
- PropertyAccessorElement property, String propertyName) {
- return DiagnosticMessageImpl(
- filePath: property.source.fullName,
- message:
- "'$propertyName' refers to a property so it could not be promoted.",
- offset: property.nameOffset,
- length: property.nameLength);
- }
-
- DiagnosticMessageImpl _contextMessageForWrite(
- String variableName, Expression writeExpression) {
- return DiagnosticMessageImpl(
- filePath: source.fullName,
- message: "Variable '$variableName' could be null due to an intervening "
- "write.",
- offset: writeExpression.offset,
- length: writeExpression.length);
- }
-}
diff --git a/pkg/analyzer/lib/src/error/nullable_dereference_verifier.dart b/pkg/analyzer/lib/src/error/nullable_dereference_verifier.dart
index 48816ec..6044fab 100644
--- a/pkg/analyzer/lib/src/error/nullable_dereference_verifier.dart
+++ b/pkg/analyzer/lib/src/error/nullable_dereference_verifier.dart
@@ -11,17 +11,23 @@
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/dart/element/type_system.dart';
import 'package:analyzer/src/error/codes.dart';
+import 'package:analyzer/src/generated/resolver.dart';
/// Helper for checking potentially nullable dereferences.
class NullableDereferenceVerifier {
final TypeSystemImpl _typeSystem;
final ErrorReporter _errorReporter;
+ /// The resolver driving this participant.
+ final ResolverVisitor _resolver;
+
NullableDereferenceVerifier({
required TypeSystemImpl typeSystem,
required ErrorReporter errorReporter,
+ required ResolverVisitor resolver,
}) : _typeSystem = typeSystem,
- _errorReporter = errorReporter;
+ _errorReporter = errorReporter,
+ _resolver = resolver;
bool expression(Expression expression,
{DartType? type, ErrorCode? errorCode}) {
@@ -59,7 +65,11 @@
return false;
}
- report(errorNode, receiverType, errorCode: errorCode);
+ List<DiagnosticMessage>? messages;
+ if (errorNode is Expression) {
+ messages = _resolver.computeWhyNotPromotedMessages(errorNode, errorNode);
+ }
+ report(errorNode, receiverType, errorCode: errorCode, messages: messages);
return true;
}
}
diff --git a/pkg/analyzer/lib/src/generated/resolver.dart b/pkg/analyzer/lib/src/generated/resolver.dart
index 1e34cb0..1411df8 100644
--- a/pkg/analyzer/lib/src/generated/resolver.dart
+++ b/pkg/analyzer/lib/src/generated/resolver.dart
@@ -4,14 +4,17 @@
import 'dart:collection';
+import 'package:_fe_analyzer_shared/src/flow_analysis/flow_analysis.dart';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/syntactic_entity.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/scope.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/dart/element/type_provider.dart';
+import 'package:analyzer/diagnostic/diagnostic.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
@@ -47,6 +50,7 @@
import 'package:analyzer/src/dart/resolver/typed_literal_resolver.dart';
import 'package:analyzer/src/dart/resolver/variable_declaration_resolver.dart';
import 'package:analyzer/src/dart/resolver/yield_statement_resolver.dart';
+import 'package:analyzer/src/diagnostic/diagnostic.dart';
import 'package:analyzer/src/error/bool_expression_verifier.dart';
import 'package:analyzer/src/error/codes.dart';
import 'package:analyzer/src/error/dead_code_verifier.dart';
@@ -61,6 +65,7 @@
import 'package:analyzer/src/generated/this_access_tracker.dart';
import 'package:analyzer/src/generated/type_promotion_manager.dart';
import 'package:analyzer/src/generated/variable_type_provider.dart';
+import 'package:analyzer/src/util/ast_data_extractor.dart';
import 'package:meta/meta.dart';
/// Maintains and manages contextual type information used for
@@ -315,6 +320,7 @@
nullableDereferenceVerifier = NullableDereferenceVerifier(
typeSystem: typeSystem,
errorReporter: errorReporter,
+ resolver: this,
);
boolExpressionVerifier = BoolExpressionVerifier(
typeSystem: typeSystem,
@@ -505,6 +511,39 @@
nullSafetyDeadCodeVerifier.visitNode(node);
}
+ /// Computes the appropriate set of context messages to report along with an
+ /// error that may have occurred because [receiver] was not type promoted.
+ List<DiagnosticMessage> computeWhyNotPromotedMessages(
+ Expression? receiver, SyntacticEntity errorEntity) {
+ List<DiagnosticMessage> messages = [];
+ if (receiver != null) {
+ var whyNotPromoted = flowAnalysis?.flow?.whyNotPromoted(receiver);
+ if (whyNotPromoted != null) {
+ for (var entry in whyNotPromoted.entries) {
+ var whyNotPromotedVisitor = _WhyNotPromotedVisitor(
+ source, receiver, flowAnalysis!.dataForTesting);
+ if (typeSystem.isPotentiallyNullable(entry.key)) continue;
+ var message = entry.value.accept(whyNotPromotedVisitor);
+ if (message != null) {
+ if (flowAnalysis!.dataForTesting != null) {
+ var nonPromotionReasonText = entry.value.shortName;
+ if (whyNotPromotedVisitor.propertyReference != null) {
+ var id =
+ computeMemberId(whyNotPromotedVisitor.propertyReference!);
+ nonPromotionReasonText += '($id)';
+ }
+ flowAnalysis!.dataForTesting!.nonPromotionReasons[errorEntity] =
+ nonPromotionReasonText;
+ }
+ messages = [message];
+ }
+ break;
+ }
+ }
+ }
+ return messages;
+ }
+
/// Return the static element associated with the given expression whose type
/// can be overridden, or `null` if there is no element whose type can be
/// overridden.
@@ -3297,3 +3336,102 @@
return null;
}
}
+
+class _WhyNotPromotedVisitor
+ implements
+ NonPromotionReasonVisitor<DiagnosticMessage?, AstNode, Expression,
+ PromotableElement> {
+ final Source source;
+
+ final Expression _receiver;
+
+ final FlowAnalysisDataForTesting? _dataForTesting;
+
+ PropertyAccessorElement? propertyReference;
+
+ _WhyNotPromotedVisitor(this.source, this._receiver, this._dataForTesting);
+
+ @override
+ DiagnosticMessage? visitDemoteViaExplicitWrite(
+ DemoteViaExplicitWrite<PromotableElement, Expression> reason) {
+ var writeExpression = reason.writeExpression;
+ if (_dataForTesting != null) {
+ _dataForTesting!.nonPromotionReasonTargets[writeExpression] =
+ reason.shortName;
+ }
+ var variableName = reason.variable.name;
+ if (variableName == null) return null;
+ return _contextMessageForWrite(variableName, writeExpression);
+ }
+
+ @override
+ DiagnosticMessage? visitDemoteViaForEachVariableWrite(
+ DemoteViaForEachVariableWrite<PromotableElement, AstNode> reason) {
+ var node = reason.node;
+ var variableName = reason.variable.name;
+ if (variableName == null) return null;
+ ForLoopParts parts;
+ if (node is ForStatement) {
+ parts = node.forLoopParts;
+ } else if (node is ForElement) {
+ parts = node.forLoopParts;
+ } else {
+ assert(false, 'Unexpected node type');
+ return null;
+ }
+ if (parts is ForEachPartsWithIdentifier) {
+ var identifier = parts.identifier;
+ if (_dataForTesting != null) {
+ _dataForTesting!.nonPromotionReasonTargets[identifier] =
+ reason.shortName;
+ }
+ return _contextMessageForWrite(variableName, identifier);
+ } else {
+ assert(false, 'Unexpected parts type');
+ return null;
+ }
+ }
+
+ @override
+ DiagnosticMessage? visitPropertyNotPromoted(PropertyNotPromoted reason) {
+ var receiver = _receiver;
+ Element? receiverElement;
+ if (receiver is SimpleIdentifier) {
+ receiverElement = receiver.staticElement;
+ } else if (receiver is PropertyAccess) {
+ receiverElement = receiver.propertyName.staticElement;
+ } else if (receiver is PrefixedIdentifier) {
+ receiverElement = receiver.identifier.staticElement;
+ } else {
+ assert(false, 'Unrecognized receiver: ${receiver.runtimeType}');
+ }
+ if (receiverElement is PropertyAccessorElement) {
+ propertyReference = receiverElement;
+ return _contextMessageForProperty(receiverElement, reason.propertyName);
+ } else {
+ assert(receiverElement == null,
+ 'Unrecognized receiver element: ${receiverElement.runtimeType}');
+ return null;
+ }
+ }
+
+ DiagnosticMessageImpl _contextMessageForProperty(
+ PropertyAccessorElement property, String propertyName) {
+ return DiagnosticMessageImpl(
+ filePath: property.source.fullName,
+ message:
+ "'$propertyName' refers to a property so it could not be promoted.",
+ offset: property.nameOffset,
+ length: property.nameLength);
+ }
+
+ DiagnosticMessageImpl _contextMessageForWrite(
+ String variableName, Expression writeExpression) {
+ return DiagnosticMessageImpl(
+ filePath: source.fullName,
+ message: "Variable '$variableName' could be null due to an intervening "
+ "write.",
+ offset: writeExpression.offset,
+ length: writeExpression.length);
+ }
+}
diff --git a/tools/VERSION b/tools/VERSION
index 54c83b9..ed26e86 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
MAJOR 2
MINOR 13
PATCH 0
-PRERELEASE 68
+PRERELEASE 69
PRERELEASE_PATCH 0
\ No newline at end of file