mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-27 12:29:28 +00:00

## Summary
This builds off of the work in
https://github.com/astral-sh/ruff/pull/10652 to implement a command
executor, backwards compatible with the commands from the previous LSP
(`ruff.applyAutofix`, `ruff.applyFormat` and
`ruff.applyOrganizeImports`).
This involved a lot of refactoring and tweaks to the code action
resolution code - the most notable change is that workspace edits are
specified in a slightly different way, using the more general `changes`
field instead of the `document_changes` field (which isn't supported on
all LSP clients). Additionally, the API for synchronous request handlers
has been updated to include access to the `Requester`, which we use to
send a `workspace/applyEdit` request to the client.
## Test Plan
7932e30f
-d944-4e35-b828-1d81aa56c087
193 lines
5.9 KiB
Rust
193 lines
5.9 KiB
Rust
//! Access to the Ruff linting API for the LSP
|
|
|
|
use std::path::Path;
|
|
|
|
use ruff_diagnostics::{Applicability, Diagnostic, DiagnosticKind, Fix};
|
|
use ruff_linter::{
|
|
directives::{extract_directives, Flags},
|
|
linter::{check_path, LinterResult, TokenSource},
|
|
registry::AsRule,
|
|
settings::{flags, LinterSettings},
|
|
source_kind::SourceKind,
|
|
};
|
|
use ruff_python_ast::PySourceType;
|
|
use ruff_python_codegen::Stylist;
|
|
use ruff_python_index::Indexer;
|
|
use ruff_python_parser::lexer::LexResult;
|
|
use ruff_python_parser::AsMode;
|
|
use ruff_source_file::Locator;
|
|
use ruff_text_size::Ranged;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::{edit::ToRangeExt, PositionEncoding, DIAGNOSTIC_NAME};
|
|
|
|
/// This is serialized on the diagnostic `data` field.
|
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
pub(crate) struct AssociatedDiagnosticData {
|
|
pub(crate) kind: DiagnosticKind,
|
|
pub(crate) fix: Fix,
|
|
pub(crate) code: String,
|
|
}
|
|
|
|
/// Describes a fix for `fixed_diagnostic` that applies `document_edits` to the source.
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) struct DiagnosticFix {
|
|
pub(crate) fixed_diagnostic: lsp_types::Diagnostic,
|
|
pub(crate) title: String,
|
|
pub(crate) code: String,
|
|
pub(crate) edits: Vec<lsp_types::TextEdit>,
|
|
}
|
|
|
|
pub(crate) fn check(
|
|
document: &crate::edit::Document,
|
|
linter_settings: &LinterSettings,
|
|
encoding: PositionEncoding,
|
|
) -> Vec<lsp_types::Diagnostic> {
|
|
let contents = document.contents();
|
|
let index = document.index().clone();
|
|
|
|
let source_type = PySourceType::default();
|
|
|
|
// TODO(jane): Support Jupyter Notebooks
|
|
let source_kind = SourceKind::Python(contents.to_string());
|
|
|
|
// Tokenize once.
|
|
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents, source_type.as_mode());
|
|
|
|
// Map row and column locations to byte slices (lazily).
|
|
let locator = Locator::with_index(contents, index);
|
|
|
|
// Detect the current code style (lazily).
|
|
let stylist = Stylist::from_tokens(&tokens, &locator);
|
|
|
|
// Extra indices from the code.
|
|
let indexer = Indexer::from_tokens(&tokens, &locator);
|
|
|
|
// Extract the `# noqa` and `# isort: skip` directives from the source.
|
|
let directives = extract_directives(&tokens, Flags::empty(), &locator, &indexer);
|
|
|
|
// Generate checks.
|
|
let LinterResult {
|
|
data: (diagnostics, _imports),
|
|
..
|
|
} = check_path(
|
|
Path::new("<filename>"),
|
|
None,
|
|
&locator,
|
|
&stylist,
|
|
&indexer,
|
|
&directives,
|
|
linter_settings,
|
|
flags::Noqa::Enabled,
|
|
&source_kind,
|
|
source_type,
|
|
TokenSource::Tokens(tokens),
|
|
);
|
|
|
|
diagnostics
|
|
.into_iter()
|
|
.map(|diagnostic| to_lsp_diagnostic(diagnostic, document, encoding))
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn fixes_for_diagnostics(
|
|
document: &crate::edit::Document,
|
|
encoding: PositionEncoding,
|
|
diagnostics: Vec<lsp_types::Diagnostic>,
|
|
) -> crate::Result<Vec<DiagnosticFix>> {
|
|
diagnostics
|
|
.into_iter()
|
|
.map(move |mut diagnostic| {
|
|
let Some(data) = diagnostic.data.take() else {
|
|
return Ok(None);
|
|
};
|
|
let fixed_diagnostic = diagnostic;
|
|
let associated_data: crate::lint::AssociatedDiagnosticData =
|
|
serde_json::from_value(data).map_err(|err| {
|
|
anyhow::anyhow!("failed to deserialize diagnostic data: {err}")
|
|
})?;
|
|
let edits = associated_data
|
|
.fix
|
|
.edits()
|
|
.iter()
|
|
.map(|edit| lsp_types::TextEdit {
|
|
range: edit
|
|
.range()
|
|
.to_range(document.contents(), document.index(), encoding),
|
|
new_text: edit.content().unwrap_or_default().to_string(),
|
|
});
|
|
Ok(Some(DiagnosticFix {
|
|
fixed_diagnostic,
|
|
code: associated_data.code,
|
|
title: associated_data
|
|
.kind
|
|
.suggestion
|
|
.unwrap_or(associated_data.kind.name),
|
|
edits: edits.collect(),
|
|
}))
|
|
})
|
|
.filter_map(crate::Result::transpose)
|
|
.collect()
|
|
}
|
|
|
|
fn to_lsp_diagnostic(
|
|
diagnostic: Diagnostic,
|
|
document: &crate::edit::Document,
|
|
encoding: PositionEncoding,
|
|
) -> lsp_types::Diagnostic {
|
|
let Diagnostic {
|
|
kind, range, fix, ..
|
|
} = diagnostic;
|
|
|
|
let rule = kind.rule();
|
|
|
|
let data = fix.and_then(|fix| {
|
|
fix.applies(Applicability::Unsafe)
|
|
.then(|| {
|
|
serde_json::to_value(&AssociatedDiagnosticData {
|
|
kind: kind.clone(),
|
|
fix,
|
|
code: rule.noqa_code().to_string(),
|
|
})
|
|
.ok()
|
|
})
|
|
.flatten()
|
|
});
|
|
|
|
let code = rule.noqa_code().to_string();
|
|
|
|
lsp_types::Diagnostic {
|
|
range: range.to_range(document.contents(), document.index(), encoding),
|
|
severity: Some(severity(&code)),
|
|
tags: tags(&code),
|
|
code: Some(lsp_types::NumberOrString::String(code)),
|
|
code_description: rule.url().and_then(|url| {
|
|
Some(lsp_types::CodeDescription {
|
|
href: lsp_types::Url::parse(&url).ok()?,
|
|
})
|
|
}),
|
|
source: Some(DIAGNOSTIC_NAME.into()),
|
|
message: kind.body,
|
|
related_information: None,
|
|
data,
|
|
}
|
|
}
|
|
|
|
fn severity(code: &str) -> lsp_types::DiagnosticSeverity {
|
|
match code {
|
|
// F821: undefined name <name>
|
|
// E902: IOError
|
|
// E999: SyntaxError
|
|
"F821" | "E902" | "E999" => lsp_types::DiagnosticSeverity::ERROR,
|
|
_ => lsp_types::DiagnosticSeverity::WARNING,
|
|
}
|
|
}
|
|
|
|
fn tags(code: &str) -> Option<Vec<lsp_types::DiagnosticTag>> {
|
|
match code {
|
|
// F401: <module> imported but unused
|
|
// F841: local variable <name> is assigned to but never used
|
|
"F401" | "F841" => Some(vec![lsp_types::DiagnosticTag::UNNECESSARY]),
|
|
_ => None,
|
|
}
|
|
}
|