Add per-file-target-version option (#16257)

## Summary

This PR is another step in preparing to detect syntax errors in the
parser. It introduces the new `per-file-target-version` top-level
configuration option, which holds a mapping of compiled glob patterns to
Python versions. I intend to use the
`LinterSettings::resolve_target_version` method here to pass to the
parser:


f50849aeef/crates/ruff_linter/src/linter.rs (L491-L493)

## Test Plan

I added two new CLI tests to show that the `per-file-target-version` is
respected in both the formatter and the linter.
This commit is contained in:
Brent Westbrook 2025-02-24 08:47:13 -05:00 committed by GitHub
parent 42a5f5ef6a
commit e7a6c19e3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
78 changed files with 820 additions and 274 deletions

View file

@ -1,3 +1,5 @@
use std::path::Path;
use ruff_formatter::PrintedRange;
use ruff_python_ast::PySourceType;
use ruff_python_formatter::{format_module_source, FormatModuleError};
@ -10,8 +12,10 @@ pub(crate) fn format(
document: &TextDocument,
source_type: PySourceType,
formatter_settings: &FormatterSettings,
path: Option<&Path>,
) -> crate::Result<Option<String>> {
let format_options = formatter_settings.to_format_options(source_type, document.contents());
let format_options =
formatter_settings.to_format_options(source_type, document.contents(), path);
match format_module_source(document.contents(), format_options) {
Ok(formatted) => {
let formatted = formatted.into_code();
@ -36,8 +40,10 @@ pub(crate) fn format_range(
source_type: PySourceType,
formatter_settings: &FormatterSettings,
range: TextRange,
path: Option<&Path>,
) -> crate::Result<Option<PrintedRange>> {
let format_options = formatter_settings.to_format_options(source_type, document.contents());
let format_options =
formatter_settings.to_format_options(source_type, document.contents(), path);
match ruff_python_formatter::format_range(document.contents(), range, format_options) {
Ok(formatted) => {
@ -56,3 +62,146 @@ pub(crate) fn format_range(
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::format::{format, format_range};
use crate::TextDocument;
#[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()
},
Some(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()
},
Some(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,
Some(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,
Some(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(())
}
}

View file

@ -85,9 +85,10 @@ fn format_text_document(
let settings = query.settings();
// If the document is excluded, return early.
if let Some(file_path) = query.file_path() {
let file_path = query.file_path();
if let Some(file_path) = &file_path {
if is_document_excluded_for_formatting(
&file_path,
file_path,
&settings.file_resolver,
&settings.formatter,
text_document.language_id(),
@ -97,8 +98,13 @@ fn format_text_document(
}
let source = text_document.contents();
let formatted = crate::format::format(text_document, query.source_type(), &settings.formatter)
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
let formatted = crate::format::format(
text_document,
query.source_type(),
&settings.formatter,
file_path.as_deref(),
)
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
let Some(mut formatted) = formatted else {
return Ok(None);
};

View file

@ -49,9 +49,10 @@ fn format_text_document_range(
let settings = query.settings();
// If the document is excluded, return early.
if let Some(file_path) = query.file_path() {
let file_path = query.file_path();
if let Some(file_path) = &file_path {
if is_document_excluded_for_formatting(
&file_path,
file_path,
&settings.file_resolver,
&settings.formatter,
text_document.language_id(),
@ -68,6 +69,7 @@ fn format_text_document_range(
query.source_type(),
&settings.formatter,
range,
file_path.as_deref(),
)
.with_failure_code(lsp_server::ErrorCode::InternalError)?;