Skip to content

Commit 25cf204

Browse files
DanTupcommit-bot@chromium.org
authored andcommitted
Prevent duplicate code actions for lines with multiple similar errors
Change-Id: If1a50f3c651efec9ba68fc7be5e50700cfcdb20b Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/151234 Commit-Queue: Danny Tuppeny <[email protected]> Reviewed-by: Brian Wilkerson <[email protected]>
1 parent 16ef3ee commit 25cf204

File tree

3 files changed

+100
-22
lines changed

3 files changed

+100
-22
lines changed

pkg/analysis_server/lib/src/lsp/handlers/handler_code_actions.dart

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import 'package:analyzer/dart/analysis/results.dart';
2525
import 'package:analyzer/dart/analysis/session.dart'
2626
show InconsistentAnalysisException;
2727
import 'package:analyzer/src/generated/engine.dart' show AnalysisEngine;
28+
import 'package:collection/collection.dart' show groupBy;
2829

2930
class CodeActionHandler extends MessageHandler<CodeActionParams,
3031
List<Either2<Command, CodeAction>>> {
@@ -96,29 +97,52 @@ class CodeActionHandler extends MessageHandler<CodeActionParams,
9697
/// version of each document being modified so it's important to call this
9798
/// immediately after computing edits to ensure the document is not modified
9899
/// before the version number is read.
99-
Either2<Command, CodeAction> _createAssistAction(Assist assist) {
100-
return Either2<Command, CodeAction>.t2(CodeAction(
100+
CodeAction _createAssistAction(Assist assist) {
101+
return CodeAction(
101102
assist.change.message,
102103
toCodeActionKind(assist.change.id, CodeActionKind.Refactor),
103104
const [],
104105
createWorkspaceEdit(server, assist.change.edits),
105106
null,
106-
));
107+
);
107108
}
108109

109110
/// Creates a CodeAction to apply this fix. Note: This code will fetch the
110111
/// version of each document being modified so it's important to call this
111112
/// immediately after computing edits to ensure the document is not modified
112113
/// before the version number is read.
113-
Either2<Command, CodeAction> _createFixAction(
114-
Fix fix, Diagnostic diagnostic) {
115-
return Either2<Command, CodeAction>.t2(CodeAction(
114+
CodeAction _createFixAction(Fix fix, Diagnostic diagnostic) {
115+
return CodeAction(
116116
fix.change.message,
117117
toCodeActionKind(fix.change.id, CodeActionKind.QuickFix),
118118
[diagnostic],
119119
createWorkspaceEdit(server, fix.change.edits),
120120
null,
121-
));
121+
);
122+
}
123+
124+
/// Dedupes actions that perform the same edit and merge their diagnostics
125+
/// together. This avoids duplicates where there are multiple errors on
126+
/// the same line that have the same fix (for example importing a
127+
/// library that fixes multiple unresolved types).
128+
List<CodeAction> _dedupeActions(Iterable<CodeAction> actions) {
129+
final groups = groupBy(actions, (CodeAction action) => action.edit);
130+
return groups.keys.map((edit) {
131+
final first = groups[edit].first;
132+
// Avoid constructing new CodeActions if there was only one in this group.
133+
if (groups[edit].length == 1) {
134+
return first;
135+
}
136+
// Build a new CodeAction that merges the diagnostics from each same
137+
// code action onto a single one.
138+
return CodeAction(
139+
first.title,
140+
first.kind,
141+
// Merge diagnostics from all of the CodeActions.
142+
groups[edit].expand((r) => r.diagnostics).toList(),
143+
first.edit,
144+
first.command);
145+
}).toList();
122146
}
123147

124148
Future<List<Either2<Command, CodeAction>>> _getAssistActions(
@@ -145,7 +169,11 @@ class CodeActionHandler extends MessageHandler<CodeActionParams,
145169
final assists = await processor.compute();
146170
assists.sort(Assist.SORT_BY_RELEVANCE);
147171

148-
return assists.map(_createAssistAction).toList();
172+
final assistActions = _dedupeActions(assists.map(_createAssistAction));
173+
174+
return assistActions
175+
.map((action) => Either2<Command, CodeAction>.t2(action))
176+
.toList();
149177
} on InconsistentAnalysisException {
150178
// If an InconsistentAnalysisException occurs, it's likely the user modified
151179
// the source and therefore is no longer interested in the results, so
@@ -172,6 +200,7 @@ class CodeActionHandler extends MessageHandler<CodeActionParams,
172200
_getFixActions(kinds, supportsLiterals, range, unit),
173201
]);
174202
final flatResults = results.expand((x) => x).toList();
203+
175204
return success(flatResults);
176205
}
177206

@@ -188,7 +217,7 @@ class CodeActionHandler extends MessageHandler<CodeActionParams,
188217
}
189218

190219
final lineInfo = unit.lineInfo;
191-
final codeActions = <Either2<Command, CodeAction>>[];
220+
final codeActions = <CodeAction>[];
192221
final fixContributor = DartFixContributor();
193222

194223
try {
@@ -216,7 +245,12 @@ class CodeActionHandler extends MessageHandler<CodeActionParams,
216245
}
217246
}
218247
}
219-
return codeActions;
248+
249+
final dedupedActions = _dedupeActions(codeActions);
250+
251+
return dedupedActions
252+
.map((action) => Either2<Command, CodeAction>.t2(action))
253+
.toList();
220254
} on InconsistentAnalysisException {
221255
// If an InconsistentAnalysisException occurs, it's likely the user modified
222256
// the source and therefore is no longer interested in the results, so

pkg/analysis_server/test/lsp/code_actions_abstract.dart

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,18 +57,21 @@ abstract class AbstractCodeActionsTest extends AbstractLspAnalysisServerTest {
5757

5858
CodeAction findEditAction(List<Either2<Command, CodeAction>> actions,
5959
CodeActionKind actionKind, String title) {
60-
for (var codeAction in actions) {
61-
final codeActionLiteral =
62-
codeAction.map((cmd) => null, (action) => action);
63-
if (codeActionLiteral?.kind == actionKind &&
64-
codeActionLiteral?.title == title) {
65-
// We're specifically looking for an action that contains an edit.
66-
assert(codeActionLiteral.command == null);
67-
assert(codeActionLiteral.edit != null);
68-
return codeActionLiteral;
69-
}
70-
}
71-
return null;
60+
return findEditActions(actions, actionKind, title)
61+
.firstWhere((element) => true, orElse: () => null);
62+
}
63+
64+
List<CodeAction> findEditActions(List<Either2<Command, CodeAction>> actions,
65+
CodeActionKind actionKind, String title) {
66+
return actions
67+
.map((action) => action.map((cmd) => null, (action) => action))
68+
.where((action) => action?.kind == actionKind && action?.title == title)
69+
.map((action) {
70+
// Expect matching actions to contain an edit and not a command.
71+
assert(action.command == null);
72+
assert(action.edit != null);
73+
return action;
74+
}).toList();
7275
}
7376

7477
/// Verifies that executing the given code actions command on the server

pkg/analysis_server/test/lsp/code_actions_fixes_test.dart

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,47 @@ class FixesCodeActionsTest extends AbstractCodeActionsTest {
9494
expect(contents[mainFilePath], equals(expectedContent));
9595
}
9696

97+
Future<void> test_noDuplicates() async {
98+
const content = '''
99+
var a = [Test, Test, Te[[]]st];
100+
''';
101+
102+
await newFile(mainFilePath, content: withoutMarkers(content));
103+
await initialize(
104+
textDocumentCapabilities: withCodeActionKinds(
105+
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
106+
);
107+
108+
final codeActions = await getCodeActions(mainFileUri.toString(),
109+
range: rangeFromMarkers(content));
110+
final createClassActions = findEditActions(codeActions,
111+
CodeActionKind('quickfix.create.class'), "Create class 'Test'");
112+
113+
expect(createClassActions, hasLength(1));
114+
expect(createClassActions.first.diagnostics, hasLength(3));
115+
}
116+
117+
Future<void> test_noDuplicates_withDocumentChangesSupport() async {
118+
const content = '''
119+
var a = [Test, Test, Te[[]]st];
120+
''';
121+
122+
await newFile(mainFilePath, content: withoutMarkers(content));
123+
await initialize(
124+
textDocumentCapabilities: withCodeActionKinds(
125+
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
126+
workspaceCapabilities: withApplyEditSupport(
127+
withDocumentChangesSupport(emptyWorkspaceClientCapabilities)));
128+
129+
final codeActions = await getCodeActions(mainFileUri.toString(),
130+
range: rangeFromMarkers(content));
131+
final createClassActions = findEditActions(codeActions,
132+
CodeActionKind('quickfix.create.class'), "Create class 'Test'");
133+
134+
expect(createClassActions, hasLength(1));
135+
expect(createClassActions.first.diagnostics, hasLength(3));
136+
}
137+
97138
Future<void> test_nonDartFile() async {
98139
await newFile(pubspecFilePath, content: simplePubspecContent);
99140
await initialize(

0 commit comments

Comments
 (0)