[analysis_server] Move CodeActionKind filter earlier in production of code actions

This removes a filter of CodeActionKinds from the final step (and a class named `_CodeActionSorter`!) and instead applies it earlier during building of the actions. This will help apply the filter in the case where we return bare Commands (which don't have `kind`s) instead of `CodeActionLiteral`s.

I added some TODOs because I still don't think this is filtering early enough (because in the case of invoking an action via the command, we need to not have to produce _all_ code actions just to locate the _one_ we want to execute), but it's a step closer (and easier to review this without it being lumped in with the CL that supports returning Commands).

Change-Id: Ibc73c4900d939ac833f6345e9e8f1fdf3f5d6823
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/427000
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
Danny Tuppeny
2025-05-06 11:03:33 -07:00
committed by Commit Queue
parent b32e5e5a91
commit 3c161d3fd1
10 changed files with 151 additions and 52 deletions
@@ -63,20 +63,26 @@ abstract class AbstractCodeActionsProducer
bool get supportsLiterals => capabilities.literalCodeActions;
/// Creates a CodeAction to apply this assist. Note: This code will fetch the
/// version of each document being modified so it's important to call this
/// immediately after computing edits to ensure the document is not modified
/// before the version number is read.
/// Creates a CodeAction to apply this assist.
///
/// This code will fetch the version of each document being modified so it's
/// important to call this immediately after computing edits to ensure the
/// document is not modified before the version number is read.
@protected
CodeActionLiteral createAssistAction(
protocol.SourceChange change,
CodeActionKind kind,
String? loggedAssistId,
String path,
LineInfo lineInfo,
) {
assert(
kind == CodeActionKind.Refactor ||
'$kind'.startsWith('${CodeActionKind.Refactor}.'),
);
return CodeActionLiteral(
title: change.message,
kind: toCodeActionKind(change.id, CodeActionKind.Refactor),
kind: kind,
diagnostics: const [],
command: createLogActionCommand(loggedAssistId),
edit: createWorkspaceEdit(
@@ -106,21 +112,27 @@ abstract class AbstractCodeActionsProducer
);
}
/// Creates a CodeAction to apply this fix. Note: This code will fetch the
/// version of each document being modified so it's important to call this
/// immediately after computing edits to ensure the document is not modified
/// before the version number is read.
/// Creates a CodeAction to apply this fix.
///
/// This code will fetch the version of each document being modified so it's
/// important to call this immediately after computing edits to ensure the
/// document is not modified before the version number is read.
@protected
CodeActionLiteral createFixAction(
protocol.SourceChange change,
CodeActionKind kind,
String? loggedFixId,
Diagnostic diagnostic,
String path,
LineInfo lineInfo,
) {
assert(
kind == CodeActionKind.QuickFix ||
'$kind'.startsWith('${CodeActionKind.QuickFix}.'),
);
return CodeActionLiteral(
title: change.message,
kind: toCodeActionKind(change.id, CodeActionKind.QuickFix),
kind: kind,
diagnostics: [diagnostic],
command: createLogActionCommand(loggedFixId),
edit: createWorkspaceEdit(
@@ -6,6 +6,7 @@ import 'dart:async';
import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/lsp/handlers/code_actions/abstract_code_actions_producer.dart';
import 'package:analysis_server/src/lsp/mapping.dart';
import 'package:analysis_server/src/services/correction/fix/analysis_options/fix_generator.dart';
import 'package:analyzer/source/file_source.dart';
import 'package:analyzer/source/line_info.dart';
@@ -95,15 +96,22 @@ class AnalysisOptionsCodeActionsProducer extends AbstractCodeActionsProducer {
var diagnostic = createDiagnostic(lineInfo, result, error);
codeActions.addAll(
fixes.map((fix) {
var kind = toCodeActionKind(fix.change.id, CodeActionKind.QuickFix);
// TODO(dantup): Find a way to filter these earlier, so we don't
// compute fixes we will filter out.
if (!shouldIncludeKind(kind)) {
return null;
}
var action = createFixAction(
fix.change,
kind,
fix.change.id,
diagnostic,
path,
lineInfo,
);
return (action: action, priority: fix.kind.priority);
}),
}).nonNulls,
);
}
@@ -162,15 +162,28 @@ class DartCodeActionsProducer extends AbstractCodeActionsProducer {
assists = await computeAssists(context);
}
return assists.map((assist) {
var action = createAssistAction(
assist.change,
assist.change.id,
unitResult.path,
unitResult.lineInfo,
);
return (action: action, priority: assist.kind.priority);
}).toList();
return assists
.map((assist) {
var kind = toCodeActionKind(
assist.change.id,
CodeActionKind.Refactor,
);
// TODO(dantup): Find a way to filter these earlier, so we don't
// compute fixes we will filter out.
if (!shouldIncludeKind(kind)) {
return null;
}
var action = createAssistAction(
assist.change,
kind,
assist.change.id,
unitResult.path,
unitResult.lineInfo,
);
return (action: action, priority: assist.kind.priority);
})
.nonNulls
.toList();
} on InconsistentAnalysisException {
// If an InconsistentAnalysisException occurs, it's likely the user modified
// the source and therefore is no longer interested in the results, so
@@ -247,15 +260,25 @@ class DartCodeActionsProducer extends AbstractCodeActionsProducer {
);
codeActions.addAll(
fixes.map((fix) {
var kind = toCodeActionKind(
fix.change.id,
CodeActionKind.QuickFix,
);
// TODO(dantup): Find a way to filter these earlier, so we don't
// compute fixes we will filter out.
if (!shouldIncludeKind(kind)) {
return null;
}
var action = createFixAction(
fix.change,
kind,
fix.change.id,
diagnostic,
path,
lineInfo,
);
return (action: action, priority: fix.kind.priority);
}),
}).nonNulls,
);
}
}
@@ -300,7 +323,11 @@ class DartCodeActionsProducer extends AbstractCodeActionsProducer {
performance: performanceTracker,
);
var actions = await processor.compute();
refactorActions.addAll(actions.map(CodeAction.t1));
refactorActions.addAll(
actions
.where((literal) => shouldIncludeKind(literal.kind))
.map(CodeAction.t1),
);
// Extracts
if (shouldIncludeKind(CodeActionKind.RefactorExtract)) {
@@ -48,6 +48,7 @@ class PluginCodeActionsProducer extends AbstractCodeActionsProducer {
.map((response) => plugin.EditGetAssistsResult.fromResponse(response))
.expand((response) => response.assists)
.map(_convertAssist)
.nonNulls
.toList();
}
@@ -78,10 +79,20 @@ class PluginCodeActionsProducer extends AbstractCodeActionsProducer {
@override
Future<List<CodeAction>> getSourceActions() async => [];
CodeActionWithPriority _convertAssist(plugin.PrioritizedSourceChange assist) {
CodeActionWithPriority? _convertAssist(
plugin.PrioritizedSourceChange assist,
) {
var kind = toCodeActionKind(assist.change.id, CodeActionKind.Refactor);
// TODO(dantup): Find a way to filter these earlier, so we don't
// compute fixes we will filter out.
if (!shouldIncludeKind(kind)) {
return null;
}
return (
action: createAssistAction(
assist.change,
kind,
'assist from plugin',
path,
lineInfo,
@@ -100,18 +111,25 @@ class PluginCodeActionsProducer extends AbstractCodeActionsProducer {
supportedTags: supportedDiagnosticTags,
clientSupportsCodeDescription: supportsCodeDescription,
);
return fixes.fixes.map(
(fix) => (
return fixes.fixes.map((fix) {
var kind = toCodeActionKind(fix.change.id, CodeActionKind.QuickFix);
// TODO(dantup): Find a way to filter these earlier, so we don't
// compute fixes we will filter out.
if (!shouldIncludeKind(kind)) {
return null;
}
return (
action: createFixAction(
fix.change,
kind,
'fix from plugin',
diagnostic,
path,
lineInfo,
),
priority: fix.priority,
),
);
);
}).nonNulls;
}
Future<List<plugin.Response>> _sendPluginRequest(
@@ -6,6 +6,7 @@ import 'dart:async';
import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/lsp/handlers/code_actions/abstract_code_actions_producer.dart';
import 'package:analysis_server/src/lsp/mapping.dart';
import 'package:analysis_server/src/services/correction/fix/pubspec/fix_generator.dart';
import 'package:analyzer/source/file_source.dart';
import 'package:analyzer/source/line_info.dart';
@@ -81,15 +82,22 @@ class PubspecCodeActionsProducer extends AbstractCodeActionsProducer {
var diagnostic = createDiagnostic(lineInfo, result, error);
codeActions.addAll(
fixes.map((fix) {
var kind = toCodeActionKind(fix.change.id, CodeActionKind.QuickFix);
// TODO(dantup): Find a way to filter these earlier, so we don't
// compute fixes we will filter out.
if (!shouldIncludeKind(kind)) {
return null;
}
var action = createFixAction(
fix.change,
kind,
fix.change.id,
diagnostic,
path,
lineInfo,
);
return (action: action, priority: fix.kind.priority);
}),
}).nonNulls,
);
}
@@ -218,7 +218,7 @@ class CodeActionHandler
analysisOptions: analysisOptions,
),
];
var sorter = _CodeActionSorter(params.range, shouldIncludeKind);
var sorter = _CodeActionSorter(params.range);
var allActions = <CodeAction>[
// Like-kinded actions are grouped (and prioritized) together
@@ -303,9 +303,8 @@ class CodeActionRegistrations extends FeatureRegistration
/// the one nearest [range].
class _CodeActionSorter {
final Range range;
final bool Function(CodeActionKind?) shouldIncludeKind;
_CodeActionSorter(this.range, this.shouldIncludeKind);
_CodeActionSorter(this.range);
List<CodeAction> sort(List<CodeActionWithPriority> actions) {
var dedupedActions = _dedupeActions(actions, range.start);
@@ -323,7 +322,6 @@ class _CodeActionSorter {
dedupedActionsWithIndex.sort(_compareCodeActions);
return dedupedActionsWithIndex
.where((action) => shouldIncludeKind(action.action.kind))
.map((action) => CodeAction.t1(action.action))
.toList();
}
@@ -312,9 +312,10 @@ Future foo;
ofKind(CodeActionKind kind) =>
getCodeActions(mainFileUri, range: code.range.range, kinds: [kind]);
// The code above will return a quickfix.remove.unusedImport
// The code above will return a 'quickfix.remove.unusedImport'.
expect(await ofKind(CodeActionKind.QuickFix), isNotEmpty);
expect(await ofKind(CodeActionKind('quickfix.remove')), isNotEmpty);
expect(await ofKind(CodeActionKind('quickfix.remove.foo')), isEmpty);
expect(await ofKind(CodeActionKind('quickfix.other')), isEmpty);
expect(await ofKind(CodeActionKind.Refactor), isEmpty);
}
@@ -12,7 +12,6 @@ import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../tool/lsp_spec/matchers.dart';
import '../utils/lsp_protocol_extensions.dart';
import '../utils/test_code_extensions.dart';
import 'code_actions_abstract.dart';
import 'request_helpers_mixin.dart';
@@ -255,22 +254,12 @@ void f() {
ofKind(CodeActionKind kind) =>
getCodeActions(mainFileUri, range: code.range.range, kinds: [kind]);
// Helper that requests CodeActions for [kind] and ensures all results
// returned have either an equal kind, or a kind that is prefixed with the
// requested kind followed by a dot.
Future<void> checkResults(CodeActionKind kind) async {
var results = await ofKind(kind);
for (var result in results) {
var resultKind = result.asCodeActionLiteral.kind;
expect('$resultKind', anyOf([equals('$kind'), startsWith('$kind.')]));
}
}
// Check a few of each that will produces multiple matches and no matches.
await checkResults(CodeActionKind.Refactor);
await checkResults(CodeActionKind.RefactorExtract);
await checkResults(CodeActionKind('refactor.extract.foo'));
await checkResults(CodeActionKind.RefactorRewrite);
// The code above will return a 'refactor.extract' (as well as some other
// refactors, but not rewrite).
expect(await ofKind(CodeActionKind.Refactor), isNotEmpty);
expect(await ofKind(CodeActionKind.RefactorExtract), isNotEmpty);
expect(await ofKind(CodeActionKind('refactor.extract.foo')), isEmpty);
expect(await ofKind(CodeActionKind.RefactorRewrite), isEmpty);
}
Future<void> test_generatesNames() async {
@@ -18,6 +18,7 @@ import 'package:path/path.dart' as path;
import 'package:test/test.dart' as test show expect;
import 'package:test/test.dart';
import '../utils/lsp_protocol_extensions.dart';
import 'change_verifier.dart';
/// A mixin with helpers for applying LSP edits to strings.
@@ -346,7 +347,7 @@ mixin LspRequestHelpersMixin {
List<CodeActionKind>? kinds,
CodeActionTriggerKind? triggerKind,
ProgressToken? workDoneToken,
}) {
}) async {
range ??=
position != null
? Range(start: position, end: position)
@@ -367,7 +368,8 @@ mixin LspRequestHelpersMixin {
workDoneToken: workDoneToken,
),
);
return expectSuccessfulResponseTo(
var actions = await expectSuccessfulResponseTo(
request,
_fromJsonList(
_generateFromJsonFor(
@@ -378,6 +380,26 @@ mixin LspRequestHelpersMixin {
),
),
);
// As an additional check, ensure all returned values are either exact
// matches or sub-kinds of the requested kind(s).
if (kinds != null && kinds.isNotEmpty) {
// Kinds must either by an exact match, or start with the
// requested value followed by a dot (a sub-kind).
var allowedKinds =
kinds
.expand((kind) => [equals('$kind'), startsWith('$kind.')])
.toList();
// Only CodeActionLiterals can be checked because bare commands do not
// have CodeActionKinds (once they've left the server).
var literals = actions.where((action) => action.isCodeActionLiteral);
for (var result in literals) {
expect(result.asCodeActionLiteral.kind.toString(), anyOf(allowedKinds));
}
}
return actions;
}
Future<TextDocumentCodeLensResult> getCodeLens(Uri uri) {
@@ -27,6 +27,22 @@ extension CodeActionExtensions on CodeAction {
return map((literal) => literal.command, (command) => command);
}
/// Whether this [CodeAction] is a [CodeActionLiteral].
bool get isCodeActionLiteral {
return map(
(_) => true, // literal
(_) => false, // command
);
}
/// Whether this [CodeAction] is a [Command].
bool get isCommand {
return map(
(_) => false, // literal
(_) => true, // command
);
}
/// The title for this [CodeAction], whether it's a [CodeActionLiteral]
/// or a [Command].
String get title {