mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-20 04:29:47 +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 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 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)]
|
#[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.
|
/// 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")]
|
#[arg(long, help_heading = "Miscellaneous", alias = "exit-non-zero-on-fix")]
|
||||||
|
|
@ -802,7 +804,7 @@ impl FormatCommand {
|
||||||
files: self.files,
|
files: self.files,
|
||||||
no_cache: self.no_cache,
|
no_cache: self.no_cache,
|
||||||
stdin_filename: self.stdin_filename,
|
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,
|
exit_non_zero_on_format: self.exit_non_zero_on_format,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1097,7 +1099,7 @@ pub struct FormatArguments {
|
||||||
pub diff: bool,
|
pub diff: bool,
|
||||||
pub files: Vec<PathBuf>,
|
pub files: Vec<PathBuf>,
|
||||||
pub stdin_filename: Option<PathBuf>,
|
pub stdin_filename: Option<PathBuf>,
|
||||||
pub range: Option<FormatRange>,
|
pub ranges: Vec<FormatRange>,
|
||||||
pub exit_non_zero_on_format: bool,
|
pub exit_non_zero_on_format: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1021,12 +1021,13 @@ mod tests {
|
||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
) -> Result<FormatResult, FormatCommandError> {
|
) -> Result<FormatResult, FormatCommandError> {
|
||||||
let file_path = self.package_root.join(path);
|
let file_path = self.package_root.join(path);
|
||||||
|
let ranges = Vec::new();
|
||||||
format_path(
|
format_path(
|
||||||
&file_path,
|
&file_path,
|
||||||
&self.settings.formatter,
|
&self.settings.formatter,
|
||||||
PySourceType::Python,
|
PySourceType::Python,
|
||||||
FormatMode::Write,
|
FormatMode::Write,
|
||||||
None,
|
&ranges,
|
||||||
Some(cache),
|
Some(cache),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,9 @@ use ruff_linter::rules::flake8_quotes::settings::Quote;
|
||||||
use ruff_linter::source_kind::{SourceError, SourceKind};
|
use ruff_linter::source_kind::{SourceError, SourceKind};
|
||||||
use ruff_linter::warn_user_once;
|
use ruff_linter::warn_user_once;
|
||||||
use ruff_python_ast::{PySourceType, SourceType};
|
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_source_file::{LineIndex, LineRanges, OneIndexed, SourceFileBuilder};
|
||||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||||
use ruff_workspace::FormatterSettings;
|
use ruff_workspace::FormatterSettings;
|
||||||
|
|
@ -84,7 +86,7 @@ pub(crate) fn format(
|
||||||
return Ok(ExitStatus::Success);
|
return Ok(ExitStatus::Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
if cli.range.is_some() && paths.len() > 1 {
|
if !cli.ranges.is_empty() && paths.len() > 1 {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"The `--range` option is only supported when formatting a single file but the specified paths resolve to {} files.",
|
"The `--range` option is only supported when formatting a single file but the specified paths resolve to {} files.",
|
||||||
paths.len()
|
paths.len()
|
||||||
|
|
@ -160,7 +162,7 @@ pub(crate) fn format(
|
||||||
&settings.formatter,
|
&settings.formatter,
|
||||||
source_type,
|
source_type,
|
||||||
mode,
|
mode,
|
||||||
cli.range,
|
&cli.ranges,
|
||||||
cache,
|
cache,
|
||||||
)
|
)
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -263,7 +265,7 @@ pub(crate) fn format_path(
|
||||||
settings: &FormatterSettings,
|
settings: &FormatterSettings,
|
||||||
source_type: PySourceType,
|
source_type: PySourceType,
|
||||||
mode: FormatMode,
|
mode: FormatMode,
|
||||||
range: Option<FormatRange>,
|
ranges: &[FormatRange],
|
||||||
cache: Option<&Cache>,
|
cache: Option<&Cache>,
|
||||||
) -> Result<FormatResult, FormatCommandError> {
|
) -> Result<FormatResult, FormatCommandError> {
|
||||||
if let Some(cache) = cache {
|
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.
|
// 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.
|
// Format the source.
|
||||||
let format_result = match format_source(&unformatted, source_type, Some(path), settings, range)?
|
let format_result =
|
||||||
{
|
match format_source(&unformatted, source_type, Some(path), settings, ranges)? {
|
||||||
FormattedSource::Formatted(formatted) => match mode {
|
FormattedSource::Formatted(formatted) => match mode {
|
||||||
FormatMode::Write => {
|
FormatMode::Write => {
|
||||||
let mut writer = File::create(path).map_err(|err| {
|
let mut writer = File::create(path).map_err(|err| {
|
||||||
FormatCommandError::Write(Some(path.to_path_buf()), err.into())
|
FormatCommandError::Write(Some(path.to_path_buf()), err.into())
|
||||||
})?;
|
})?;
|
||||||
formatted
|
formatted
|
||||||
.write(&mut writer)
|
.write(&mut writer)
|
||||||
.map_err(|err| FormatCommandError::Write(Some(path.to_path_buf()), err))?;
|
.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 Some(cache) = cache {
|
||||||
if let Ok(cache_key) = FileCacheKey::from_path(path) {
|
if let Ok(cache_key) = FileCacheKey::from_path(path) {
|
||||||
let relative_path = cache
|
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)
|
Ok(format_result)
|
||||||
}
|
}
|
||||||
|
|
@ -360,13 +362,14 @@ pub(crate) fn format_source(
|
||||||
source_type: PySourceType,
|
source_type: PySourceType,
|
||||||
path: Option<&Path>,
|
path: Option<&Path>,
|
||||||
settings: &FormatterSettings,
|
settings: &FormatterSettings,
|
||||||
range: Option<FormatRange>,
|
ranges: &[FormatRange],
|
||||||
) -> Result<FormattedSource, FormatCommandError> {
|
) -> Result<FormattedSource, FormatCommandError> {
|
||||||
match &source_kind {
|
match &source_kind {
|
||||||
SourceKind::Python(unformatted) => {
|
SourceKind::Python(unformatted) => {
|
||||||
let options = settings.to_format_options(source_type, unformatted, path);
|
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 line_index = LineIndex::from_source_text(unformatted);
|
||||||
let byte_range = range.to_text_range(unformatted, &line_index);
|
let byte_range = range.to_text_range(unformatted, &line_index);
|
||||||
format_range(unformatted, byte_range, options).map(|formatted_range| {
|
format_range(unformatted, byte_range, options).map(|formatted_range| {
|
||||||
|
|
@ -378,6 +381,55 @@ pub(crate) fn format_source(
|
||||||
|
|
||||||
formatted
|
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 {
|
} else {
|
||||||
// Using `Printed::into_code` requires adding `ruff_formatter` as a direct dependency, and I suspect that Rust can optimize the closure away regardless.
|
// 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)]
|
#[expect(clippy::redundant_closure_for_method_calls)]
|
||||||
|
|
@ -408,7 +460,7 @@ pub(crate) fn format_source(
|
||||||
return Ok(FormattedSource::Unchanged);
|
return Ok(FormattedSource::Unchanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
if range.is_some() {
|
if !ranges.is_empty() {
|
||||||
return Err(FormatCommandError::RangeFormatNotebook(
|
return Err(FormatCommandError::RangeFormatNotebook(
|
||||||
path.map(Path::to_path_buf),
|
path.map(Path::to_path_buf),
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ pub(crate) fn format_stdin(
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format the file.
|
// 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 {
|
Ok(result) => match mode {
|
||||||
FormatMode::Write => Ok(ExitStatus::Success),
|
FormatMode::Write => Ok(ExitStatus::Success),
|
||||||
FormatMode::Check | FormatMode::Diff => {
|
FormatMode::Check | FormatMode::Diff => {
|
||||||
|
|
@ -86,7 +86,7 @@ pub(crate) fn format_stdin(
|
||||||
/// Format source code read from `stdin`.
|
/// Format source code read from `stdin`.
|
||||||
fn format_source_code(
|
fn format_source_code(
|
||||||
path: Option<&Path>,
|
path: Option<&Path>,
|
||||||
range: Option<FormatRange>,
|
ranges: &[FormatRange],
|
||||||
settings: &FormatterSettings,
|
settings: &FormatterSettings,
|
||||||
source_type: PySourceType,
|
source_type: PySourceType,
|
||||||
mode: FormatMode,
|
mode: FormatMode,
|
||||||
|
|
@ -104,7 +104,7 @@ fn format_source_code(
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format the source.
|
// 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 {
|
match &formatted {
|
||||||
FormattedSource::Formatted(formatted) => match mode {
|
FormattedSource::Formatted(formatted) => match mode {
|
||||||
|
|
|
||||||
|
|
@ -2158,6 +2158,178 @@ def foo(arg1, arg2,):
|
||||||
Ok(())
|
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]
|
#[test]
|
||||||
fn range_missing_line() -> Result<()> {
|
fn range_missing_line() -> Result<()> {
|
||||||
let test = CliTest::new()?;
|
let test = CliTest::new()?;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use ruff_db::source::source_text;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
|
|
||||||
pub use range::format_range;
|
pub use range::{expand_and_collapse_ranges, format_range};
|
||||||
use ruff_formatter::prelude::*;
|
use ruff_formatter::prelude::*;
|
||||||
use ruff_formatter::{FormatError, Formatted, PrintError, Printed, SourceCode, format, write};
|
use ruff_formatter::{FormatError, Formatted, PrintError, Printed, SourceCode, format, write};
|
||||||
use ruff_python_ast::{AnyNodeRef, Mod};
|
use ruff_python_ast::{AnyNodeRef, Mod};
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,83 @@ pub fn format_range(
|
||||||
Ok(printed.slice_range(narrowed_range, source))
|
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`.
|
/// Finds the node with the minimum covering range of `range`.
|
||||||
///
|
///
|
||||||
/// It traverses the tree and returns the deepest node that fully encloses `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))
|
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.
|
/// Add an offset to this range.
|
||||||
///
|
///
|
||||||
/// Note that this is not appropriate for changing where a `TextRange` is
|
/// 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 {
|
impl Index<TextRange> for str {
|
||||||
type Output = str;
|
type Output = str;
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue