Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enable automatically fix diagnostics #16715

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/ide-assists/src/assist_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ pub struct AssistConfig {
pub prefer_no_std: bool,
pub prefer_prelude: bool,
pub assist_emit_must_use: bool,
// If set to `Some(...)`, we just get the only assist corresponding to this diagnostic code.
pub specified_diagnostic_code: Option<String>,
}
3 changes: 3 additions & 0 deletions crates/ide-assists/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub(crate) const TEST_CONFIG: AssistConfig = AssistConfig {
prefer_no_std: false,
prefer_prelude: true,
assist_emit_must_use: false,
specified_diagnostic_code: None,
};

pub(crate) const TEST_CONFIG_NO_SNIPPET_CAP: AssistConfig = AssistConfig {
Expand All @@ -48,6 +49,7 @@ pub(crate) const TEST_CONFIG_NO_SNIPPET_CAP: AssistConfig = AssistConfig {
prefer_no_std: false,
prefer_prelude: true,
assist_emit_must_use: false,
specified_diagnostic_code: None,
};

pub(crate) const TEST_CONFIG_IMPORT_ONE: AssistConfig = AssistConfig {
Expand All @@ -63,6 +65,7 @@ pub(crate) const TEST_CONFIG_IMPORT_ONE: AssistConfig = AssistConfig {
prefer_no_std: false,
prefer_prelude: true,
assist_emit_must_use: false,
specified_diagnostic_code: None,
};

pub(crate) fn with_single_file(text: &str) -> (RootDatabase, FileId) {
Expand Down
23 changes: 17 additions & 6 deletions crates/ide/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -649,23 +649,34 @@ impl Analysis {
};

self.with_db(|db| {
let specific_diagnostic_code = assist_config.specified_diagnostic_code.as_ref();
let diagnostic_assists = if diagnostics_config.enabled && include_fixes {
ide_diagnostics::diagnostics(db, diagnostics_config, &resolve, frange.file_id)
.into_iter()
.filter(|it| {
specific_diagnostic_code.map_or(true, |specific_diagnostic_code| {
it.code.as_str() == specific_diagnostic_code
})
})
.flat_map(|it| it.fixes.unwrap_or_default())
.filter(|it| it.target.intersect(frange.range).is_some())
.collect()
} else {
Vec::new()
};
let ssr_assists = ssr::ssr_assists(db, &resolve, frange);
let assists = ide_assists::assists(db, assist_config, resolve, frange);

let mut res = diagnostic_assists;
res.extend(ssr_assists);
res.extend(assists);
if specific_diagnostic_code.is_some() {
diagnostic_assists
} else {
let ssr_assists = ssr::ssr_assists(db, &resolve, frange);
let assists = ide_assists::assists(db, assist_config, resolve, frange);

let mut res = diagnostic_assists;
res.extend(ssr_assists);
res.extend(assists);

res
res
}
})
}

Expand Down
1 change: 1 addition & 0 deletions crates/rust-analyzer/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1584,6 +1584,7 @@ impl Config {
prefer_no_std: self.data.imports_preferNoStd,
prefer_prelude: self.data.imports_preferPrelude,
assist_emit_must_use: self.data.assist_emitMustUse,
specified_diagnostic_code: None,
}
}

Expand Down
43 changes: 38 additions & 5 deletions crates/rust-analyzer/src/handlers/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ use lsp_types::{
CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams, CallHierarchyItem,
CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams, CallHierarchyPrepareParams,
CodeLens, CompletionItem, FoldingRange, FoldingRangeParams, HoverContents, InlayHint,
InlayHintParams, Location, LocationLink, Position, PrepareRenameResponse, Range, RenameParams,
ResourceOp, ResourceOperationKind, SemanticTokensDeltaParams, SemanticTokensFullDeltaResult,
SemanticTokensParams, SemanticTokensRangeParams, SemanticTokensRangeResult,
SemanticTokensResult, SymbolInformation, SymbolTag, TextDocumentIdentifier, Url, WorkspaceEdit,
InlayHintParams, Location, LocationLink, NumberOrString, Position, PrepareRenameResponse,
Range, RenameParams, ResourceOp, ResourceOperationKind, SemanticTokensDeltaParams,
SemanticTokensFullDeltaResult, SemanticTokensParams, SemanticTokensRangeParams,
SemanticTokensRangeResult, SemanticTokensResult, SymbolInformation, SymbolTag,
TextDocumentIdentifier, Url, WorkspaceEdit,
};
use project_model::{ManifestPath, ProjectWorkspace, TargetKind};
use serde_json::json;
Expand Down Expand Up @@ -1115,6 +1116,33 @@ pub(crate) fn handle_code_action(
) -> anyhow::Result<Option<Vec<lsp_ext::CodeAction>>> {
let _p = tracing::span!(tracing::Level::INFO, "handle_code_action").entered();

let assists_config = snap.config.assist();

fetch_assist(snap, params, assists_config)
}

pub(crate) fn handle_code_action_for_specified_diagnostic(
snap: GlobalStateSnapshot,
params: lsp_types::CodeActionParams,
) -> anyhow::Result<Option<Vec<lsp_ext::CodeAction>>> {
let _p = tracing::span!(tracing::Level::INFO, "handle_code_action_for_specified_diagnostic")
.entered();

let mut assists_config = snap.config.assist();
assists_config.specified_diagnostic_code =
params.context.diagnostics.first().and_then(|it| match it.code.clone()? {
NumberOrString::String(code) => Some(code),
_ => None,
});

fetch_assist(snap, params, assists_config)
}

fn fetch_assist(
snap: GlobalStateSnapshot,
params: lsp_types::CodeActionParams,
mut assists_config: ide::AssistConfig,
) -> anyhow::Result<Option<Vec<lsp_ext::CodeAction>>> {
if !snap.config.code_action_literals() {
// We intentionally don't support command-based actions, as those either
// require either custom client-code or server-initiated edits. Server
Expand All @@ -1126,7 +1154,6 @@ pub(crate) fn handle_code_action(
snap.file_line_index(from_proto::file_id(&snap, &params.text_document.uri)?)?;
let frange = from_proto::file_range(&snap, &params.text_document, params.range)?;

let mut assists_config = snap.config.assist();
assists_config.allowed = params
.context
.only
Expand Down Expand Up @@ -1165,6 +1192,12 @@ pub(crate) fn handle_code_action(
res.push(code_action)
}

// FIXME: currently we only support native diagnostic fix from rust-analyzer(crates/ide-diagnostics/src/handlers/), ideally we should support fix from `cargo check` also.
if assists_config.specified_diagnostic_code.is_some() {
return Ok(Some(res));
}

// FIXME: need to filter out fixes(from `cargo check`) which not corresponding to `assists_config.specified_diagnostic_code`.
// Fixes from `cargo check`.
for fix in snap.check_fixes.values().filter_map(|it| it.get(&frange.file_id)).flatten() {
// FIXME: this mapping is awkward and shouldn't exist. Refactor
Expand Down
8 changes: 8 additions & 0 deletions crates/rust-analyzer/src/lsp/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,14 @@ impl Request for CodeActionRequest {
const METHOD: &'static str = "textDocument/codeAction";
}

pub enum CodeActionForDiagnosticRequest {}

impl Request for CodeActionForDiagnosticRequest {
type Params = lsp_types::CodeActionParams;
type Result = Option<Vec<CodeAction>>;
const METHOD: &'static str = "rust-analyzer/codeActionForDiagnostic";
}

pub enum CodeActionResolveRequest {}

impl Request for CodeActionResolveRequest {
Expand Down
3 changes: 3 additions & 0 deletions crates/rust-analyzer/src/main_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,9 @@ impl GlobalState {
.on::<lsp_ext::Runnables>(handlers::handle_runnables)
.on::<lsp_ext::RelatedTests>(handlers::handle_related_tests)
.on::<lsp_ext::CodeActionRequest>(handlers::handle_code_action)
.on::<lsp_ext::CodeActionForDiagnosticRequest>(
handlers::handle_code_action_for_specified_diagnostic,
)
.on::<lsp_ext::CodeActionResolveRequest>(handlers::handle_code_action_resolve)
.on::<lsp_ext::HoverRequest>(handlers::handle_hover)
.on::<lsp_ext::ExternalDocs>(handlers::handle_open_docs)
Expand Down
2 changes: 1 addition & 1 deletion docs/dev/lsp-extensions.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!---
lsp/ext.rs hash: 8be79cc3b7f10ad7
lsp/ext.rs hash: 681f7433af4db5c2

If you need to change the above hash to make the test pass, please check if you
need to adjust this doc as well and ping this issue:
Expand Down
8 changes: 8 additions & 0 deletions editors/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,14 @@
"type": "object",
"title": "rust-analyzer",
"properties": {
"rust-analyzer.autoFixDiagnostics": {
"type": "array",
"default": [],
"items": {
"type": "string"
},
"description": "List of diagnostic to automatically apply fix."
},
"rust-analyzer.cargoRunner": {
"type": [
"null",
Expand Down
5 changes: 5 additions & 0 deletions editors/code/src/lsp_ext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export type CommandLinkGroup = {
export const analyzerStatus = new lc.RequestType<AnalyzerStatusParams, string, void>(
"rust-analyzer/analyzerStatus",
);
export const codeActionForDiagnostic = new lc.RequestType<
lc.CodeActionParams,
(lc.Command | lc.CodeAction)[] | null,
void
>("rust-analyzer/codeActionForDiagnostic");
export const cancelFlycheck = new lc.NotificationType0("rust-analyzer/cancelFlycheck");
export const clearFlycheck = new lc.NotificationType0("rust-analyzer/clearFlycheck");
export const expandMacro = new lc.RequestType<ExpandMacroParams, ExpandedMacro | null, void>(
Expand Down
68 changes: 68 additions & 0 deletions editors/code/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as diagnostics from "./diagnostics";
import { activateTaskProvider } from "./tasks";
import { setContextValue } from "./util";
import type { JsonProject } from "./rust_project";
import * as ra from "./lsp_ext";

const RUST_PROJECT_CONTEXT_NAME = "inRustProject";

Expand Down Expand Up @@ -105,6 +106,18 @@ async function activateServer(ctx: Ctx): Promise<RustAnalyzerExtensionApi> {
null,
ctx.subscriptions,
);
vscode.workspace.onWillSaveTextDocument(async (event) => {
const client = ctx.client;
const document = event.document;
if (document.languageId === "rust" && client) {
// get 'rust-analyzer.autoFixDiagnostics' configuration, empty by default
const diagnosticsToFix =
vscode.workspace
.getConfiguration("rust-analyzer")
.get<string[]>("autoFixDiagnostics") || [];
event.waitUntil(autoFixDiagnostics(document, diagnosticsToFix, client));
}
});

await ctx.start();
return ctx;
Expand Down Expand Up @@ -214,3 +227,58 @@ function checkConflictingExtensions() {
.then(() => {}, console.error);
}
}

async function autoFixDiagnostics(
document: vscode.TextDocument,
diagnosticsToFix: string[],
client: lc.LanguageClient,
) {
// get the diagnosis specified by the user for the current document
const getDiagnostics = () => {
const isInclude = (diagnostic: vscode.Diagnostic) => {
const diagnosticCode =
typeof diagnostic.code === "string" || typeof diagnostic.code === "number"
? diagnostic.code
: diagnostic.code?.value || "";
return diagnosticsToFix.includes(diagnosticCode.toString());
};

const diagnostics = vscode.languages.getDiagnostics(document.uri);
return diagnostics.filter((diagnostic) => isInclude(diagnostic));
};

let diagnostics = getDiagnostics();

while (diagnostics.length !== 0) {
const currentDiagnostic = diagnostics.at(0);
if (!currentDiagnostic) return;
const params: lc.CodeActionParams = {
textDocument: { uri: document.uri.toString() },
range: currentDiagnostic.range,
context: {
diagnostics: [client.code2ProtocolConverter.asDiagnostic(currentDiagnostic)],
},
};

const actions = await client.sendRequest(ra.codeActionForDiagnostic, params);
const action = actions?.at(0);
if (lc.CodeAction.is(action) && action.edit) {
const edit = await client.protocol2CodeConverter.asWorkspaceEdit(action.edit);
await vscode.workspace.applyEdit(edit);
} else if (action) {
const resolvedCodeAction = await client.sendRequest(
lc.CodeActionResolveRequest.type,
action,
);
if (resolvedCodeAction.edit) {
const edit = await client.protocol2CodeConverter.asWorkspaceEdit(
resolvedCodeAction.edit,
);
await vscode.workspace.applyEdit(edit);
}
}

// after the above `applyEdit(edit)`, source code changed, so we refresh the diagnostics
diagnostics = getDiagnostics();
}
}