ruff/crates/ruff_server/src/fix.rs
Dhruv Manilawala cc0a5dd14a
Directly include Settings struct for the server (#16042)
## Summary

This PR refactors the `RuffSettings` struct to directly include the
resolved `Settings` instead of including the specific fields from it.
The server utilizes a lot of it already, so it makes sense to just
include the entire struct for simplicity.

### `Deref`

I implemented `Deref` on `RuffSettings` to return the `Settings` because
`RuffSettings` is now basically a wrapper around it with the config path
as the other field. This path field is only used for debugging
("printDebugInformation" command).
2025-02-10 10:20:01 +05:30

157 lines
5 KiB
Rust

use std::borrow::Cow;
use rustc_hash::FxHashMap;
use crate::{
edit::{Replacement, ToRangeExt},
resolve::is_document_excluded_for_linting,
session::DocumentQuery,
PositionEncoding,
};
use ruff_linter::package::PackageRoot;
use ruff_linter::{
linter::{FixerResult, LinterResult},
packaging::detect_package_root,
settings::{flags, LinterSettings},
};
use ruff_notebook::SourceValue;
use ruff_source_file::LineIndex;
/// A simultaneous fix made across a single text document or among an arbitrary
/// number of notebook cells.
pub(crate) type Fixes = FxHashMap<lsp_types::Url, Vec<lsp_types::TextEdit>>;
pub(crate) fn fix_all(
query: &DocumentQuery,
linter_settings: &LinterSettings,
encoding: PositionEncoding,
) -> crate::Result<Fixes> {
let source_kind = query.make_source_kind();
let settings = query.settings();
let document_path = query.file_path();
// If the document is excluded, return an empty list of fixes.
let package = if let Some(document_path) = document_path.as_ref() {
if is_document_excluded_for_linting(
document_path,
&settings.file_resolver,
linter_settings,
query.text_document_language_id(),
) {
return Ok(Fixes::default());
}
detect_package_root(
document_path
.parent()
.expect("a path to a document should have a parent path"),
&linter_settings.namespace_packages,
)
.map(PackageRoot::root)
} else {
None
};
let source_type = query.source_type();
// We need to iteratively apply all safe fixes onto a single file and then
// create a diff between the modified file and the original source to use as a single workspace
// edit.
// If we simply generated the diagnostics with `check_path` and then applied fixes individually,
// there's a possibility they could overlap or introduce new problems that need to be fixed,
// which is inconsistent with how `ruff check --fix` works.
let FixerResult {
transformed,
result: LinterResult {
has_syntax_error, ..
},
..
} = ruff_linter::linter::lint_fix(
&query.virtual_file_path(),
package,
flags::Noqa::Enabled,
settings.unsafe_fixes,
linter_settings,
&source_kind,
source_type,
)?;
if has_syntax_error {
// If there's a syntax error, then there won't be any fixes to apply.
return Ok(Fixes::default());
}
// fast path: if `transformed` is still borrowed, no changes were made and we can return early
if let Cow::Borrowed(_) = transformed {
return Ok(Fixes::default());
}
if let (Some(source_notebook), Some(modified_notebook)) =
(source_kind.as_ipy_notebook(), transformed.as_ipy_notebook())
{
fn cell_source(cell: &ruff_notebook::Cell) -> String {
match cell.source() {
SourceValue::String(string) => string.clone(),
SourceValue::StringArray(array) => array.join(""),
}
}
let Some(notebook) = query.as_notebook() else {
anyhow::bail!("Notebook document expected from notebook source kind");
};
let mut fixes = Fixes::default();
for ((source, modified), url) in source_notebook
.cells()
.iter()
.map(cell_source)
.zip(modified_notebook.cells().iter().map(cell_source))
.zip(notebook.urls())
{
let source_index = LineIndex::from_source_text(&source);
let modified_index = LineIndex::from_source_text(&modified);
let Replacement {
source_range,
modified_range,
} = Replacement::between(
&source,
source_index.line_starts(),
&modified,
modified_index.line_starts(),
);
fixes.insert(
url.clone(),
vec![lsp_types::TextEdit {
range: source_range.to_range(&source, &source_index, encoding),
new_text: modified[modified_range].to_owned(),
}],
);
}
Ok(fixes)
} else {
let source_index = LineIndex::from_source_text(source_kind.source_code());
let modified = transformed.source_code();
let modified_index = LineIndex::from_source_text(modified);
let Replacement {
source_range,
modified_range,
} = Replacement::between(
source_kind.source_code(),
source_index.line_starts(),
modified,
modified_index.line_starts(),
);
Ok([(
query.make_key().into_url(),
vec![lsp_types::TextEdit {
range: source_range.to_range(source_kind.source_code(), &source_index, encoding),
new_text: modified[modified_range].to_owned(),
}],
)]
.into_iter()
.collect())
}
}