mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-27 18:36:35 +00:00
## Summary Ref: https://github.com/astral-sh/ruff/issues/14820#issuecomment-2996690681 This PR fixes a bug where virtual paths or any paths that doesn't exists on the file system weren't being considered for checking inclusion / exclusion. This was because the logic used `file_path` which returns `None` for those path. This PR fixes that by using the `virtual_file_path` method that returns a `Path` corresponding to the actual file on disk or any kind of virtual path. This should ideally just fix the above linked issue by way of excluding the documents representing the interactive window because they aren't in the inclusion set. It failed only on Windows previously because the file path construction would fail and then Ruff would default to including all the files. ## Test Plan On my machine, the `.interactive` paths are always excluded so I'm using the inclusion set instead: ```json { "ruff.nativeServer": "on", "ruff.path": ["/Users/dhruv/work/astral/ruff/target/debug/ruff"], "ruff.configuration": { "extend-include": ["*.interactive"] } } ``` The diagnostics are shown for both the file paths and the interactive window: <img width="1727" alt="Screenshot 2025-06-24 at 14 56 40" src="https://github.com/user-attachments/assets/d36af96a-777e-4367-8acf-4d9c9014d025" /> And, the logs: ``` 2025-06-24 14:56:26.478275000 DEBUG notification{method="notebookDocument/didChange"}: Included path via `extend-include`: /Interactive-1.interactive ``` And, when using `ruff.exclude` via: ```json { "ruff.exclude": ["*.interactive"] } ``` With logs: ``` 2025-06-24 14:58:41.117743000 DEBUG notification{method="notebookDocument/didChange"}: Ignored path via `exclude`: /Interactive-1.interactive ```
207 lines
6.6 KiB
Rust
207 lines
6.6 KiB
Rust
use std::path::Path;
|
|
|
|
use ruff_formatter::PrintedRange;
|
|
use ruff_python_ast::PySourceType;
|
|
use ruff_python_formatter::{FormatModuleError, format_module_source};
|
|
use ruff_text_size::TextRange;
|
|
use ruff_workspace::FormatterSettings;
|
|
|
|
use crate::edit::TextDocument;
|
|
|
|
pub(crate) fn format(
|
|
document: &TextDocument,
|
|
source_type: PySourceType,
|
|
formatter_settings: &FormatterSettings,
|
|
path: &Path,
|
|
) -> crate::Result<Option<String>> {
|
|
let format_options =
|
|
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
|
|
match format_module_source(document.contents(), format_options) {
|
|
Ok(formatted) => {
|
|
let formatted = formatted.into_code();
|
|
if formatted == document.contents() {
|
|
Ok(None)
|
|
} else {
|
|
Ok(Some(formatted))
|
|
}
|
|
}
|
|
// Special case - syntax/parse errors are handled here instead of
|
|
// being propagated as visible server errors.
|
|
Err(FormatModuleError::ParseError(error)) => {
|
|
tracing::warn!("Unable to format document: {error}");
|
|
Ok(None)
|
|
}
|
|
Err(err) => Err(err.into()),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn format_range(
|
|
document: &TextDocument,
|
|
source_type: PySourceType,
|
|
formatter_settings: &FormatterSettings,
|
|
range: TextRange,
|
|
path: &Path,
|
|
) -> crate::Result<Option<PrintedRange>> {
|
|
let format_options =
|
|
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
|
|
|
|
match ruff_python_formatter::format_range(document.contents(), range, format_options) {
|
|
Ok(formatted) => {
|
|
if formatted.as_code() == document.contents() {
|
|
Ok(None)
|
|
} else {
|
|
Ok(Some(formatted))
|
|
}
|
|
}
|
|
// Special case - syntax/parse errors are handled here instead of
|
|
// being propagated as visible server errors.
|
|
Err(FormatModuleError::ParseError(error)) => {
|
|
tracing::warn!("Unable to format document range: {error}");
|
|
Ok(None)
|
|
}
|
|
Err(err) => Err(err.into()),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::path::Path;
|
|
|
|
use insta::assert_snapshot;
|
|
use ruff_linter::settings::types::{CompiledPerFileTargetVersionList, PerFileTargetVersion};
|
|
use ruff_python_ast::{PySourceType, PythonVersion};
|
|
use ruff_text_size::{TextRange, TextSize};
|
|
use ruff_workspace::FormatterSettings;
|
|
|
|
use crate::TextDocument;
|
|
use crate::format::{format, format_range};
|
|
|
|
#[test]
|
|
fn format_per_file_version() {
|
|
let document = TextDocument::new(r#"
|
|
with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a_really_long_baz") as baz:
|
|
pass
|
|
"#.to_string(), 0);
|
|
let per_file_target_version =
|
|
CompiledPerFileTargetVersionList::resolve(vec![PerFileTargetVersion::new(
|
|
"test.py".to_string(),
|
|
PythonVersion::PY310,
|
|
Some(Path::new(".")),
|
|
)])
|
|
.unwrap();
|
|
let result = format(
|
|
&document,
|
|
PySourceType::Python,
|
|
&FormatterSettings {
|
|
unresolved_target_version: PythonVersion::PY38,
|
|
per_file_target_version,
|
|
..Default::default()
|
|
},
|
|
Path::new("test.py"),
|
|
)
|
|
.expect("Expected no errors when formatting")
|
|
.expect("Expected formatting changes");
|
|
|
|
assert_snapshot!(result, @r#"
|
|
with (
|
|
open("a_really_long_foo") as foo,
|
|
open("a_really_long_bar") as bar,
|
|
open("a_really_long_baz") as baz,
|
|
):
|
|
pass
|
|
"#);
|
|
|
|
// same as above but without the per_file_target_version override
|
|
let result = format(
|
|
&document,
|
|
PySourceType::Python,
|
|
&FormatterSettings {
|
|
unresolved_target_version: PythonVersion::PY38,
|
|
..Default::default()
|
|
},
|
|
Path::new("test.py"),
|
|
)
|
|
.expect("Expected no errors when formatting")
|
|
.expect("Expected formatting changes");
|
|
|
|
assert_snapshot!(result, @r#"
|
|
with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open(
|
|
"a_really_long_baz"
|
|
) as baz:
|
|
pass
|
|
"#);
|
|
}
|
|
|
|
#[test]
|
|
fn format_per_file_version_range() -> anyhow::Result<()> {
|
|
// prepare a document with formatting changes before and after the intended range (the
|
|
// context manager)
|
|
let document = TextDocument::new(r#"
|
|
def fn(x: str) -> Foo | Bar: return foobar(x)
|
|
|
|
with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a_really_long_baz") as baz:
|
|
pass
|
|
|
|
sys.exit(
|
|
1
|
|
)
|
|
"#.to_string(), 0);
|
|
|
|
let start = document.contents().find("with").unwrap();
|
|
let end = document.contents().find("pass").unwrap() + "pass".len();
|
|
let range = TextRange::new(TextSize::try_from(start)?, TextSize::try_from(end)?);
|
|
|
|
let per_file_target_version =
|
|
CompiledPerFileTargetVersionList::resolve(vec![PerFileTargetVersion::new(
|
|
"test.py".to_string(),
|
|
PythonVersion::PY310,
|
|
Some(Path::new(".")),
|
|
)])
|
|
.unwrap();
|
|
let result = format_range(
|
|
&document,
|
|
PySourceType::Python,
|
|
&FormatterSettings {
|
|
unresolved_target_version: PythonVersion::PY38,
|
|
per_file_target_version,
|
|
..Default::default()
|
|
},
|
|
range,
|
|
Path::new("test.py"),
|
|
)
|
|
.expect("Expected no errors when formatting")
|
|
.expect("Expected formatting changes");
|
|
|
|
assert_snapshot!(result.as_code(), @r#"
|
|
with (
|
|
open("a_really_long_foo") as foo,
|
|
open("a_really_long_bar") as bar,
|
|
open("a_really_long_baz") as baz,
|
|
):
|
|
pass
|
|
"#);
|
|
|
|
// same as above but without the per_file_target_version override
|
|
let result = format_range(
|
|
&document,
|
|
PySourceType::Python,
|
|
&FormatterSettings {
|
|
unresolved_target_version: PythonVersion::PY38,
|
|
..Default::default()
|
|
},
|
|
range,
|
|
Path::new("test.py"),
|
|
)
|
|
.expect("Expected no errors when formatting")
|
|
.expect("Expected formatting changes");
|
|
|
|
assert_snapshot!(result.as_code(), @r#"
|
|
with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open(
|
|
"a_really_long_baz"
|
|
) as baz:
|
|
pass
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
}
|