mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-19 03:48:29 +00:00
Merge 166b9d18ee into 665f68036c
This commit is contained in:
commit
6952d06812
8 changed files with 386 additions and 45 deletions
|
|
@ -554,8 +554,10 @@ pub struct FormatCommand {
|
|||
/// - The start position is optional. You can write `--range=-3` to format the first three lines of the document.
|
||||
///
|
||||
/// The option can only be used when formatting a single file. Range formatting of notebooks is unsupported.
|
||||
///
|
||||
/// The option can be specified multiple times to format more than one block.
|
||||
#[clap(long, help_heading = "Editor options", verbatim_doc_comment)]
|
||||
pub range: Option<FormatRange>,
|
||||
pub range: Option<Vec<FormatRange>>,
|
||||
|
||||
/// Exit with a non-zero status code if any files were modified via format, even if all files were formatted successfully.
|
||||
#[arg(long, help_heading = "Miscellaneous", alias = "exit-non-zero-on-fix")]
|
||||
|
|
@ -802,7 +804,7 @@ impl FormatCommand {
|
|||
files: self.files,
|
||||
no_cache: self.no_cache,
|
||||
stdin_filename: self.stdin_filename,
|
||||
range: self.range,
|
||||
ranges: self.range.unwrap_or_default(),
|
||||
exit_non_zero_on_format: self.exit_non_zero_on_format,
|
||||
};
|
||||
|
||||
|
|
@ -1097,7 +1099,7 @@ pub struct FormatArguments {
|
|||
pub diff: bool,
|
||||
pub files: Vec<PathBuf>,
|
||||
pub stdin_filename: Option<PathBuf>,
|
||||
pub range: Option<FormatRange>,
|
||||
pub ranges: Vec<FormatRange>,
|
||||
pub exit_non_zero_on_format: bool,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1021,12 +1021,13 @@ mod tests {
|
|||
cache: &Cache,
|
||||
) -> Result<FormatResult, FormatCommandError> {
|
||||
let file_path = self.package_root.join(path);
|
||||
let ranges = Vec::new();
|
||||
format_path(
|
||||
&file_path,
|
||||
&self.settings.formatter,
|
||||
PySourceType::Python,
|
||||
FormatMode::Write,
|
||||
None,
|
||||
&ranges,
|
||||
Some(cache),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ use ruff_linter::rules::flake8_quotes::settings::Quote;
|
|||
use ruff_linter::source_kind::{SourceError, SourceKind};
|
||||
use ruff_linter::warn_user_once;
|
||||
use ruff_python_ast::{PySourceType, SourceType};
|
||||
use ruff_python_formatter::{FormatModuleError, QuoteStyle, format_module_source, format_range};
|
||||
use ruff_python_formatter::{
|
||||
FormatModuleError, QuoteStyle, expand_and_collapse_ranges, format_module_source, format_range,
|
||||
};
|
||||
use ruff_source_file::{LineIndex, LineRanges, OneIndexed, SourceFileBuilder};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use ruff_workspace::FormatterSettings;
|
||||
|
|
@ -84,7 +86,7 @@ pub(crate) fn format(
|
|||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
if cli.range.is_some() && paths.len() > 1 {
|
||||
if !cli.ranges.is_empty() && paths.len() > 1 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"The `--range` option is only supported when formatting a single file but the specified paths resolve to {} files.",
|
||||
paths.len()
|
||||
|
|
@ -160,7 +162,7 @@ pub(crate) fn format(
|
|||
&settings.formatter,
|
||||
source_type,
|
||||
mode,
|
||||
cli.range,
|
||||
&cli.ranges,
|
||||
cache,
|
||||
)
|
||||
}) {
|
||||
|
|
@ -263,7 +265,7 @@ pub(crate) fn format_path(
|
|||
settings: &FormatterSettings,
|
||||
source_type: PySourceType,
|
||||
mode: FormatMode,
|
||||
range: Option<FormatRange>,
|
||||
ranges: &[FormatRange],
|
||||
cache: Option<&Cache>,
|
||||
) -> Result<FormatResult, FormatCommandError> {
|
||||
if let Some(cache) = cache {
|
||||
|
|
@ -289,20 +291,37 @@ pub(crate) fn format_path(
|
|||
};
|
||||
|
||||
// Don't write back to the cache if formatting a range.
|
||||
let cache = cache.filter(|_| range.is_none());
|
||||
let cache = cache.filter(|_| ranges.is_empty());
|
||||
|
||||
// Format the source.
|
||||
let format_result = match format_source(&unformatted, source_type, Some(path), settings, range)?
|
||||
{
|
||||
FormattedSource::Formatted(formatted) => match mode {
|
||||
FormatMode::Write => {
|
||||
let mut writer = File::create(path).map_err(|err| {
|
||||
FormatCommandError::Write(Some(path.to_path_buf()), err.into())
|
||||
})?;
|
||||
formatted
|
||||
.write(&mut writer)
|
||||
.map_err(|err| FormatCommandError::Write(Some(path.to_path_buf()), err))?;
|
||||
let format_result =
|
||||
match format_source(&unformatted, source_type, Some(path), settings, ranges)? {
|
||||
FormattedSource::Formatted(formatted) => match mode {
|
||||
FormatMode::Write => {
|
||||
let mut writer = File::create(path).map_err(|err| {
|
||||
FormatCommandError::Write(Some(path.to_path_buf()), err.into())
|
||||
})?;
|
||||
formatted
|
||||
.write(&mut writer)
|
||||
.map_err(|err| FormatCommandError::Write(Some(path.to_path_buf()), err))?;
|
||||
|
||||
if let Some(cache) = cache {
|
||||
if let Ok(cache_key) = FileCacheKey::from_path(path) {
|
||||
let relative_path = cache
|
||||
.relative_path(path)
|
||||
.expect("wrong package cache for file");
|
||||
cache.set_formatted(relative_path.to_path_buf(), &cache_key);
|
||||
}
|
||||
}
|
||||
|
||||
FormatResult::Formatted
|
||||
}
|
||||
FormatMode::Check | FormatMode::Diff => FormatResult::Diff {
|
||||
unformatted,
|
||||
formatted,
|
||||
},
|
||||
},
|
||||
FormattedSource::Unchanged => {
|
||||
if let Some(cache) = cache {
|
||||
if let Ok(cache_key) = FileCacheKey::from_path(path) {
|
||||
let relative_path = cache
|
||||
|
|
@ -312,26 +331,9 @@ pub(crate) fn format_path(
|
|||
}
|
||||
}
|
||||
|
||||
FormatResult::Formatted
|
||||
FormatResult::Unchanged
|
||||
}
|
||||
FormatMode::Check | FormatMode::Diff => FormatResult::Diff {
|
||||
unformatted,
|
||||
formatted,
|
||||
},
|
||||
},
|
||||
FormattedSource::Unchanged => {
|
||||
if let Some(cache) = cache {
|
||||
if let Ok(cache_key) = FileCacheKey::from_path(path) {
|
||||
let relative_path = cache
|
||||
.relative_path(path)
|
||||
.expect("wrong package cache for file");
|
||||
cache.set_formatted(relative_path.to_path_buf(), &cache_key);
|
||||
}
|
||||
}
|
||||
|
||||
FormatResult::Unchanged
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
Ok(format_result)
|
||||
}
|
||||
|
|
@ -360,13 +362,14 @@ pub(crate) fn format_source(
|
|||
source_type: PySourceType,
|
||||
path: Option<&Path>,
|
||||
settings: &FormatterSettings,
|
||||
range: Option<FormatRange>,
|
||||
ranges: &[FormatRange],
|
||||
) -> Result<FormattedSource, FormatCommandError> {
|
||||
match &source_kind {
|
||||
SourceKind::Python(unformatted) => {
|
||||
let options = settings.to_format_options(source_type, unformatted, path);
|
||||
|
||||
let formatted = if let Some(range) = range {
|
||||
let formatted = if ranges.len() == 1 {
|
||||
let range = &ranges[0];
|
||||
let line_index = LineIndex::from_source_text(unformatted);
|
||||
let byte_range = range.to_text_range(unformatted, &line_index);
|
||||
format_range(unformatted, byte_range, options).map(|formatted_range| {
|
||||
|
|
@ -378,6 +381,55 @@ pub(crate) fn format_source(
|
|||
|
||||
formatted
|
||||
})
|
||||
} else if !ranges.is_empty() {
|
||||
// Convert to text ranges, and expand and collapse.
|
||||
let line_index = LineIndex::from_source_text(unformatted);
|
||||
let byte_ranges: Vec<TextRange> = ranges
|
||||
.iter()
|
||||
.map(|range| range.to_text_range(unformatted, &line_index))
|
||||
.collect();
|
||||
let treated_ranges: Vec<TextRange> =
|
||||
expand_and_collapse_ranges(unformatted, &byte_ranges, options.clone())
|
||||
.map_err(|err| {
|
||||
if let FormatModuleError::ParseError(err) = err {
|
||||
DisplayParseError::from_source_kind(
|
||||
err,
|
||||
path.map(Path::to_path_buf),
|
||||
source_kind,
|
||||
)
|
||||
.into()
|
||||
} else {
|
||||
FormatCommandError::Format(path.map(Path::to_path_buf), err)
|
||||
}
|
||||
})?;
|
||||
|
||||
let mut result = Ok(String::new());
|
||||
|
||||
let mut unformatted = unformatted.clone();
|
||||
|
||||
// Format in each text range starting from the bottom towards the top.
|
||||
for byte_range in treated_ranges.iter().rev() {
|
||||
result = format_range(&unformatted, *byte_range, options.clone()).map(
|
||||
|formatted_range| {
|
||||
let mut formatted = unformatted.to_string();
|
||||
formatted.replace_range(
|
||||
std::ops::Range::<usize>::from(formatted_range.source_range()),
|
||||
formatted_range.as_code(),
|
||||
);
|
||||
|
||||
unformatted.clone_from(&formatted);
|
||||
|
||||
formatted
|
||||
},
|
||||
);
|
||||
|
||||
// Formatting will be halted on first error.
|
||||
if result.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
} else {
|
||||
// Using `Printed::into_code` requires adding `ruff_formatter` as a direct dependency, and I suspect that Rust can optimize the closure away regardless.
|
||||
#[expect(clippy::redundant_closure_for_method_calls)]
|
||||
|
|
@ -408,7 +460,7 @@ pub(crate) fn format_source(
|
|||
return Ok(FormattedSource::Unchanged);
|
||||
}
|
||||
|
||||
if range.is_some() {
|
||||
if !ranges.is_empty() {
|
||||
return Err(FormatCommandError::RangeFormatNotebook(
|
||||
path.map(Path::to_path_buf),
|
||||
));
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ pub(crate) fn format_stdin(
|
|||
};
|
||||
|
||||
// Format the file.
|
||||
match format_source_code(path, cli.range, settings, source_type, mode) {
|
||||
match format_source_code(path, &cli.ranges, settings, source_type, mode) {
|
||||
Ok(result) => match mode {
|
||||
FormatMode::Write => Ok(ExitStatus::Success),
|
||||
FormatMode::Check | FormatMode::Diff => {
|
||||
|
|
@ -86,7 +86,7 @@ pub(crate) fn format_stdin(
|
|||
/// Format source code read from `stdin`.
|
||||
fn format_source_code(
|
||||
path: Option<&Path>,
|
||||
range: Option<FormatRange>,
|
||||
ranges: &[FormatRange],
|
||||
settings: &FormatterSettings,
|
||||
source_type: PySourceType,
|
||||
mode: FormatMode,
|
||||
|
|
@ -104,7 +104,7 @@ fn format_source_code(
|
|||
};
|
||||
|
||||
// Format the source.
|
||||
let formatted = format_source(&source_kind, source_type, path, settings, range)?;
|
||||
let formatted = format_source(&source_kind, source_type, path, settings, ranges)?;
|
||||
|
||||
match &formatted {
|
||||
FormattedSource::Formatted(formatted) => match mode {
|
||||
|
|
|
|||
|
|
@ -2158,6 +2158,178 @@ def foo(arg1, arg2,):
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_ranges() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
assert_cmd_snapshot!(test.format_command()
|
||||
.args(["--isolated", "--stdin-filename", "test.py", "--range=7-9", "--range=2-3", "--range=4-6"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
a = [2 ]
|
||||
b = [3 ]
|
||||
c = [4 ]
|
||||
d = [5 ]
|
||||
e = [6 ]
|
||||
f = [7 ]
|
||||
g = [8 ]
|
||||
h = [9 ]
|
||||
"#), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
a = [2]
|
||||
b = [3 ]
|
||||
c = [4]
|
||||
d = [5]
|
||||
e = [6 ]
|
||||
f = [7]
|
||||
g = [8]
|
||||
h = [9 ]
|
||||
|
||||
----- stderr -----
|
||||
"#);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_overlapping_ranges() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
assert_cmd_snapshot!(test.format_command()
|
||||
.args(["--isolated", "--stdin-filename", "test.py", "--range=5-9", "--range=5-8", "--range=4-6"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
a = [2 ]
|
||||
b = [3 ]
|
||||
c = [4 ]
|
||||
d = [5 ]
|
||||
e = [6 ]
|
||||
f = [7 ]
|
||||
g = [8 ]
|
||||
h = [9 ]
|
||||
"#), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
a = [2 ]
|
||||
b = [3 ]
|
||||
c = [4]
|
||||
d = [5]
|
||||
e = [6]
|
||||
f = [7]
|
||||
g = [8]
|
||||
h = [9 ]
|
||||
|
||||
----- stderr -----
|
||||
"#);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_ranges_in_same_statement() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
assert_cmd_snapshot!(test.format_command()
|
||||
.args(["--isolated", "--stdin-filename", "test.py", "--range=3-4", "--range=6-7"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
a = (2,
|
||||
3 ,
|
||||
4,
|
||||
5,
|
||||
6 ,
|
||||
7,
|
||||
8,
|
||||
9
|
||||
)
|
||||
b = ('a', 'b', 'c', 'd',)
|
||||
"#), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
a = (2, 3, 4, 5, 6, 7, 8, 9)
|
||||
b = ('a', 'b', 'c', 'd',)
|
||||
|
||||
----- stderr -----
|
||||
"#);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_overlapping_ranges_in_adjacent_statement() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
assert_cmd_snapshot!(test.format_command()
|
||||
.args(["--isolated", "--stdin-filename", "test.py", "--range=2-3", "--range=4-7", "--range=11-12:35", "--range=12:74-17", "--range=21-22", "--range=29-30", "--range=25-30"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def partition(array, begin,
|
||||
end):
|
||||
pivot = begin
|
||||
for i in range( begin+1,
|
||||
|
||||
end + 1):
|
||||
if array[i] <= array[begin]:
|
||||
pivot += 1
|
||||
array[i], array[ pivot ] = array[
|
||||
pivot], array[ i]
|
||||
array[pivot], array[begin ] = array[begin
|
||||
|
||||
], array[
|
||||
|
||||
pivot
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
]
|
||||
return pivot
|
||||
|
||||
|
||||
|
||||
def quicksort(array, begin=0, end=None):
|
||||
if end is None:
|
||||
end = len( array) - 1
|
||||
def _quicksort(array, begin , end):
|
||||
if begin >= end:
|
||||
return
|
||||
pivot = partition(array, begin, end)
|
||||
_quicksort(array, begin, pivot-1)
|
||||
_quicksort(array, pivot+1, end)
|
||||
return _quicksort(array, begin, end)
|
||||
"#), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
def partition(array, begin, end):
|
||||
pivot = begin
|
||||
for i in range(begin + 1, end + 1):
|
||||
if array[i] <= array[begin]:
|
||||
pivot += 1
|
||||
array[i], array[pivot] = array[pivot], array[i]
|
||||
array[pivot], array[begin] = array[begin], array[pivot]
|
||||
return pivot
|
||||
|
||||
|
||||
def quicksort(array, begin=0, end=None):
|
||||
if end is None:
|
||||
end = len( array) - 1
|
||||
def _quicksort(array, begin , end):
|
||||
if begin >= end:
|
||||
return
|
||||
pivot = partition(array, begin, end)
|
||||
_quicksort(array, begin, pivot-1)
|
||||
_quicksort(array, pivot+1, end)
|
||||
return _quicksort(array, begin, end)
|
||||
|
||||
----- stderr -----
|
||||
"#);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_missing_line() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use ruff_db::source::source_text;
|
|||
use thiserror::Error;
|
||||
use tracing::Level;
|
||||
|
||||
pub use range::format_range;
|
||||
pub use range::{expand_and_collapse_ranges, format_range};
|
||||
use ruff_formatter::prelude::*;
|
||||
use ruff_formatter::{FormatError, Formatted, PrintError, Printed, SourceCode, format, write};
|
||||
use ruff_python_ast::{AnyNodeRef, Mod};
|
||||
|
|
|
|||
|
|
@ -117,6 +117,83 @@ pub fn format_range(
|
|||
Ok(printed.slice_range(narrowed_range, source))
|
||||
}
|
||||
|
||||
/// Expands the text ranges into their enclosing nodes, and collapses those that overlap or touch.
|
||||
pub fn expand_and_collapse_ranges(
|
||||
source: &str,
|
||||
ranges: &[TextRange],
|
||||
options: PyFormatOptions,
|
||||
) -> Result<Vec<TextRange>, FormatModuleError> {
|
||||
for range in ranges {
|
||||
// Error if the specified range lies outside of the source file.
|
||||
if source.text_len() < range.end() {
|
||||
return Err(FormatModuleError::FormatError(FormatError::RangeError {
|
||||
input: *range,
|
||||
tree: TextRange::up_to(source.text_len()),
|
||||
}));
|
||||
}
|
||||
|
||||
// If any range covers the entire file, that becomes the only range and no processing is
|
||||
// required.
|
||||
if *range == TextRange::up_to(source.text_len()) {
|
||||
return Ok(vec![*range]);
|
||||
}
|
||||
}
|
||||
|
||||
let parsed = parse(source, ParseOptions::from(options.source_type()))?;
|
||||
let source_code = SourceCode::new(source);
|
||||
let comment_ranges = CommentRanges::from(parsed.tokens());
|
||||
let comments = Comments::from_ast(parsed.syntax(), source_code, &comment_ranges);
|
||||
|
||||
let context = PyFormatContext::new(
|
||||
options.with_source_map_generation(SourceMapGeneration::Enabled),
|
||||
source,
|
||||
comments,
|
||||
parsed.tokens(),
|
||||
);
|
||||
|
||||
let mut expanded_ranges: Vec<TextRange> = Vec::new();
|
||||
for range in ranges {
|
||||
if range.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let enclosing_node =
|
||||
match find_enclosing_node(*range, AnyNodeRef::from(parsed.syntax()), &context) {
|
||||
EnclosingNode::Node {
|
||||
node,
|
||||
indent_level: _,
|
||||
} => node,
|
||||
EnclosingNode::Suppressed => {
|
||||
// The entire range falls into a suppressed range. There's nothing to format.
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let narrowed_range = narrow_range(*range, enclosing_node, &context);
|
||||
assert_valid_char_boundaries(narrowed_range, source);
|
||||
|
||||
expanded_ranges.push(narrowed_range);
|
||||
}
|
||||
if expanded_ranges.is_empty() {
|
||||
return Ok(expanded_ranges);
|
||||
}
|
||||
expanded_ranges.sort();
|
||||
|
||||
let mut collapsed_ranges: Vec<TextRange> = Vec::new();
|
||||
collapsed_ranges.push(expanded_ranges[0]);
|
||||
for range in expanded_ranges {
|
||||
let back_range_index = collapsed_ranges.len() - 1;
|
||||
let back_range = &collapsed_ranges[back_range_index];
|
||||
if back_range.overlap_or_touch(&range) {
|
||||
collapsed_ranges[back_range_index] = back_range.joined(&range);
|
||||
} else {
|
||||
collapsed_ranges.push(range);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(collapsed_ranges)
|
||||
}
|
||||
|
||||
/// Finds the node with the minimum covering range of `range`.
|
||||
///
|
||||
/// It traverses the tree and returns the deepest node that fully encloses `range`.
|
||||
|
|
|
|||
|
|
@ -269,6 +269,30 @@ impl TextRange {
|
|||
self.cover(TextRange::empty(offset))
|
||||
}
|
||||
|
||||
/// Checks if the text ranges overlap or touch each other.
|
||||
#[must_use]
|
||||
pub fn overlap_or_touch(&self, other: &Self) -> bool {
|
||||
(self.start <= other.start && other.start <= self.end)
|
||||
|| (other.start <= self.start && self.start <= other.end)
|
||||
}
|
||||
|
||||
/// Joins the two text ranges.
|
||||
#[must_use]
|
||||
pub fn joined(&self, other: &Self) -> Self {
|
||||
Self {
|
||||
start: if self.start < other.start {
|
||||
self.start
|
||||
} else {
|
||||
other.start
|
||||
},
|
||||
end: if self.end > other.end {
|
||||
self.end
|
||||
} else {
|
||||
other.end
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an offset to this range.
|
||||
///
|
||||
/// Note that this is not appropriate for changing where a `TextRange` is
|
||||
|
|
@ -441,6 +465,19 @@ impl TextRange {
|
|||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for TextRange {
|
||||
#[inline]
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for TextRange {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.start.cmp(&other.start).then(self.end.cmp(&other.end))
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<TextRange> for str {
|
||||
type Output = str;
|
||||
#[inline]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue