mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-03 18:28:56 +00:00
Range formatting API (#9635)
This commit is contained in:
parent
6bb126415d
commit
ce14f4dea5
65 changed files with 3273 additions and 762 deletions
|
@ -328,6 +328,12 @@ pub struct SourcePosition(TextSize);
|
|||
|
||||
impl<Context> Format<Context> for SourcePosition {
|
||||
fn fmt(&self, f: &mut Formatter<Context>) -> FormatResult<()> {
|
||||
if let Some(FormatElement::SourcePosition(last_position)) = f.buffer.elements().last() {
|
||||
if *last_position == self.0 {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
f.write_element(FormatElement::SourcePosition(self.0));
|
||||
|
||||
Ok(())
|
||||
|
@ -353,8 +359,8 @@ where
|
|||
Context: FormatContext,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter<Context>) -> FormatResult<()> {
|
||||
if let Some(source_position) = self.position {
|
||||
f.write_element(FormatElement::SourcePosition(source_position));
|
||||
if let Some(position) = self.position {
|
||||
source_position(position).fmt(f)?;
|
||||
}
|
||||
|
||||
f.write_element(FormatElement::Text {
|
||||
|
|
|
@ -53,7 +53,7 @@ pub use crate::diagnostics::{ActualStart, FormatError, InvalidDocumentError, Pri
|
|||
pub use format_element::{normalize_newlines, FormatElement, LINE_TERMINATORS};
|
||||
pub use group_id::GroupId;
|
||||
use ruff_macros::CacheKey;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash, CacheKey)]
|
||||
#[cfg_attr(
|
||||
|
@ -432,6 +432,90 @@ impl Printed {
|
|||
pub fn take_verbatim_ranges(&mut self) -> Vec<TextRange> {
|
||||
std::mem::take(&mut self.verbatim_ranges)
|
||||
}
|
||||
|
||||
/// Slices the formatted code to the sub-slices that covers the passed `source_range`.
|
||||
///
|
||||
/// The implementation uses the source map generated during formatting to find the closest range
|
||||
/// in the formatted document that covers `source_range` or more. The returned slice
|
||||
/// matches the `source_range` exactly (except indent, see below) if the formatter emits [`FormatElement::SourcePosition`] for
|
||||
/// the range's offsets.
|
||||
///
|
||||
/// Returns the entire document if the source map is empty.
|
||||
#[must_use]
|
||||
pub fn slice_range(self, source_range: TextRange) -> PrintedRange {
|
||||
let mut start_marker: Option<SourceMarker> = None;
|
||||
let mut end_marker: Option<SourceMarker> = None;
|
||||
|
||||
// Note: The printer can generate multiple source map entries for the same source position.
|
||||
// For example if you have:
|
||||
// * `source_position(276)`
|
||||
// * `token("def")`
|
||||
// * `token("foo")`
|
||||
// * `source_position(284)`
|
||||
// The printer uses the source position 276 for both the tokens `def` and `foo` because that's the only position it knows of.
|
||||
//
|
||||
// Warning: Source markers are often emitted sorted by their source position but it's not guaranteed.
|
||||
// They are only guaranteed to be sorted in increasing order by their destination position.
|
||||
for marker in self.sourcemap {
|
||||
// Take the closest start marker, but skip over start_markers that have the same start.
|
||||
if marker.source <= source_range.start()
|
||||
&& !start_marker.is_some_and(|existing| existing.source >= marker.source)
|
||||
{
|
||||
start_marker = Some(marker);
|
||||
}
|
||||
|
||||
if marker.source >= source_range.end()
|
||||
&& !end_marker.is_some_and(|existing| existing.source <= marker.source)
|
||||
{
|
||||
end_marker = Some(marker);
|
||||
}
|
||||
}
|
||||
|
||||
let start = start_marker.map(|marker| marker.dest).unwrap_or_default();
|
||||
let end = end_marker.map_or_else(|| self.code.text_len(), |marker| marker.dest);
|
||||
let code_range = TextRange::new(start, end);
|
||||
|
||||
PrintedRange {
|
||||
code: self.code[code_range].to_string(),
|
||||
source_range,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct PrintedRange {
|
||||
code: String,
|
||||
source_range: TextRange,
|
||||
}
|
||||
|
||||
impl PrintedRange {
|
||||
pub fn new(code: String, source_range: TextRange) -> Self {
|
||||
Self { code, source_range }
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
code: String::new(),
|
||||
source_range: TextRange::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The formatted code.
|
||||
pub fn as_code(&self) -> &str {
|
||||
&self.code
|
||||
}
|
||||
|
||||
/// The range the formatted code corresponds to in the source document.
|
||||
pub fn source_range(&self) -> TextRange {
|
||||
self.source_range
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_code(self, code: String) -> Self {
|
||||
Self { code, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
/// Public return type of the formatter
|
||||
|
|
|
@ -60,7 +60,10 @@ impl<'a> Printer<'a> {
|
|||
document: &'a Document,
|
||||
indent: u16,
|
||||
) -> PrintResult<Printed> {
|
||||
let mut stack = PrintCallStack::new(PrintElementArgs::new(Indention::Level(indent)));
|
||||
let indentation = Indention::Level(indent);
|
||||
self.state.pending_indent = indentation;
|
||||
|
||||
let mut stack = PrintCallStack::new(PrintElementArgs::new(indentation));
|
||||
let mut queue: PrintQueue<'a> = PrintQueue::new(document.as_ref());
|
||||
|
||||
loop {
|
||||
|
@ -143,8 +146,14 @@ impl<'a> Printer<'a> {
|
|||
|
||||
FormatElement::SourcePosition(position) => {
|
||||
self.state.source_position = *position;
|
||||
// The printer defers printing indents until the next text
|
||||
// is printed. Pushing the marker now would mean that the
|
||||
// mapped range includes the indent range, which we don't want.
|
||||
// Only add a marker if we're not in an indented context, e.g. at the end of the file.
|
||||
if self.state.pending_indent.is_empty() {
|
||||
self.push_marker();
|
||||
}
|
||||
}
|
||||
|
||||
FormatElement::LineSuffixBoundary => {
|
||||
const HARD_BREAK: &FormatElement = &FormatElement::Line(LineMode::Hard);
|
||||
|
@ -511,11 +520,12 @@ impl<'a> Printer<'a> {
|
|||
dest: self.state.buffer.text_len(),
|
||||
};
|
||||
|
||||
if let Some(last) = self.state.source_markers.last() {
|
||||
if last != &marker {
|
||||
self.state.source_markers.push(marker);
|
||||
}
|
||||
} else {
|
||||
if self
|
||||
.state
|
||||
.source_markers
|
||||
.last()
|
||||
.map_or(true, |last| last != &marker)
|
||||
{
|
||||
self.state.source_markers.push(marker);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5978,9 +5978,10 @@ impl<'a> AnyNodeRef<'a> {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn visit_preorder<'b, V>(&'b self, visitor: &mut V)
|
||||
pub fn visit_preorder<'b, V>(self, visitor: &mut V)
|
||||
where
|
||||
V: PreorderVisitor<'b> + ?Sized,
|
||||
'a: 'b,
|
||||
{
|
||||
match self {
|
||||
AnyNodeRef::ModModule(node) => node.visit_preorder(visitor),
|
||||
|
|
|
@ -239,7 +239,7 @@ pub enum TraversalSignal {
|
|||
}
|
||||
|
||||
impl TraversalSignal {
|
||||
const fn is_traverse(self) -> bool {
|
||||
pub const fn is_traverse(self) -> bool {
|
||||
matches!(self, TraversalSignal::Traverse)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
[mixed_space_and_tab.py]
|
||||
generated_code = true
|
||||
ij_formatter_enabled = false
|
||||
|
||||
["range_formatting/*.py"]
|
||||
generated_code = true
|
||||
ij_formatter_enabled = false
|
20
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/ancestory.py
vendored
Normal file
20
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/ancestory.py
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
def test ():
|
||||
if True:
|
||||
print(<RANGE_START>1 + 2)
|
||||
|
||||
else:
|
||||
print(3 + 4)<RANGE_END>
|
||||
|
||||
print(" Do not format this")
|
||||
|
||||
|
||||
|
||||
def test_empty_lines ():
|
||||
if True:
|
||||
print(<RANGE_START>1 + 2)
|
||||
|
||||
|
||||
else:
|
||||
print(3 + 4)<RANGE_END>
|
||||
|
||||
print(" Do not format this")
|
44
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/clause_header.py
vendored
Normal file
44
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/clause_header.py
vendored
Normal file
|
@ -0,0 +1,44 @@
|
|||
def test(<RANGE_START>a, b, c: str<RANGE_END>, d):
|
||||
print ( "Don't format the body when only making changes to the clause header")
|
||||
|
||||
|
||||
print( "Should not get formatted")
|
||||
|
||||
|
||||
class <RANGE_START> Test(OtherClass<RANGE_END>)\
|
||||
: # comment
|
||||
|
||||
# Should not get formatted
|
||||
def __init__( self):
|
||||
print("hello")
|
||||
|
||||
print( "dont' format this")
|
||||
|
||||
|
||||
def test2(<RANGE_START>a, b, c: str, d):<RANGE_END>
|
||||
print ( "Don't format the body when only making changes to the clause header")
|
||||
|
||||
|
||||
print( "Should not get formatted")
|
||||
|
||||
|
||||
def test3(<RANGE_START>a, b, c: str, d):<RANGE_END> # fmt: skip
|
||||
print ( "Don't format the body when only making changes to the clause header")
|
||||
|
||||
|
||||
|
||||
def test4(<RANGE_START> a):
|
||||
print("Format this" )
|
||||
|
||||
if True:
|
||||
print( "and this")<RANGE_END>
|
||||
|
||||
print("Not this" )
|
||||
|
||||
|
||||
<RANGE_START>if a + b : # trailing clause header comment<RANGE_END>
|
||||
print("Not formatted" )
|
||||
|
||||
|
||||
<RANGE_START>if b + c :<RANGE_END> # trailing clause header comment
|
||||
print("Not formatted" )
|
|
@ -0,0 +1,6 @@
|
|||
def test ():
|
||||
<RANGE_START># Some leading comment
|
||||
# that spans multiple lines
|
||||
<RANGE_END>
|
||||
print("Do not format this" )
|
||||
|
23
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/decorators.py
vendored
Normal file
23
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/decorators.py
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
def test():
|
||||
|
||||
print("before" )
|
||||
|
||||
@<RANGE_START> decorator( aa)
|
||||
|
||||
<RANGE_END>def func ():
|
||||
print("Do not format this" )
|
||||
|
||||
|
||||
<RANGE_START>@ decorator( a)
|
||||
def test( a):<RANGE_END>
|
||||
print( "body")
|
||||
|
||||
print("after" )
|
||||
|
||||
|
||||
<RANGE_START>@ decorator( a)<RANGE_END>
|
||||
def test( a):
|
||||
print( "body")
|
||||
|
||||
print("after" )
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
[
|
||||
{
|
||||
"docstring_code": "enabled",
|
||||
"indent_style": "space",
|
||||
"indent_width": 4
|
||||
},
|
||||
{
|
||||
"docstring_code": "enabled",
|
||||
"docstring_code_line_width": 88,
|
||||
"indent_style": "space",
|
||||
"indent_width": 4
|
||||
}
|
||||
]
|
|
@ -0,0 +1,103 @@
|
|||
def doctest_simple ():
|
||||
<RANGE_START>"""
|
||||
Do cool stuff.
|
||||
|
||||
>>> cool_stuff( 1 )
|
||||
2
|
||||
"""
|
||||
pass<RANGE_END>
|
||||
|
||||
|
||||
def doctest_only ():
|
||||
<RANGE_START>"""
|
||||
Do cool stuff.
|
||||
|
||||
>>> def cool_stuff( x ):
|
||||
... print( f"hi {x}" );
|
||||
hi 2
|
||||
"""<RANGE_END>
|
||||
pass
|
||||
|
||||
|
||||
def in_doctest ():
|
||||
"""
|
||||
Do cool stuff.
|
||||
<RANGE_START>
|
||||
>>> cool_stuff( x )
|
||||
>>> cool_stuff( y )
|
||||
2<RANGE_END>
|
||||
"""
|
||||
pass
|
||||
|
||||
def suppressed_doctest ():
|
||||
"""
|
||||
Do cool stuff.
|
||||
<RANGE_START>
|
||||
>>> cool_stuff( x )
|
||||
>>> cool_stuff( y )
|
||||
2<RANGE_END>
|
||||
""" # fmt: skip
|
||||
pass
|
||||
|
||||
|
||||
def fmt_off_doctest ():
|
||||
# fmt: off
|
||||
"""
|
||||
Do cool stuff.
|
||||
<RANGE_START>
|
||||
>>> cool_stuff( x )
|
||||
>>> cool_stuff( y )
|
||||
2<RANGE_END>
|
||||
"""
|
||||
# fmt: on
|
||||
pass
|
||||
|
||||
|
||||
|
||||
if True:
|
||||
def doctest_long_lines():
|
||||
<RANGE_START>
|
||||
'''
|
||||
This won't get wrapped even though it exceeds our configured
|
||||
line width because it doesn't exceed the line width within this
|
||||
docstring. e.g, the `f` in `foo` is treated as the first column.
|
||||
>>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey)
|
||||
|
||||
But this one is long enough to get wrapped.
|
||||
>>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard)
|
||||
|
||||
This one doesn't get wrapped with an indent width of 4 even with `dynamic` line width
|
||||
>>> a = this_is_a_long_line(lion, giraffe, hippo, zebra, lemur, penguin, monkey)
|
||||
|
||||
This one gets wrapped with `dynamic` line width and an indent width of 4 because it exceeds the width by 1
|
||||
>>> ab = this_is_a_long_line(lion, giraffe, hippo, zebra, lemur, penguin, monkey)
|
||||
'''
|
||||
# This demonstrates a normal line that will get wrapped but won't
|
||||
# get wrapped in the docstring above because of how the line-width
|
||||
# setting gets reset at the first column in each code snippet.
|
||||
foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey)
|
||||
<RANGE_END>
|
||||
|
||||
|
||||
if True:
|
||||
def doctest_long_lines():
|
||||
<RANGE_START>'''
|
||||
This won't get wrapped even though it exceeds our configured
|
||||
line width because it doesn't exceed the line width within this
|
||||
docstring. e.g, the `f` in `foo` is treated as the first column.
|
||||
>>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey)
|
||||
|
||||
But this one is long enough to get wrapped.
|
||||
>>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard)
|
||||
|
||||
This one doesn't get wrapped with an indent width of 4 even with `dynamic` line width
|
||||
>>> a = this_is_a_long_line(lion, giraffe, hippo, zebra, lemur, penguin, monkey)
|
||||
|
||||
This one gets wrapped with `dynamic` line width and an indent width of 4 because it exceeds the width by 1
|
||||
>>> ab = this_is_a_long_line(lion, giraffe, hippo, zebra, lemur, penguin, monkey)
|
||||
'''<RANGE_END>
|
||||
# This demonstrates a normal line that will get wrapped but won't
|
||||
# get wrapped in the docstring above because of how the line-width
|
||||
# setting gets reset at the first column in each code snippet.
|
||||
foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey)
|
||||
|
1
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/empty_file.py
vendored
Normal file
1
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/empty_file.py
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<RANGE_START><RANGE_END>
|
2
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/empty_range.py
vendored
Normal file
2
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/empty_range.py
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
def test():
|
||||
<RANGE_START><RANGE_END>print( "test" )
|
24
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/fmt_on_off.py
vendored
Normal file
24
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/fmt_on_off.py
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
class MyClass:
|
||||
|
||||
# Range that falls entirely in a suppressed range
|
||||
# fmt: off<RANGE_START>
|
||||
def method( self ):
|
||||
print ( "str" )
|
||||
<RANGE_END># fmt: on
|
||||
|
||||
# This should net get formatted because it isn't in a formatting range.
|
||||
def not_in_formatting_range ( self): ...
|
||||
|
||||
|
||||
# Range that starts in a suppressed range and ends in a formatting range
|
||||
# fmt: off<RANGE_START>
|
||||
def other( self):
|
||||
print ( "str" )
|
||||
|
||||
# fmt: on
|
||||
|
||||
def formatted ( self):
|
||||
pass
|
||||
<RANGE_END>
|
||||
def outside_formatting_range (self): pass
|
||||
|
12
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/indent.options.json
vendored
Normal file
12
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/indent.options.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
[
|
||||
{
|
||||
"indent_style": "space"
|
||||
},
|
||||
{
|
||||
"indent_style": "tab"
|
||||
},
|
||||
{
|
||||
"indent_style": "space",
|
||||
"indent_width": 2
|
||||
}
|
||||
]
|
63
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/indent.py
vendored
Normal file
63
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/indent.py
vendored
Normal file
|
@ -0,0 +1,63 @@
|
|||
# Formats the entire function with tab or 4 space indentation
|
||||
# because the statement indentations don't match the preferred indentation.
|
||||
def test ():
|
||||
print("before" )
|
||||
<RANGE_START>1 + 2
|
||||
if True:
|
||||
pass
|
||||
print("Done" )<RANGE_END>
|
||||
|
||||
print("formatted" )
|
||||
|
||||
print("not formatted" )
|
||||
|
||||
def test2 ():
|
||||
print("before" )
|
||||
<RANGE_START>1 + 2
|
||||
(
|
||||
3 + 2
|
||||
)
|
||||
print("Done" )<RANGE_END>
|
||||
|
||||
print("formatted" )
|
||||
|
||||
print("not formatted" )
|
||||
|
||||
def test3 ():
|
||||
print("before" )
|
||||
<RANGE_START>1 + 2
|
||||
"""A Multiline string
|
||||
that starts at the beginning of the line and we need to preserve the leading spaces"""
|
||||
|
||||
"""A Multiline string
|
||||
that has some indentation on the second line and we need to preserve the leading spaces"""
|
||||
|
||||
print("Done" )<RANGE_END>
|
||||
|
||||
|
||||
def test4 ():
|
||||
print("before" )
|
||||
<RANGE_START>1 + 2
|
||||
"""A Multiline string
|
||||
that uses the same indentation as the formatted code will. This should not be dedented."""
|
||||
|
||||
print("Done" )<RANGE_END>
|
||||
|
||||
def test5 ():
|
||||
print("before" )
|
||||
if True:
|
||||
print("Format to fix indentation" )
|
||||
print(<RANGE_START>1 + 2)
|
||||
|
||||
else:
|
||||
print(3 + 4)<RANGE_END>
|
||||
print("Format to fix indentation" )
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def test6 ():
|
||||
<RANGE_START>
|
||||
print("Format" )
|
||||
print(3 + 4)<RANGE_END>
|
||||
print("Format to fix indentation" )
|
34
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/leading_comments.py
vendored
Normal file
34
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/leading_comments.py
vendored
Normal file
|
@ -0,0 +1,34 @@
|
|||
def test ():
|
||||
print( "hello" )
|
||||
<RANGE_START># leading comment
|
||||
1 + 2
|
||||
|
||||
print( "world" )<RANGE_END>
|
||||
|
||||
print( "unformatted" )
|
||||
|
||||
|
||||
print( "Hy" )
|
||||
|
||||
|
||||
def test2 ():
|
||||
print( "hello" )
|
||||
# Leading comments don't get formatted. That's why Ruff won't fixup
|
||||
# the indentation here. That's something we might want to explore in the future
|
||||
# leading comment 1<RANGE_START>
|
||||
# leading comment 2
|
||||
1 + 2
|
||||
|
||||
print( "world" )<RANGE_END>
|
||||
|
||||
print( "unformatted" )
|
||||
|
||||
def test3 ():
|
||||
<RANGE_START>print( "hello" )
|
||||
# leading comment 1
|
||||
# leading comment 2
|
||||
1 + 2
|
||||
|
||||
print( "world" )<RANGE_END>
|
||||
|
||||
print( "unformatted" )
|
|
@ -0,0 +1,16 @@
|
|||
def test ():
|
||||
# Leading comments before the statements that should be formatted
|
||||
# Don't duplicate the comments
|
||||
<RANGE_START>print( "format this")<RANGE_END> # trailing end of line comment
|
||||
# here's some trailing comment as well
|
||||
|
||||
print("Do not format this" )
|
||||
|
||||
def test ():
|
||||
# Leading comments before the statements that should be formatted
|
||||
# Don't duplicate the comments
|
||||
<RANGE_START>print( "format this")
|
||||
# here's some trailing comment as well
|
||||
<RANGE_END>
|
||||
|
||||
print("Do not format this 2" )
|
10
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/module.py
vendored
Normal file
10
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/module.py
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
print("Before range start" )
|
||||
|
||||
<RANGE_START>
|
||||
if a + b :
|
||||
print("formatted" )
|
||||
|
||||
print("still in range" )
|
||||
<RANGE_END>
|
||||
|
||||
print("After range end" )
|
|
@ -0,0 +1,8 @@
|
|||
def test ():
|
||||
<RANGE_START>if True:
|
||||
print( "format")
|
||||
elif False:
|
||||
print ( "and this")<RANGE_END>
|
||||
print("not this" )
|
||||
|
||||
print("nor this" )
|
69
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/regressions.py
vendored
Normal file
69
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/regressions.py
vendored
Normal file
|
@ -0,0 +1,69 @@
|
|||
class Event:
|
||||
event_name: ClassVar[str]
|
||||
|
||||
@staticmethod
|
||||
def cls_for(event_name: str) -> type[Event]:
|
||||
event_cls = _CONCRETE_EVENT_CLASSES.get(event_name)
|
||||
if event_cls is not <RANGE_START>None:
|
||||
return event_cls<RANGE_END>
|
||||
else:
|
||||
raise ValueError(f"unknown event name '{event_name}'")
|
||||
|
||||
class Event:
|
||||
event_name: ClassVar[str]
|
||||
|
||||
@staticmethod
|
||||
def cls_for(event_name: str) -> type[Event]:
|
||||
event_cls = _CONCRETE_EVENT_CLASSES.get(event_name)
|
||||
if event_cls is not None:
|
||||
<RANGE_START>
|
||||
|
||||
<RANGE_END>
|
||||
return event_cls
|
||||
else:
|
||||
raise ValueError(f"unknown event name '{event_name}'")
|
||||
|
||||
|
||||
# The user starts adding items to a list and then hits save.
|
||||
# Ruff should trim the empty lines
|
||||
a = [
|
||||
1,
|
||||
2,
|
||||
3,<RANGE_START>
|
||||
|
||||
|
||||
<RANGE_END>
|
||||
]
|
||||
|
||||
print("Don't format this" )
|
||||
|
||||
|
||||
# The user removed an argument from a call. Ruff should reformat the entire call
|
||||
call(
|
||||
a,
|
||||
<RANGE_START>
|
||||
<RANGE_END>b,
|
||||
c,
|
||||
d
|
||||
)
|
||||
|
||||
print("Don't format this" )
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# The user adds a new comment at the end:
|
||||
<RANGE_START># <RANGE_END>
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
print("Don't format this" )
|
||||
|
||||
|
||||
def convert_str(value: str) -> str: # Trailing comment
|
||||
"""Return a string as-is."""
|
||||
|
||||
<RANGE_START>
|
||||
|
||||
return value # Trailing comment
|
||||
<RANGE_END>
|
||||
def test ():
|
||||
pass
|
19
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/same_line_body.py
vendored
Normal file
19
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/same_line_body.py
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
def test(<RANGE_START>a ): <RANGE_END>print("body" )
|
||||
|
||||
|
||||
def test2( a): <RANGE_START>print("body" )<RANGE_END>
|
||||
|
||||
|
||||
def test3( a): <RANGE_START>print("body" )
|
||||
|
||||
print("more" )<RANGE_END>
|
||||
print("after" )
|
||||
|
||||
|
||||
# The if header and the print statement together are longer than 100 characters.
|
||||
# The print statement should either be wrapped to fit at the end of the if statement, or be converted to a
|
||||
# suite body
|
||||
if aaaaaaaaaaaa + bbbbbbbbbbbbbb + cccccccccccccccccc + ddd: <RANGE_START>print("aaaa long body, should wrap or be intented" )<RANGE_END>
|
||||
|
||||
# This print statement is too-long even when intented. It should be wrapped
|
||||
if aaaaaaaaaaaa + bbbbbbbbbbbbbb + cccccccccccccccccc + ddd: <RANGE_START>print("aaaa long body, should wrap or be intented", "more content to make it exceed the 88 chars limit")<RANGE_END>
|
6
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/stub.options.json
vendored
Normal file
6
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/stub.options.json
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
[
|
||||
{
|
||||
"preview": "enabled",
|
||||
"source_type": "Stub"
|
||||
}
|
||||
]
|
16
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/stub.pyi
vendored
Normal file
16
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/stub.pyi
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
# Don't collapse the ellipsis if only formatting the ellipsis line.
|
||||
class Test:
|
||||
<RANGE_START>...<RANGE_END>
|
||||
|
||||
class Test2: <RANGE_START>pass<RANGE_END>
|
||||
|
||||
class Test3: <RANGE_START>...<RANGE_END>
|
||||
|
||||
class Test4:
|
||||
# leading comment
|
||||
<RANGE_START>...<RANGE_END>
|
||||
# trailing comment
|
||||
|
||||
|
||||
class Test4:
|
||||
<RANGE_START> ...<RANGE_END>
|
30
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/trailing_comments.py
vendored
Normal file
30
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/trailing_comments.py
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
def test1 ():
|
||||
print("hello" )
|
||||
|
||||
<RANGE_START>1 + 2<RANGE_END> # trailing comment
|
||||
print ("world" )
|
||||
|
||||
def test2 ():
|
||||
print("hello" )
|
||||
# FIXME: For some reason the trailing comment here gets not formatted
|
||||
# but is correctly formatted above
|
||||
<RANGE_START>1 + 2 # trailing comment<RANGE_END>
|
||||
print ("world" )
|
||||
|
||||
def test3 ():
|
||||
print("hellO" )
|
||||
|
||||
<RANGE_START>1 + 2 # trailing comment
|
||||
|
||||
# trailing section comment
|
||||
<RANGE_END>
|
||||
|
||||
def test3 ():
|
||||
print("hellO" )
|
||||
|
||||
<RANGE_START>1 + 2 # trailing comment
|
||||
print("more" ) # trailing comment 2
|
||||
# trailing section comment
|
||||
<RANGE_END>
|
||||
|
||||
print( "world" )
|
|
@ -0,0 +1,6 @@
|
|||
def test():
|
||||
pass <RANGE_START>
|
||||
|
||||
<RANGE_END>
|
||||
|
||||
def test_formatted(): pass
|
|
@ -432,7 +432,17 @@ impl Format<PyFormatContext<'_>> for FormatNormalizedComment<'_> {
|
|||
fn fmt(&self, f: &mut Formatter<PyFormatContext>) -> FormatResult<()> {
|
||||
match self.comment {
|
||||
Cow::Borrowed(borrowed) => {
|
||||
source_text_slice(TextRange::at(self.range.start(), borrowed.text_len())).fmt(f)
|
||||
source_text_slice(TextRange::at(self.range.start(), borrowed.text_len())).fmt(f)?;
|
||||
|
||||
// Write the end position if the borrowed comment is shorter than the original comment
|
||||
// So that we still can map back the end of a comment to the formatted code.
|
||||
if f.options().source_map_generation().is_enabled()
|
||||
&& self.range.len() != borrowed.text_len()
|
||||
{
|
||||
source_position(self.range.end()).fmt(f)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Cow::Owned(ref owned) => {
|
||||
|
|
|
@ -98,7 +98,6 @@ pub(crate) use format::{
|
|||
use ruff_formatter::{SourceCode, SourceCodeSlice};
|
||||
use ruff_python_ast::visitor::preorder::{PreorderVisitor, TraversalSignal};
|
||||
use ruff_python_ast::AnyNodeRef;
|
||||
use ruff_python_ast::Mod;
|
||||
use ruff_python_trivia::{CommentRanges, PythonWhitespace};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
@ -347,10 +346,15 @@ impl<'a> Comments<'a> {
|
|||
|
||||
/// Extracts the comments from the AST.
|
||||
pub(crate) fn from_ast(
|
||||
root: &'a Mod,
|
||||
root: impl Into<AnyNodeRef<'a>>,
|
||||
source_code: SourceCode<'a>,
|
||||
comment_ranges: &'a CommentRanges,
|
||||
) -> Self {
|
||||
fn collect_comments<'a>(
|
||||
root: AnyNodeRef<'a>,
|
||||
source_code: SourceCode<'a>,
|
||||
comment_ranges: &'a CommentRanges,
|
||||
) -> Comments<'a> {
|
||||
let map = if comment_ranges.is_empty() {
|
||||
CommentsMap::new()
|
||||
} else {
|
||||
|
@ -360,7 +364,10 @@ impl<'a> Comments<'a> {
|
|||
builder.finish()
|
||||
};
|
||||
|
||||
Self::new(map, comment_ranges)
|
||||
Comments::new(map, comment_ranges)
|
||||
}
|
||||
|
||||
collect_comments(root.into(), source_code, comment_ranges)
|
||||
}
|
||||
|
||||
/// Returns `true` if the given `node` has any comments.
|
||||
|
|
|
@ -24,7 +24,7 @@ pub(crate) fn collect_comments<'a>(
|
|||
comment_ranges: &'a CommentRanges,
|
||||
) -> Vec<DecoratedComment<'a>> {
|
||||
let mut collector = CommentsVecBuilder::default();
|
||||
CommentsVisitor::new(source_code, comment_ranges, &mut collector).visit(root);
|
||||
CommentsVisitor::new(source_code, comment_ranges, &mut collector).visit(AnyNodeRef::from(root));
|
||||
collector.comments
|
||||
}
|
||||
|
||||
|
@ -52,8 +52,12 @@ impl<'a, 'builder> CommentsVisitor<'a, 'builder> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn visit(mut self, root: &'a Mod) {
|
||||
self.visit_mod(root);
|
||||
pub(super) fn visit(mut self, root: AnyNodeRef<'a>) {
|
||||
if self.enter_node(root).is_traverse() {
|
||||
root.visit_preorder(&mut self);
|
||||
}
|
||||
|
||||
self.leave_node(root);
|
||||
}
|
||||
|
||||
// Try to skip the subtree if
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use thiserror::Error;
|
||||
use tracing::Level;
|
||||
|
||||
pub use range::format_range;
|
||||
use ruff_formatter::prelude::*;
|
||||
use ruff_formatter::{format, FormatError, Formatted, PrintError, Printed, SourceCode};
|
||||
use ruff_python_ast::AstNode;
|
||||
|
@ -33,6 +34,7 @@ pub(crate) mod other;
|
|||
pub(crate) mod pattern;
|
||||
mod prelude;
|
||||
mod preview;
|
||||
mod range;
|
||||
mod shared_traits;
|
||||
pub(crate) mod statement;
|
||||
pub(crate) mod string;
|
||||
|
@ -170,8 +172,9 @@ mod tests {
|
|||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_index::tokens_and_ranges;
|
||||
use ruff_python_parser::{parse_tokens, AsMode};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use crate::{format_module_ast, format_module_source, PyFormatOptions};
|
||||
use crate::{format_module_ast, format_module_source, format_range, PyFormatOptions};
|
||||
|
||||
/// Very basic test intentionally kept very similar to the CLI
|
||||
#[test]
|
||||
|
@ -242,6 +245,34 @@ def main() -> None:
|
|||
);
|
||||
}
|
||||
|
||||
/// Use this test to quickly debug some formatting issue.
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn range_formatting_quick_test() {
|
||||
let source = r#"def test2( a): print("body" )
|
||||
"#;
|
||||
|
||||
let start = TextSize::new(20);
|
||||
let end = TextSize::new(35);
|
||||
|
||||
let source_type = PySourceType::Python;
|
||||
let options = PyFormatOptions::from_source_type(source_type);
|
||||
let printed = format_range(source, TextRange::new(start, end), options).unwrap();
|
||||
|
||||
let mut formatted = source.to_string();
|
||||
formatted.replace_range(
|
||||
std::ops::Range::<usize>::from(printed.source_range()),
|
||||
printed.as_code(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
formatted,
|
||||
r#"def test2(a):
|
||||
print("body")
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_processing() {
|
||||
use crate::prelude::*;
|
||||
|
|
|
@ -203,6 +203,12 @@ impl PyFormatOptions {
|
|||
self.preview = preview;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_source_map_generation(mut self, source_map: SourceMapGeneration) -> Self {
|
||||
self.source_map_generation = source_map;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatOptions for PyFormatOptions {
|
||||
|
|
740
crates/ruff_python_formatter/src/range.rs
Normal file
740
crates/ruff_python_formatter/src/range.rs
Normal file
|
@ -0,0 +1,740 @@
|
|||
use tracing::Level;
|
||||
|
||||
use ruff_formatter::printer::SourceMapGeneration;
|
||||
use ruff_formatter::{
|
||||
format, FormatContext, FormatError, FormatOptions, IndentStyle, PrintedRange, SourceCode,
|
||||
};
|
||||
use ruff_python_ast::visitor::preorder::{walk_body, PreorderVisitor, TraversalSignal};
|
||||
use ruff_python_ast::{AnyNode, AnyNodeRef, Stmt, StmtMatch, StmtTry};
|
||||
use ruff_python_index::tokens_and_ranges;
|
||||
use ruff_python_parser::{parse_tokens, AsMode, ParseError, ParseErrorType};
|
||||
use ruff_python_trivia::{indentation_at_offset, BackwardsTokenizer, SimpleToken, SimpleTokenKind};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::comments::Comments;
|
||||
use crate::context::{IndentLevel, NodeLevel};
|
||||
use crate::prelude::*;
|
||||
use crate::statement::suite::DocstringStmt;
|
||||
use crate::verbatim::{ends_suppression, starts_suppression};
|
||||
use crate::{format_module_source, FormatModuleError, PyFormatOptions};
|
||||
|
||||
/// Formats the given `range` in source rather than the entire file.
|
||||
///
|
||||
/// The returned formatted range guarantees to cover at least `range` (excluding whitespace), but the range might be larger.
|
||||
/// Some cases in which the returned range is larger than `range` are:
|
||||
/// * The logical lines in `range` use a indentation different from the configured [`IndentStyle`] and [`IndentWidth`].
|
||||
/// * `range` is smaller than a logical lines and the formatter needs to format the entire logical line.
|
||||
/// * `range` falls on a single line body.
|
||||
///
|
||||
/// The formatting of logical lines using range formatting should produce the same result as when formatting the entire document (for the same lines and options).
|
||||
///
|
||||
/// ## Implementation
|
||||
///
|
||||
/// This is an optimisation problem. The goal is to find the minimal range that fully covers `range`, is still formattable,
|
||||
/// and produces the same result as when formatting the entire document.
|
||||
///
|
||||
/// The implementation performs the following steps:
|
||||
/// 1. Find the deepest node that fully encloses `range`. The node with the minimum covering range.
|
||||
/// 2. Try to narrow the range found in step one by searching its children and find node and comment start and end offsets that are closer to `range`'s start and end.
|
||||
/// 3. Format the node from step 1 and use the source map information generated by the formatter to map the narrowed range in the source document to the range in the formatted output.
|
||||
/// 4. Take the formatted code and return it.
|
||||
///
|
||||
/// # Error
|
||||
/// Returns a range error if `range` lies outside of the source file.
|
||||
///
|
||||
/// # Panics
|
||||
/// If `range` doesn't point to a valid char boundaries.
|
||||
///
|
||||
/// [`IndentWidth`]: `ruff_formatter::IndentWidth`
|
||||
#[tracing::instrument(name = "format_range", level = Level::TRACE, skip_all)]
|
||||
pub fn format_range(
|
||||
source: &str,
|
||||
range: TextRange,
|
||||
options: PyFormatOptions,
|
||||
) -> Result<PrintedRange, FormatModuleError> {
|
||||
// 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()),
|
||||
}));
|
||||
}
|
||||
|
||||
// Formatting an empty string always yields an empty string. Return directly.
|
||||
if range.is_empty() {
|
||||
return Ok(PrintedRange::empty());
|
||||
}
|
||||
|
||||
if range == TextRange::up_to(source.text_len()) {
|
||||
let formatted = format_module_source(source, options)?;
|
||||
return Ok(PrintedRange::new(formatted.into_code(), range));
|
||||
}
|
||||
|
||||
let (tokens, comment_ranges) =
|
||||
tokens_and_ranges(source, options.source_type()).map_err(|err| ParseError {
|
||||
offset: err.location,
|
||||
error: ParseErrorType::Lexical(err.error),
|
||||
})?;
|
||||
|
||||
assert_valid_char_boundaries(range, source);
|
||||
|
||||
let module = parse_tokens(tokens, source, options.source_type().as_mode())?;
|
||||
let root = AnyNode::from(module);
|
||||
let source_code = SourceCode::new(source);
|
||||
let comments = Comments::from_ast(root.as_ref(), source_code, &comment_ranges);
|
||||
|
||||
let mut context = PyFormatContext::new(
|
||||
options.with_source_map_generation(SourceMapGeneration::Enabled),
|
||||
source,
|
||||
comments,
|
||||
);
|
||||
|
||||
let (enclosing_node, base_indent) = match find_enclosing_node(range, root.as_ref(), &context) {
|
||||
EnclosingNode::Node { node, indent_level } => (node, indent_level),
|
||||
EnclosingNode::Suppressed => {
|
||||
// The entire range falls into a suppressed range. There's nothing to format.
|
||||
return Ok(PrintedRange::empty());
|
||||
}
|
||||
};
|
||||
|
||||
let narrowed_range = narrow_range(range, enclosing_node, &context);
|
||||
assert_valid_char_boundaries(narrowed_range, source);
|
||||
|
||||
// Correctly initialize the node level for the blank line rules.
|
||||
if !enclosing_node.is_mod_module() {
|
||||
context.set_node_level(NodeLevel::CompoundStatement);
|
||||
context.set_indent_level(
|
||||
// Plus 1 because `IndentLevel=0` equals the module level.
|
||||
IndentLevel::new(base_indent.saturating_add(1)),
|
||||
);
|
||||
}
|
||||
|
||||
let formatted = format!(
|
||||
context,
|
||||
[FormatEnclosingNode {
|
||||
root: enclosing_node
|
||||
}]
|
||||
)?;
|
||||
|
||||
let printed = formatted.print_with_indent(base_indent)?;
|
||||
Ok(printed.slice_range(narrowed_range))
|
||||
}
|
||||
|
||||
/// Finds the node with the minimum covering range of `range`.
|
||||
///
|
||||
/// It traverses the tree and returns the deepest node that fully encloses `range`.
|
||||
///
|
||||
/// ## Eligible nodes
|
||||
/// The search is restricted to nodes that mark the start of a logical line to ensure
|
||||
/// formatting a range results in the same formatting for that logical line as when formatting the entire document.
|
||||
/// This property can't be guaranteed when supporting sub-expression formatting because
|
||||
/// a) Adding parentheses around enclosing expressions can toggle an expression from non-splittable to splittable,
|
||||
/// b) formatting a sub-expression has fewer split points than formatting the entire expressions.
|
||||
///
|
||||
/// ### Possible docstrings
|
||||
/// Strings that are suspected to be docstrings are excluded from the search to format the enclosing suite instead
|
||||
/// so that the formatter's docstring detection in [`FormatSuite`] correctly detects and formats the docstrings.
|
||||
///
|
||||
/// ### Compound statements with a simple statement body
|
||||
/// Don't include simple-statement bodies of compound statements `if True: pass` because the formatter
|
||||
/// must run [`FormatClauseBody`] to determine if the body should be collapsed or not.
|
||||
///
|
||||
/// ### Incorrectly indented code
|
||||
/// Code that uses indentations that don't match the configured [`IndentStyle`] and [`IndentWidth`] are excluded from the search,
|
||||
/// because formatting such nodes on their own can lead to indentation mismatch with its sibling nodes.
|
||||
///
|
||||
/// ## Suppression comments
|
||||
/// The search ends when `range` falls into a suppressed range because there's nothing to format. It also avoids that the
|
||||
/// formatter formats the statement because it doesn't see the suppression comment of the enclosing node.
|
||||
///
|
||||
/// The implementation doesn't handle `fmt: ignore` suppression comments because the statement's formatting logic
|
||||
/// correctly detects the suppression comment and returns the statement text as is.
|
||||
fn find_enclosing_node<'ast>(
|
||||
range: TextRange,
|
||||
root: AnyNodeRef<'ast>,
|
||||
context: &PyFormatContext<'ast>,
|
||||
) -> EnclosingNode<'ast> {
|
||||
let mut visitor = FindEnclosingNode::new(range, context);
|
||||
|
||||
if visitor.enter_node(root).is_traverse() {
|
||||
root.visit_preorder(&mut visitor);
|
||||
}
|
||||
visitor.leave_node(root);
|
||||
|
||||
visitor.closest
|
||||
}
|
||||
|
||||
struct FindEnclosingNode<'a, 'ast> {
|
||||
range: TextRange,
|
||||
context: &'a PyFormatContext<'ast>,
|
||||
|
||||
/// The, to this point, deepest node that fully encloses `range`.
|
||||
closest: EnclosingNode<'ast>,
|
||||
|
||||
/// Tracks if the current statement is suppressed
|
||||
suppressed: Suppressed,
|
||||
}
|
||||
|
||||
impl<'a, 'ast> FindEnclosingNode<'a, 'ast> {
|
||||
fn new(range: TextRange, context: &'a PyFormatContext<'ast>) -> Self {
|
||||
Self {
|
||||
range,
|
||||
context,
|
||||
suppressed: Suppressed::No,
|
||||
closest: EnclosingNode::Suppressed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ast> PreorderVisitor<'ast> for FindEnclosingNode<'_, 'ast> {
|
||||
fn enter_node(&mut self, node: AnyNodeRef<'ast>) -> TraversalSignal {
|
||||
if !(is_logical_line(node) || node.is_mod_module()) {
|
||||
return TraversalSignal::Skip;
|
||||
}
|
||||
|
||||
// Handle `fmt: off` suppression comments for statements.
|
||||
if node.is_statement() {
|
||||
let leading_comments = self.context.comments().leading(node);
|
||||
self.suppressed = Suppressed::from(match self.suppressed {
|
||||
Suppressed::No => starts_suppression(leading_comments, self.context.source()),
|
||||
Suppressed::Yes => !ends_suppression(leading_comments, self.context.source()),
|
||||
});
|
||||
}
|
||||
|
||||
if !node.range().contains_range(self.range) {
|
||||
return TraversalSignal::Skip;
|
||||
}
|
||||
|
||||
if self.suppressed.is_yes() && node.is_statement() {
|
||||
self.closest = EnclosingNode::Suppressed;
|
||||
return TraversalSignal::Skip;
|
||||
}
|
||||
|
||||
// Don't pick potential docstrings as the closest enclosing node because `suite.rs` than fails to identify them as
|
||||
// docstrings and docstring formatting won't kick in.
|
||||
// Format the enclosing node instead and slice the formatted docstring from the result.
|
||||
let is_maybe_docstring = node
|
||||
.as_stmt_expr()
|
||||
.is_some_and(|stmt| DocstringStmt::is_docstring_statement(stmt));
|
||||
|
||||
if is_maybe_docstring {
|
||||
return TraversalSignal::Skip;
|
||||
}
|
||||
|
||||
// Only computing the count here is sufficient because each enclosing node ensures that it has the necessary indent
|
||||
// or we don't traverse otherwise.
|
||||
let Some(indent_level) =
|
||||
indent_level(node.start(), self.context.source(), self.context.options())
|
||||
else {
|
||||
// Non standard indent or a simple-statement body of a compound statement, format the enclosing node
|
||||
return TraversalSignal::Skip;
|
||||
};
|
||||
|
||||
self.closest = EnclosingNode::Node { node, indent_level };
|
||||
|
||||
TraversalSignal::Traverse
|
||||
}
|
||||
|
||||
fn leave_node(&mut self, node: AnyNodeRef<'ast>) {
|
||||
if node.is_statement() {
|
||||
let trailing_comments = self.context.comments().trailing(node);
|
||||
// Update the suppressed state for the next statement.
|
||||
self.suppressed = Suppressed::from(match self.suppressed {
|
||||
Suppressed::No => starts_suppression(trailing_comments, self.context.source()),
|
||||
Suppressed::Yes => !ends_suppression(trailing_comments, self.context.source()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_body(&mut self, body: &'ast [Stmt]) {
|
||||
// We only visit statements that aren't suppressed that's why we don't need to track the suppression
|
||||
// state in a stack. Assert that this assumption is safe.
|
||||
debug_assert!(self.suppressed.is_no());
|
||||
walk_body(self, body);
|
||||
self.suppressed = Suppressed::No;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum EnclosingNode<'a> {
|
||||
/// The entire range falls into a suppressed `fmt: off` range.
|
||||
Suppressed,
|
||||
|
||||
/// The node outside of a suppression range that fully encloses the searched range.
|
||||
Node {
|
||||
node: AnyNodeRef<'a>,
|
||||
indent_level: u16,
|
||||
},
|
||||
}
|
||||
|
||||
/// Narrows the formatting `range` to a smaller sub-range than the enclosing node's range.
|
||||
///
|
||||
/// The range is narrowed by searching the enclosing node's children and:
|
||||
/// * Find the closest node or comment start or end offset to `range.start`
|
||||
/// * Find the closest node or comment start or end offset, or the clause header's `:` end offset to `range.end`
|
||||
///
|
||||
/// The search is restricted to positions where the formatter emits source map entries because it guarantees
|
||||
/// that we know the exact range in the formatted range and not just an approximation that could include other tokens.
|
||||
///
|
||||
/// ## Clause Headers
|
||||
/// For clause headers like `if`, `while`, `match`, `case` etc. consider the `:` end position for narrowing `range.end`
|
||||
/// to support formatting the clause header without its body.
|
||||
///
|
||||
/// ## Compound statements with simple statement bodies
|
||||
/// Similar to [`find_enclosing_node`], exclude the compound statement's body if it is a simple statement (not a suite) from the search to format the entire clause header
|
||||
/// with the body. This ensures that the formatter runs [`FormatClauseBody`] that determines if the body should be indented.s
|
||||
///
|
||||
/// ## Non-standard indentation
|
||||
/// Node's that use an indentation that doesn't match the configured [`IndentStyle`] and [`IndentWidth`] are excluded from the search.
|
||||
/// This is because the formatter always uses the configured [`IndentStyle`] and [`IndentWidth`], resulting in the
|
||||
/// formatted nodes using a different indentation than the unformatted sibling nodes. This would be tolerable
|
||||
/// in non whitespace sensitive languages like JavaScript but results in lexical errors in Python.
|
||||
///
|
||||
/// ## Implementation
|
||||
/// It would probably be possible to merge this visitor with [`FindEnclosingNode`] but they are separate because
|
||||
/// it avoids some unnecessary work for nodes that aren't the `enclosing_node` and I found reasoning
|
||||
/// and debugging the visiting logic easier when they are separate.
|
||||
///
|
||||
/// [`IndentStyle`]: ruff_formatter::IndentStyle
|
||||
/// [`IndentWidth`]: ruff_formatter::IndentWidth
|
||||
fn narrow_range(
|
||||
range: TextRange,
|
||||
enclosing_node: AnyNodeRef,
|
||||
context: &PyFormatContext,
|
||||
) -> TextRange {
|
||||
let locator = context.locator();
|
||||
let enclosing_indent = indentation_at_offset(enclosing_node.start(), &locator)
|
||||
.expect("Expected enclosing to never be a same line body statement.");
|
||||
|
||||
let mut visitor = NarrowRange {
|
||||
context,
|
||||
range,
|
||||
|
||||
narrowed_start: enclosing_node.start(),
|
||||
narrowed_end: enclosing_node.end(),
|
||||
|
||||
enclosing_indent,
|
||||
level: usize::from(!enclosing_node.is_mod_module()),
|
||||
};
|
||||
|
||||
if visitor.enter_node(enclosing_node).is_traverse() {
|
||||
enclosing_node.visit_preorder(&mut visitor);
|
||||
}
|
||||
|
||||
visitor.leave_node(enclosing_node);
|
||||
|
||||
TextRange::new(visitor.narrowed_start, visitor.narrowed_end)
|
||||
}
|
||||
|
||||
struct NarrowRange<'a> {
|
||||
context: &'a PyFormatContext<'a>,
|
||||
|
||||
// The range to format
|
||||
range: TextRange,
|
||||
|
||||
// The narrowed range
|
||||
narrowed_start: TextSize,
|
||||
narrowed_end: TextSize,
|
||||
|
||||
// Stated tracked by the visitor
|
||||
enclosing_indent: &'a str,
|
||||
level: usize,
|
||||
}
|
||||
|
||||
impl PreorderVisitor<'_> for NarrowRange<'_> {
|
||||
fn enter_node(&mut self, node: AnyNodeRef<'_>) -> TraversalSignal {
|
||||
if !(is_logical_line(node) || node.is_mod_module()) {
|
||||
return TraversalSignal::Skip;
|
||||
}
|
||||
|
||||
// Find the start offset of the node that starts the closest to (and before) the start offset of the formatting range.
|
||||
// We do this by iterating over known positions that emit source map entries and pick the start point that ends closest
|
||||
// to the searched range's start.
|
||||
let leading_comments = self.context.comments().leading(node);
|
||||
self.narrow(leading_comments);
|
||||
self.narrow([node]);
|
||||
|
||||
// Avoid traversing when it's known to not be able to narrow the range further to avoid traversing the entire tree (entire file in the worst case).
|
||||
// If the node's range is entirely before the searched range, don't traverse because non of its children
|
||||
// can be closer to `narrow_start` than the node itself (which we already narrowed).
|
||||
//
|
||||
// Don't traverse if the current node is passed the narrowed range (it's impossible to refine it further).
|
||||
if node.end() < self.range.start()
|
||||
|| (self.narrowed_start > node.start() && self.narrowed_end <= node.end())
|
||||
{
|
||||
return TraversalSignal::Skip;
|
||||
}
|
||||
|
||||
// Handle nodes that have indented child-nodes that aren't a `Body` (which is handled by `visit_body`).
|
||||
// Ideally, this would be handled as part of `visit_stmt` but `visit_stmt` doesn't get called for the `enclosing_node`
|
||||
// because it's not possible to convert` AnyNodeRef` to `&Stmt` :(
|
||||
match node {
|
||||
AnyNodeRef::StmtMatch(StmtMatch {
|
||||
subject: _,
|
||||
cases,
|
||||
range: _,
|
||||
}) => {
|
||||
if let Some(saved_state) = self.enter_level(cases.first().map(AnyNodeRef::from)) {
|
||||
for match_case in cases {
|
||||
self.visit_match_case(match_case);
|
||||
}
|
||||
self.leave_level(saved_state);
|
||||
}
|
||||
|
||||
// Already traversed as part of `enter_node`.
|
||||
TraversalSignal::Skip
|
||||
}
|
||||
AnyNodeRef::StmtTry(StmtTry {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
is_star: _,
|
||||
range: _,
|
||||
}) => {
|
||||
self.visit_body(body);
|
||||
if let Some(except_handler_saved) =
|
||||
self.enter_level(handlers.first().map(AnyNodeRef::from))
|
||||
{
|
||||
for except_handler in handlers {
|
||||
self.visit_except_handler(except_handler);
|
||||
}
|
||||
self.leave_level(except_handler_saved);
|
||||
}
|
||||
self.visit_body(orelse);
|
||||
self.visit_body(finalbody);
|
||||
|
||||
// Already traversed as part of `enter_node`.
|
||||
TraversalSignal::Skip
|
||||
}
|
||||
_ => TraversalSignal::Traverse,
|
||||
}
|
||||
}
|
||||
|
||||
fn leave_node(&mut self, node: AnyNodeRef<'_>) {
|
||||
if !(is_logical_line(node) || node.is_mod_module()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the end offset of the closest node to the end offset of the formatting range.
|
||||
// We do this by iterating over end positions that we know generate source map entries end pick the end
|
||||
// that ends closest or after the searched range's end.
|
||||
let trailing_comments = self.context.comments().trailing(node);
|
||||
self.narrow(trailing_comments);
|
||||
}
|
||||
|
||||
fn visit_body(&mut self, body: &'_ [Stmt]) {
|
||||
if let Some(saved_state) = self.enter_level(body.first().map(AnyNodeRef::from)) {
|
||||
walk_body(self, body);
|
||||
self.leave_level(saved_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NarrowRange<'_> {
|
||||
fn narrow<I, T>(&mut self, items: I)
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Ranged,
|
||||
{
|
||||
for ranged in items {
|
||||
self.narrow_offset(ranged.start());
|
||||
self.narrow_offset(ranged.end());
|
||||
}
|
||||
}
|
||||
|
||||
fn narrow_offset(&mut self, offset: TextSize) {
|
||||
self.narrow_start(offset);
|
||||
self.narrow_end(offset);
|
||||
}
|
||||
|
||||
fn narrow_start(&mut self, offset: TextSize) {
|
||||
if offset <= self.range.start() {
|
||||
self.narrowed_start = self.narrowed_start.max(offset);
|
||||
}
|
||||
}
|
||||
|
||||
fn narrow_end(&mut self, offset: TextSize) {
|
||||
if offset >= self.range.end() {
|
||||
self.narrowed_end = self.narrowed_end.min(offset);
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_level(&mut self, first_child: Option<AnyNodeRef>) -> Option<SavedLevel> {
|
||||
if let Some(first_child) = first_child {
|
||||
// If this is a clause header and the `range` ends within the clause header, then avoid formatting the body.
|
||||
// This prevents that we format an entire function definition when the selected range is fully enclosed by the parameters.
|
||||
// ```python
|
||||
// 1| def foo(<RANGE_START>a, b, c<RANGE_END>):
|
||||
// 2| pass
|
||||
// ```
|
||||
// We don't want to format the body of the function.
|
||||
if let Some(SimpleToken {
|
||||
kind: SimpleTokenKind::Colon,
|
||||
range: colon_range,
|
||||
}) = BackwardsTokenizer::up_to(
|
||||
first_child.start(),
|
||||
self.context.source(),
|
||||
self.context.comments().ranges(),
|
||||
)
|
||||
.skip_trivia()
|
||||
.next()
|
||||
{
|
||||
self.narrow_offset(colon_range.end());
|
||||
}
|
||||
|
||||
// It is necessary to format all statements if the statement or any of its parents don't use the configured indentation.
|
||||
// ```python
|
||||
// 0| def foo():
|
||||
// 1| if True:
|
||||
// 2| print("Hello")
|
||||
// 3| print("More")
|
||||
// 4| a = 10
|
||||
// ```
|
||||
// Here, the `if` statement uses the correct 4 spaces indentation, but the two `print` statements use a 2 spaces indentation.
|
||||
// The formatter output uses 8 space indentation for the `print` statement which doesn't match the indentation of the statement on line 4 when
|
||||
// replacing the source with the formatted code. That's why we expand the range in this case to cover the entire if-body range.
|
||||
//
|
||||
// I explored the alternative of using `indent(dedent(formatted))` to retain the correct indentation. It works pretty well except that it can change the
|
||||
// content of multiline strings:
|
||||
// ```python
|
||||
// def test ():
|
||||
// pass
|
||||
// <RANGE_START>1 + 2
|
||||
// """A Multiline string
|
||||
// that uses the same indentation as the formatted code will. This should not be dedented."""
|
||||
//
|
||||
// print("Done")<RANGE_END>
|
||||
// ```
|
||||
// The challenge here is that the second line of the multiline string uses a 4 space indentation. Using `dedent` would
|
||||
// dedent the second line to 0 spaces and the `indent` then adds a 2 space indentation to match the indentation in the source.
|
||||
// This is incorrect because the leading whitespace is the content of the string and not indentation, resulting in changed string content.
|
||||
if let Some(indentation) =
|
||||
indentation_at_offset(first_child.start(), &self.context.locator())
|
||||
{
|
||||
let relative_indent = indentation.strip_prefix(self.enclosing_indent).unwrap();
|
||||
let expected_indents = self.level;
|
||||
|
||||
// Each level must always add one level of indent. That's why an empty relative indent to the parent node tells us that the enclosing node is the Module.
|
||||
let has_expected_indentation = match self.context.options().indent_style() {
|
||||
IndentStyle::Tab => {
|
||||
relative_indent.len() == expected_indents
|
||||
&& relative_indent.chars().all(|c| c == '\t')
|
||||
}
|
||||
IndentStyle::Space => {
|
||||
relative_indent.len()
|
||||
== expected_indents
|
||||
* self.context.options().indent_width().value() as usize
|
||||
&& relative_indent.chars().all(|c| c == ' ')
|
||||
}
|
||||
};
|
||||
|
||||
if !has_expected_indentation {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
// Simple-statement body of a compound statement (not a suite body).
|
||||
// Don't narrow the range because the formatter must run `FormatClauseBody` to determine if the body should be collapsed or not.
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let saved_level = self.level;
|
||||
self.level += 1;
|
||||
|
||||
Some(SavedLevel { level: saved_level })
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn leave_level(&mut self, saved_state: SavedLevel) {
|
||||
self.level = saved_state.level;
|
||||
}
|
||||
}
|
||||
|
||||
const fn is_logical_line(node: AnyNodeRef) -> bool {
|
||||
// Make sure to update [`FormatEnclosingLine`] when changing this.
|
||||
node.is_statement()
|
||||
|| node.is_decorator()
|
||||
|| node.is_except_handler()
|
||||
|| node.is_elif_else_clause()
|
||||
|| node.is_match_case()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SavedLevel {
|
||||
level: usize,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug)]
|
||||
enum Suppressed {
|
||||
/// Code is not suppressed
|
||||
#[default]
|
||||
No,
|
||||
|
||||
/// The node is suppressed by a suppression comment in the same body block.
|
||||
Yes,
|
||||
}
|
||||
|
||||
impl Suppressed {
|
||||
const fn is_no(self) -> bool {
|
||||
matches!(self, Suppressed::No)
|
||||
}
|
||||
|
||||
const fn is_yes(self) -> bool {
|
||||
matches!(self, Suppressed::Yes)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Suppressed {
|
||||
fn from(value: bool) -> Self {
|
||||
if value {
|
||||
Suppressed::Yes
|
||||
} else {
|
||||
Suppressed::No
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_valid_char_boundaries(range: TextRange, source: &str) {
|
||||
assert!(source.is_char_boundary(usize::from(range.start())));
|
||||
assert!(source.is_char_boundary(usize::from(range.end())));
|
||||
}
|
||||
|
||||
struct FormatEnclosingNode<'a> {
|
||||
root: AnyNodeRef<'a>,
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for FormatEnclosingNode<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
|
||||
// Note: It's important that this supports formatting all nodes for which `is_logical_line`
|
||||
// returns + the root `Mod` nodes.
|
||||
match self.root {
|
||||
AnyNodeRef::ModModule(node) => node.format().fmt(f),
|
||||
AnyNodeRef::ModExpression(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtFunctionDef(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtClassDef(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtReturn(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtDelete(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtTypeAlias(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtAssign(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtAugAssign(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtAnnAssign(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtFor(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtWhile(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtIf(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtWith(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtMatch(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtRaise(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtTry(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtAssert(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtImport(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtImportFrom(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtGlobal(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtNonlocal(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtExpr(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtPass(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtBreak(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtContinue(node) => node.format().fmt(f),
|
||||
AnyNodeRef::StmtIpyEscapeCommand(node) => node.format().fmt(f),
|
||||
AnyNodeRef::ExceptHandlerExceptHandler(node) => node.format().fmt(f),
|
||||
AnyNodeRef::MatchCase(node) => node.format().fmt(f),
|
||||
AnyNodeRef::Decorator(node) => node.format().fmt(f),
|
||||
AnyNodeRef::ElifElseClause(node) => node.format().fmt(f),
|
||||
|
||||
AnyNodeRef::ExprBoolOp(_)
|
||||
| AnyNodeRef::ExprNamedExpr(_)
|
||||
| AnyNodeRef::ExprBinOp(_)
|
||||
| AnyNodeRef::ExprUnaryOp(_)
|
||||
| AnyNodeRef::ExprLambda(_)
|
||||
| AnyNodeRef::ExprIfExp(_)
|
||||
| AnyNodeRef::ExprDict(_)
|
||||
| AnyNodeRef::ExprSet(_)
|
||||
| AnyNodeRef::ExprListComp(_)
|
||||
| AnyNodeRef::ExprSetComp(_)
|
||||
| AnyNodeRef::ExprDictComp(_)
|
||||
| AnyNodeRef::ExprGeneratorExp(_)
|
||||
| AnyNodeRef::ExprAwait(_)
|
||||
| AnyNodeRef::ExprYield(_)
|
||||
| AnyNodeRef::ExprYieldFrom(_)
|
||||
| AnyNodeRef::ExprCompare(_)
|
||||
| AnyNodeRef::ExprCall(_)
|
||||
| AnyNodeRef::FStringExpressionElement(_)
|
||||
| AnyNodeRef::FStringLiteralElement(_)
|
||||
| AnyNodeRef::ExprFString(_)
|
||||
| AnyNodeRef::ExprStringLiteral(_)
|
||||
| AnyNodeRef::ExprBytesLiteral(_)
|
||||
| AnyNodeRef::ExprNumberLiteral(_)
|
||||
| AnyNodeRef::ExprBooleanLiteral(_)
|
||||
| AnyNodeRef::ExprNoneLiteral(_)
|
||||
| AnyNodeRef::ExprEllipsisLiteral(_)
|
||||
| AnyNodeRef::ExprAttribute(_)
|
||||
| AnyNodeRef::ExprSubscript(_)
|
||||
| AnyNodeRef::ExprStarred(_)
|
||||
| AnyNodeRef::ExprName(_)
|
||||
| AnyNodeRef::ExprList(_)
|
||||
| AnyNodeRef::ExprTuple(_)
|
||||
| AnyNodeRef::ExprSlice(_)
|
||||
| AnyNodeRef::ExprIpyEscapeCommand(_)
|
||||
| AnyNodeRef::FString(_)
|
||||
| AnyNodeRef::StringLiteral(_)
|
||||
| AnyNodeRef::PatternMatchValue(_)
|
||||
| AnyNodeRef::PatternMatchSingleton(_)
|
||||
| AnyNodeRef::PatternMatchSequence(_)
|
||||
| AnyNodeRef::PatternMatchMapping(_)
|
||||
| AnyNodeRef::PatternMatchClass(_)
|
||||
| AnyNodeRef::PatternMatchStar(_)
|
||||
| AnyNodeRef::PatternMatchAs(_)
|
||||
| AnyNodeRef::PatternMatchOr(_)
|
||||
| AnyNodeRef::PatternArguments(_)
|
||||
| AnyNodeRef::PatternKeyword(_)
|
||||
| AnyNodeRef::Comprehension(_)
|
||||
| AnyNodeRef::Arguments(_)
|
||||
| AnyNodeRef::Parameters(_)
|
||||
| AnyNodeRef::Parameter(_)
|
||||
| AnyNodeRef::ParameterWithDefault(_)
|
||||
| AnyNodeRef::Keyword(_)
|
||||
| AnyNodeRef::Alias(_)
|
||||
| AnyNodeRef::WithItem(_)
|
||||
| AnyNodeRef::TypeParams(_)
|
||||
| AnyNodeRef::TypeParamTypeVar(_)
|
||||
| AnyNodeRef::TypeParamTypeVarTuple(_)
|
||||
| AnyNodeRef::TypeParamParamSpec(_)
|
||||
| AnyNodeRef::BytesLiteral(_) => {
|
||||
panic!("Range formatting only supports formatting logical lines")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the level of indentation for `indentation` when using the configured [`IndentStyle`] and [`IndentWidth`].
|
||||
///
|
||||
/// Returns `None` if the indentation doesn't conform to the configured [`IndentStyle`] and [`IndentWidth`].
|
||||
///
|
||||
/// # Panics
|
||||
/// If `offset` is outside of `source`.
|
||||
fn indent_level(offset: TextSize, source: &str, options: &PyFormatOptions) -> Option<u16> {
|
||||
let locator = Locator::new(source);
|
||||
let indentation = indentation_at_offset(offset, &locator)?;
|
||||
|
||||
let level = match options.indent_style() {
|
||||
IndentStyle::Tab => {
|
||||
if indentation.chars().all(|c| c == '\t') {
|
||||
Some(indentation.len())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
IndentStyle::Space => {
|
||||
let indent_width = options.indent_width().value() as usize;
|
||||
if indentation.chars().all(|c| c == ' ') && indentation.len() % indent_width == 0 {
|
||||
Some(indentation.len() / indent_width)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
level.map(|level| u16::try_from(level).unwrap_or(u16::MAX))
|
||||
}
|
|
@ -357,8 +357,20 @@ impl<'ast> Format<PyFormatContext<'ast>> for FormatClauseHeader<'_, 'ast> {
|
|||
if SuppressionKind::has_skip_comment(self.trailing_colon_comment, f.context().source()) {
|
||||
write_suppressed_clause_header(self.header, f)?;
|
||||
} else {
|
||||
f.write_fmt(Arguments::from(&self.formatter))?;
|
||||
token(":").fmt(f)?;
|
||||
// Write a source map entry for the colon for range formatting to support formatting the clause header without
|
||||
// the clause body. Avoid computing `self.header.range()` otherwise because it's somewhat involved.
|
||||
let clause_end = if f.options().source_map_generation().is_enabled() {
|
||||
Some(source_position(
|
||||
self.header.range(f.context().source())?.end(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
write!(
|
||||
f,
|
||||
[Arguments::from(&self.formatter), token(":"), clause_end]
|
||||
)?;
|
||||
}
|
||||
|
||||
trailing_comments(self.trailing_colon_comment).fmt(f)
|
||||
|
@ -458,7 +470,12 @@ fn find_keyword(
|
|||
fn colon_range(after_keyword_or_condition: TextSize, source: &str) -> FormatResult<TextRange> {
|
||||
let mut tokenizer = SimpleTokenizer::starts_at(after_keyword_or_condition, source)
|
||||
.skip_trivia()
|
||||
.skip_while(|token| token.kind() == SimpleTokenKind::RParen);
|
||||
.skip_while(|token| {
|
||||
matches!(
|
||||
token.kind(),
|
||||
SimpleTokenKind::RParen | SimpleTokenKind::Comma
|
||||
)
|
||||
});
|
||||
|
||||
match tokenizer.next() {
|
||||
Some(SimpleToken {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use ruff_formatter::{format_args, write};
|
||||
use ruff_python_ast::AnyNodeRef;
|
||||
use ruff_python_ast::{ElifElseClause, StmtIf};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::expression::maybe_parenthesize_expression;
|
||||
|
@ -81,7 +82,12 @@ pub(crate) fn format_elif_else_clause(
|
|||
clause_header(
|
||||
ClauseHeader::ElifElse(item),
|
||||
trailing_colon_comment,
|
||||
&format_with(|f| {
|
||||
&format_with(|f: &mut PyFormatter| {
|
||||
f.options()
|
||||
.source_map_generation()
|
||||
.is_enabled()
|
||||
.then_some(source_position(item.start()))
|
||||
.fmt(f)?;
|
||||
if let Some(test) = test {
|
||||
write!(
|
||||
f,
|
||||
|
@ -98,6 +104,10 @@ pub(crate) fn format_elif_else_clause(
|
|||
)
|
||||
.with_leading_comments(leading_comments, last_node),
|
||||
clause_body(body, trailing_colon_comment),
|
||||
f.options()
|
||||
.source_map_generation()
|
||||
.is_enabled()
|
||||
.then_some(source_position(item.end()))
|
||||
]
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ use ruff_formatter::{
|
|||
write, FormatContext, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions,
|
||||
};
|
||||
use ruff_python_ast::helpers::is_compound_statement;
|
||||
use ruff_python_ast::AnyNodeRef;
|
||||
use ruff_python_ast::{self as ast, Expr, PySourceType, Stmt, Suite};
|
||||
use ruff_python_ast::{AnyNodeRef, StmtExpr};
|
||||
use ruff_python_trivia::{lines_after, lines_after_ignoring_end_of_line_trivia, lines_before};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
|
@ -740,6 +740,14 @@ impl<'a> DocstringStmt<'a> {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_docstring_statement(stmt: &StmtExpr) -> bool {
|
||||
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = stmt.value.as_ref() {
|
||||
!value.is_implicit_concatenated()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for DocstringStmt<'_> {
|
||||
|
|
|
@ -19,6 +19,40 @@ use crate::statement::clause::ClauseHeader;
|
|||
use crate::statement::suite::SuiteChildStatement;
|
||||
use crate::statement::trailing_semicolon;
|
||||
|
||||
/// Returns `true` if the statements coming after `leading_or_trailing_comments` are suppressed.
|
||||
///
|
||||
/// The result is only correct if called for statement comments in a non-suppressed range.
|
||||
///
|
||||
/// # Panics
|
||||
/// If `leading_or_trailing_comments` contain any range that's outside of `source`.
|
||||
pub(crate) fn starts_suppression(
|
||||
leading_or_trailing_comments: &[SourceComment],
|
||||
source: &str,
|
||||
) -> bool {
|
||||
let mut iter = CommentRangeIter::outside_suppression(leading_or_trailing_comments, source);
|
||||
// Move the iter to the last element.
|
||||
let _ = iter.by_ref().last();
|
||||
|
||||
matches!(iter.in_suppression, InSuppression::Yes)
|
||||
}
|
||||
|
||||
/// Returns `true` if the statements coming after `leading_or_trailing_comments` are no longer suppressed.
|
||||
///
|
||||
/// The result is only correct if called for statement comments in a suppressed range.
|
||||
///
|
||||
/// # Panics
|
||||
/// If `leading_or_trailing_comments` contain any range that's outside of `source`.
|
||||
pub(crate) fn ends_suppression(
|
||||
leading_or_trailing_comments: &[SourceComment],
|
||||
source: &str,
|
||||
) -> bool {
|
||||
let mut iter = CommentRangeIter::in_suppression(leading_or_trailing_comments, source);
|
||||
// Move the iter to the last element.
|
||||
let _ = iter.by_ref().last();
|
||||
|
||||
!matches!(iter.in_suppression, InSuppression::Yes)
|
||||
}
|
||||
|
||||
/// Disables formatting for all statements between the `first_suppressed` that has a leading `fmt: off` comment
|
||||
/// and the first trailing or leading `fmt: on` comment. The statements are formatted as they appear in the source code.
|
||||
///
|
||||
|
@ -855,7 +889,7 @@ impl Format<PyFormatContext<'_>> for VerbatimText {
|
|||
|
||||
match normalize_newlines(f.context().locator().slice(self.verbatim_range), ['\r']) {
|
||||
Cow::Borrowed(_) => {
|
||||
write!(f, [source_text_slice(self.verbatim_range,)])?;
|
||||
write!(f, [source_text_slice(self.verbatim_range)])?;
|
||||
}
|
||||
Cow::Owned(cleaned) => {
|
||||
write!(
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use std::borrow::Cow;
|
||||
use std::fmt::{Formatter, Write};
|
||||
use std::io::BufReader;
|
||||
use std::ops::Range;
|
||||
use std::path::Path;
|
||||
use std::{fmt, fs};
|
||||
|
||||
|
@ -8,8 +10,10 @@ use similar::TextDiff;
|
|||
use crate::normalizer::Normalizer;
|
||||
use ruff_formatter::FormatOptions;
|
||||
use ruff_python_ast::comparable::ComparableMod;
|
||||
use ruff_python_formatter::{format_module_source, PreviewMode, PyFormatOptions};
|
||||
use ruff_python_formatter::{format_module_source, format_range, PreviewMode, PyFormatOptions};
|
||||
use ruff_python_parser::{parse, AsMode};
|
||||
use ruff_source_file::{LineIndex, OneIndexed};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
mod normalizer;
|
||||
|
||||
|
@ -28,6 +32,52 @@ fn black_compatibility() {
|
|||
PyFormatOptions::from_extension(input_path)
|
||||
};
|
||||
|
||||
let first_line = content.lines().next().unwrap_or_default();
|
||||
let formatted_code = if first_line.starts_with("# flags:")
|
||||
&& first_line.contains("--line-ranges=")
|
||||
{
|
||||
let line_index = LineIndex::from_source_text(&content);
|
||||
|
||||
let ranges = first_line
|
||||
.split_ascii_whitespace()
|
||||
.filter_map(|chunk| {
|
||||
let (_, lines) = chunk.split_once("--line-ranges=")?;
|
||||
let (lower, upper) = lines.split_once('-')?;
|
||||
|
||||
let lower = lower
|
||||
.parse::<OneIndexed>()
|
||||
.expect("Expected a valid line number");
|
||||
let upper = upper
|
||||
.parse::<OneIndexed>()
|
||||
.expect("Expected a valid line number");
|
||||
|
||||
let range_start = line_index.line_start(lower, &content);
|
||||
let range_end = line_index.line_end(upper, &content);
|
||||
|
||||
Some(TextRange::new(range_start, range_end))
|
||||
})
|
||||
.rev();
|
||||
|
||||
let mut formatted_code = content.clone();
|
||||
|
||||
for range in ranges {
|
||||
let formatted =
|
||||
format_range(&content, range, options.clone()).unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"Range-formatting of {} to succeed but encountered error {err}",
|
||||
input_path.display()
|
||||
)
|
||||
});
|
||||
|
||||
let range = formatted.source_range();
|
||||
|
||||
formatted_code.replace_range(Range::<usize>::from(range), formatted.as_code());
|
||||
}
|
||||
|
||||
// We can't do stability checks for range formatting because we don't know the updated rangs.
|
||||
|
||||
formatted_code
|
||||
} else {
|
||||
let printed = format_module_source(&content, options.clone()).unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"Formatting of {} to succeed but encountered error {err}",
|
||||
|
@ -35,6 +85,13 @@ fn black_compatibility() {
|
|||
)
|
||||
});
|
||||
|
||||
let formatted_code = printed.into_code();
|
||||
|
||||
ensure_stability_when_formatting_twice(&formatted_code, &options, input_path);
|
||||
|
||||
formatted_code
|
||||
};
|
||||
|
||||
let extension = input_path
|
||||
.extension()
|
||||
.expect("Test file to have py or pyi extension")
|
||||
|
@ -43,10 +100,7 @@ fn black_compatibility() {
|
|||
let expected_output = fs::read_to_string(&expected_path)
|
||||
.unwrap_or_else(|_| panic!("Expected Black output file '{expected_path:?}' to exist"));
|
||||
|
||||
let formatted_code = printed.as_code();
|
||||
|
||||
ensure_unchanged_ast(&content, formatted_code, &options, input_path);
|
||||
ensure_stability_when_formatting_twice(formatted_code, &options, input_path);
|
||||
ensure_unchanged_ast(&content, &formatted_code, &options, input_path);
|
||||
|
||||
if formatted_code == expected_output {
|
||||
// Black and Ruff formatting matches. Delete any existing snapshot files because the Black output
|
||||
|
@ -88,7 +142,7 @@ fn black_compatibility() {
|
|||
|
||||
write!(snapshot, "{}", Header::new("Black Differences")).unwrap();
|
||||
|
||||
let diff = TextDiff::from_lines(expected_output.as_str(), formatted_code)
|
||||
let diff = TextDiff::from_lines(expected_output.as_str(), &formatted_code)
|
||||
.unified_diff()
|
||||
.header("Black", "Ruff")
|
||||
.to_string();
|
||||
|
@ -124,12 +178,7 @@ fn format() {
|
|||
let content = fs::read_to_string(input_path).unwrap();
|
||||
|
||||
let options = PyFormatOptions::from_extension(input_path);
|
||||
let printed =
|
||||
format_module_source(&content, options.clone()).expect("Formatting to succeed");
|
||||
let formatted_code = printed.as_code();
|
||||
|
||||
ensure_unchanged_ast(&content, formatted_code, &options, input_path);
|
||||
ensure_stability_when_formatting_twice(formatted_code, &options, input_path);
|
||||
let formatted_code = format_file(&content, &options, input_path);
|
||||
|
||||
let mut snapshot = format!("## Input\n{}", CodeFrame::new("python", &content));
|
||||
|
||||
|
@ -142,12 +191,7 @@ fn format() {
|
|||
writeln!(snapshot, "## Outputs").unwrap();
|
||||
|
||||
for (i, options) in options.into_iter().enumerate() {
|
||||
let printed =
|
||||
format_module_source(&content, options.clone()).expect("Formatting to succeed");
|
||||
let formatted_code = printed.as_code();
|
||||
|
||||
ensure_unchanged_ast(&content, formatted_code, &options, input_path);
|
||||
ensure_stability_when_formatting_twice(formatted_code, &options, input_path);
|
||||
let formatted_code = format_file(&content, &options, input_path);
|
||||
|
||||
writeln!(
|
||||
snapshot,
|
||||
|
@ -164,16 +208,7 @@ fn format() {
|
|||
|
||||
// We want to capture the differences in the preview style in our fixtures
|
||||
let options_preview = options.with_preview(PreviewMode::Enabled);
|
||||
let printed_preview = format_module_source(&content, options_preview.clone())
|
||||
.expect("Formatting to succeed");
|
||||
let formatted_preview = printed_preview.as_code();
|
||||
|
||||
ensure_unchanged_ast(&content, formatted_preview, &options_preview, input_path);
|
||||
ensure_stability_when_formatting_twice(
|
||||
formatted_preview,
|
||||
&options_preview,
|
||||
input_path,
|
||||
);
|
||||
let formatted_preview = format_file(&content, &options_preview, input_path);
|
||||
|
||||
if formatted_code != formatted_preview {
|
||||
// Having both snapshots makes it hard to see the difference, so we're keeping only
|
||||
|
@ -183,7 +218,7 @@ fn format() {
|
|||
"#### Preview changes\n{}",
|
||||
CodeFrame::new(
|
||||
"diff",
|
||||
TextDiff::from_lines(formatted_code, formatted_preview)
|
||||
TextDiff::from_lines(&formatted_code, &formatted_preview)
|
||||
.unified_diff()
|
||||
.header("Stable", "Preview")
|
||||
)
|
||||
|
@ -194,12 +229,7 @@ fn format() {
|
|||
} else {
|
||||
// We want to capture the differences in the preview style in our fixtures
|
||||
let options_preview = options.with_preview(PreviewMode::Enabled);
|
||||
let printed_preview = format_module_source(&content, options_preview.clone())
|
||||
.expect("Formatting to succeed");
|
||||
let formatted_preview = printed_preview.as_code();
|
||||
|
||||
ensure_unchanged_ast(&content, formatted_preview, &options_preview, input_path);
|
||||
ensure_stability_when_formatting_twice(formatted_preview, &options_preview, input_path);
|
||||
let formatted_preview = format_file(&content, &options_preview, input_path);
|
||||
|
||||
if formatted_code == formatted_preview {
|
||||
writeln!(
|
||||
|
@ -217,7 +247,7 @@ fn format() {
|
|||
CodeFrame::new("python", &formatted_code),
|
||||
CodeFrame::new(
|
||||
"diff",
|
||||
TextDiff::from_lines(formatted_code, formatted_preview)
|
||||
TextDiff::from_lines(&formatted_code, &formatted_preview)
|
||||
.unified_diff()
|
||||
.header("Stable", "Preview")
|
||||
)
|
||||
|
@ -242,6 +272,66 @@ fn format() {
|
|||
);
|
||||
}
|
||||
|
||||
fn format_file(source: &str, options: &PyFormatOptions, input_path: &Path) -> String {
|
||||
let (unformatted, formatted_code) = if source.contains("<RANGE_START>") {
|
||||
let mut content = source.to_string();
|
||||
let without_markers = content
|
||||
.replace("<RANGE_START>", "")
|
||||
.replace("<RANGE_END>", "");
|
||||
|
||||
while let Some(range_start_marker) = content.find("<RANGE_START>") {
|
||||
// Remove the start marker
|
||||
content.replace_range(
|
||||
range_start_marker..range_start_marker + "<RANGE_START>".len(),
|
||||
"",
|
||||
);
|
||||
|
||||
let range_end_marker = content[range_start_marker..]
|
||||
.find("<RANGE_END>")
|
||||
.expect("Matching <RANGE_END> marker for <RANGE_START> to exist")
|
||||
+ range_start_marker;
|
||||
|
||||
content.replace_range(range_end_marker..range_end_marker + "<RANGE_END>".len(), "");
|
||||
|
||||
// Replace all other markers to get a valid Python input
|
||||
let format_input = content
|
||||
.replace("<RANGE_START>", "")
|
||||
.replace("<RANGE_END>", "");
|
||||
|
||||
let range = TextRange::new(
|
||||
TextSize::try_from(range_start_marker).unwrap(),
|
||||
TextSize::try_from(range_end_marker).unwrap(),
|
||||
);
|
||||
|
||||
let formatted =
|
||||
format_range(&format_input, range, options.clone()).unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"Range-formatting of {} to succeed but encountered error {err}",
|
||||
input_path.display()
|
||||
)
|
||||
});
|
||||
|
||||
content.replace_range(
|
||||
Range::<usize>::from(formatted.source_range()),
|
||||
formatted.as_code(),
|
||||
);
|
||||
}
|
||||
|
||||
(Cow::Owned(without_markers), content)
|
||||
} else {
|
||||
let printed = format_module_source(source, options.clone()).expect("Formatting to succeed");
|
||||
let formatted_code = printed.into_code();
|
||||
|
||||
ensure_stability_when_formatting_twice(&formatted_code, options, input_path);
|
||||
|
||||
(Cow::Borrowed(source), formatted_code)
|
||||
};
|
||||
|
||||
ensure_unchanged_ast(&unformatted, &formatted_code, options, input_path);
|
||||
|
||||
formatted_code
|
||||
}
|
||||
|
||||
/// Format another time and make sure that there are no changes anymore
|
||||
fn ensure_stability_when_formatting_twice(
|
||||
formatted_code: &str,
|
||||
|
|
|
@ -1,319 +0,0 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/line_ranges_basic.py
|
||||
---
|
||||
## Input
|
||||
|
||||
```python
|
||||
# flags: --line-ranges=5-6
|
||||
# NOTE: If you need to modify this file, pay special attention to the --line-ranges=
|
||||
# flag above as it's formatting specifically these lines.
|
||||
def foo1(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
|
||||
def foo2(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
|
||||
def foo3(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
|
||||
def foo4(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
|
||||
|
||||
# Adding some unformated code covering a wide range of syntaxes.
|
||||
|
||||
if True:
|
||||
# Incorrectly indented prefix comments.
|
||||
pass
|
||||
|
||||
import typing
|
||||
from typing import (
|
||||
Any ,
|
||||
)
|
||||
class MyClass( object): # Trailing comment with extra leading space.
|
||||
#NOTE: The following indentation is incorrect:
|
||||
@decor( 1 * 3 )
|
||||
def my_func( arg):
|
||||
pass
|
||||
|
||||
try: # Trailing comment with extra leading space.
|
||||
for i in range(10): # Trailing comment with extra leading space.
|
||||
while condition:
|
||||
if something:
|
||||
then_something( )
|
||||
elif something_else:
|
||||
then_something_else( )
|
||||
except ValueError as e:
|
||||
unformatted( )
|
||||
finally:
|
||||
unformatted( )
|
||||
|
||||
async def test_async_unformatted( ): # Trailing comment with extra leading space.
|
||||
async for i in some_iter( unformatted ): # Trailing comment with extra leading space.
|
||||
await asyncio.sleep( 1 )
|
||||
async with some_context( unformatted ):
|
||||
print( "unformatted" )
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -1,7 +1,18 @@
|
||||
# flags: --line-ranges=5-6
|
||||
# NOTE: If you need to modify this file, pay special attention to the --line-ranges=
|
||||
# flag above as it's formatting specifically these lines.
|
||||
-def foo1(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
|
||||
+def foo1(
|
||||
+ parameter_1,
|
||||
+ parameter_2,
|
||||
+ parameter_3,
|
||||
+ parameter_4,
|
||||
+ parameter_5,
|
||||
+ parameter_6,
|
||||
+ parameter_7,
|
||||
+):
|
||||
+ pass
|
||||
+
|
||||
+
|
||||
def foo2(
|
||||
parameter_1,
|
||||
parameter_2,
|
||||
@@ -26,38 +37,52 @@
|
||||
pass
|
||||
|
||||
|
||||
-def foo4(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
|
||||
+def foo4(
|
||||
+ parameter_1,
|
||||
+ parameter_2,
|
||||
+ parameter_3,
|
||||
+ parameter_4,
|
||||
+ parameter_5,
|
||||
+ parameter_6,
|
||||
+ parameter_7,
|
||||
+):
|
||||
+ pass
|
||||
+
|
||||
|
||||
# Adding some unformated code covering a wide range of syntaxes.
|
||||
|
||||
if True:
|
||||
- # Incorrectly indented prefix comments.
|
||||
- pass
|
||||
+ # Incorrectly indented prefix comments.
|
||||
+ pass
|
||||
|
||||
-import typing
|
||||
-from typing import (
|
||||
- Any ,
|
||||
- )
|
||||
-class MyClass( object): # Trailing comment with extra leading space.
|
||||
- #NOTE: The following indentation is incorrect:
|
||||
- @decor( 1 * 3 )
|
||||
- def my_func( arg):
|
||||
- pass
|
||||
+import typing
|
||||
+from typing import (
|
||||
+ Any,
|
||||
+)
|
||||
+
|
||||
+
|
||||
+class MyClass(object): # Trailing comment with extra leading space.
|
||||
+ # NOTE: The following indentation is incorrect:
|
||||
+ @decor(1 * 3)
|
||||
+ def my_func(arg):
|
||||
+ pass
|
||||
|
||||
-try: # Trailing comment with extra leading space.
|
||||
- for i in range(10): # Trailing comment with extra leading space.
|
||||
- while condition:
|
||||
- if something:
|
||||
- then_something( )
|
||||
- elif something_else:
|
||||
- then_something_else( )
|
||||
-except ValueError as e:
|
||||
- unformatted( )
|
||||
+
|
||||
+try: # Trailing comment with extra leading space.
|
||||
+ for i in range(10): # Trailing comment with extra leading space.
|
||||
+ while condition:
|
||||
+ if something:
|
||||
+ then_something()
|
||||
+ elif something_else:
|
||||
+ then_something_else()
|
||||
+except ValueError as e:
|
||||
+ unformatted()
|
||||
finally:
|
||||
- unformatted( )
|
||||
+ unformatted()
|
||||
+
|
||||
|
||||
-async def test_async_unformatted( ): # Trailing comment with extra leading space.
|
||||
- async for i in some_iter( unformatted ): # Trailing comment with extra leading space.
|
||||
- await asyncio.sleep( 1 )
|
||||
- async with some_context( unformatted ):
|
||||
- print( "unformatted" )
|
||||
+async def test_async_unformatted(): # Trailing comment with extra leading space.
|
||||
+ async for i in some_iter(unformatted): # Trailing comment with extra leading space.
|
||||
+ await asyncio.sleep(1)
|
||||
+ async with some_context(unformatted):
|
||||
+ print("unformatted")
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```python
|
||||
# flags: --line-ranges=5-6
|
||||
# NOTE: If you need to modify this file, pay special attention to the --line-ranges=
|
||||
# flag above as it's formatting specifically these lines.
|
||||
def foo1(
|
||||
parameter_1,
|
||||
parameter_2,
|
||||
parameter_3,
|
||||
parameter_4,
|
||||
parameter_5,
|
||||
parameter_6,
|
||||
parameter_7,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def foo2(
|
||||
parameter_1,
|
||||
parameter_2,
|
||||
parameter_3,
|
||||
parameter_4,
|
||||
parameter_5,
|
||||
parameter_6,
|
||||
parameter_7,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def foo3(
|
||||
parameter_1,
|
||||
parameter_2,
|
||||
parameter_3,
|
||||
parameter_4,
|
||||
parameter_5,
|
||||
parameter_6,
|
||||
parameter_7,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def foo4(
|
||||
parameter_1,
|
||||
parameter_2,
|
||||
parameter_3,
|
||||
parameter_4,
|
||||
parameter_5,
|
||||
parameter_6,
|
||||
parameter_7,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
# Adding some unformated code covering a wide range of syntaxes.
|
||||
|
||||
if True:
|
||||
# Incorrectly indented prefix comments.
|
||||
pass
|
||||
|
||||
import typing
|
||||
from typing import (
|
||||
Any,
|
||||
)
|
||||
|
||||
|
||||
class MyClass(object): # Trailing comment with extra leading space.
|
||||
# NOTE: The following indentation is incorrect:
|
||||
@decor(1 * 3)
|
||||
def my_func(arg):
|
||||
pass
|
||||
|
||||
|
||||
try: # Trailing comment with extra leading space.
|
||||
for i in range(10): # Trailing comment with extra leading space.
|
||||
while condition:
|
||||
if something:
|
||||
then_something()
|
||||
elif something_else:
|
||||
then_something_else()
|
||||
except ValueError as e:
|
||||
unformatted()
|
||||
finally:
|
||||
unformatted()
|
||||
|
||||
|
||||
async def test_async_unformatted(): # Trailing comment with extra leading space.
|
||||
async for i in some_iter(unformatted): # Trailing comment with extra leading space.
|
||||
await asyncio.sleep(1)
|
||||
async with some_context(unformatted):
|
||||
print("unformatted")
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```python
|
||||
# flags: --line-ranges=5-6
|
||||
# NOTE: If you need to modify this file, pay special attention to the --line-ranges=
|
||||
# flag above as it's formatting specifically these lines.
|
||||
def foo1(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
|
||||
def foo2(
|
||||
parameter_1,
|
||||
parameter_2,
|
||||
parameter_3,
|
||||
parameter_4,
|
||||
parameter_5,
|
||||
parameter_6,
|
||||
parameter_7,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def foo3(
|
||||
parameter_1,
|
||||
parameter_2,
|
||||
parameter_3,
|
||||
parameter_4,
|
||||
parameter_5,
|
||||
parameter_6,
|
||||
parameter_7,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def foo4(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
|
||||
|
||||
# Adding some unformated code covering a wide range of syntaxes.
|
||||
|
||||
if True:
|
||||
# Incorrectly indented prefix comments.
|
||||
pass
|
||||
|
||||
import typing
|
||||
from typing import (
|
||||
Any ,
|
||||
)
|
||||
class MyClass( object): # Trailing comment with extra leading space.
|
||||
#NOTE: The following indentation is incorrect:
|
||||
@decor( 1 * 3 )
|
||||
def my_func( arg):
|
||||
pass
|
||||
|
||||
try: # Trailing comment with extra leading space.
|
||||
for i in range(10): # Trailing comment with extra leading space.
|
||||
while condition:
|
||||
if something:
|
||||
then_something( )
|
||||
elif something_else:
|
||||
then_something_else( )
|
||||
except ValueError as e:
|
||||
unformatted( )
|
||||
finally:
|
||||
unformatted( )
|
||||
|
||||
async def test_async_unformatted( ): # Trailing comment with extra leading space.
|
||||
async for i in some_iter( unformatted ): # Trailing comment with extra leading space.
|
||||
await asyncio.sleep( 1 )
|
||||
async with some_context( unformatted ):
|
||||
print( "unformatted" )
|
||||
```
|
||||
|
||||
|
|
@ -25,16 +25,14 @@ print ( "format me" )
|
|||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -6,7 +6,8 @@
|
||||
# This can be fixed in the future if we use a better diffing algorithm, or make Black
|
||||
# perform formatting in a single pass.
|
||||
|
||||
-print ( "format me" )
|
||||
@@ -9,5 +9,5 @@
|
||||
print ( "format me" )
|
||||
print("format me")
|
||||
print("format me")
|
||||
print("format me")
|
||||
print("format me")
|
||||
+print("format me")
|
||||
-print("format me")
|
||||
-print("format me")
|
||||
+print ( "format me" )
|
||||
+print ( "format me" )
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
@ -48,11 +46,11 @@ print ( "format me" )
|
|||
# This can be fixed in the future if we use a better diffing algorithm, or make Black
|
||||
# perform formatting in a single pass.
|
||||
|
||||
print ( "format me" )
|
||||
print("format me")
|
||||
print("format me")
|
||||
print("format me")
|
||||
print("format me")
|
||||
print("format me")
|
||||
print ( "format me" )
|
||||
print ( "format me" )
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
|
|
@ -1,111 +0,0 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/line_ranges_fmt_off.py
|
||||
---
|
||||
## Input
|
||||
|
||||
```python
|
||||
# flags: --line-ranges=7-7 --line-ranges=17-23
|
||||
# NOTE: If you need to modify this file, pay special attention to the --line-ranges=
|
||||
# flag above as it's formatting specifically these lines.
|
||||
|
||||
# fmt: off
|
||||
import os
|
||||
def myfunc( ): # Intentionally unformatted.
|
||||
pass
|
||||
# fmt: on
|
||||
|
||||
|
||||
def myfunc( ): # This will not be reformatted.
|
||||
print( {"also won't be reformatted"} )
|
||||
# fmt: off
|
||||
def myfunc( ): # This will not be reformatted.
|
||||
print( {"also won't be reformatted"} )
|
||||
def myfunc( ): # This will not be reformatted.
|
||||
print( {"also won't be reformatted"} )
|
||||
# fmt: on
|
||||
|
||||
|
||||
def myfunc( ): # This will be reformatted.
|
||||
print( {"this will be reformatted"} )
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -9,8 +9,10 @@
|
||||
# fmt: on
|
||||
|
||||
|
||||
-def myfunc( ): # This will not be reformatted.
|
||||
- print( {"also won't be reformatted"} )
|
||||
+def myfunc(): # This will not be reformatted.
|
||||
+ print({"also won't be reformatted"})
|
||||
+
|
||||
+
|
||||
# fmt: off
|
||||
def myfunc( ): # This will not be reformatted.
|
||||
print( {"also won't be reformatted"} )
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```python
|
||||
# flags: --line-ranges=7-7 --line-ranges=17-23
|
||||
# NOTE: If you need to modify this file, pay special attention to the --line-ranges=
|
||||
# flag above as it's formatting specifically these lines.
|
||||
|
||||
# fmt: off
|
||||
import os
|
||||
def myfunc( ): # Intentionally unformatted.
|
||||
pass
|
||||
# fmt: on
|
||||
|
||||
|
||||
def myfunc(): # This will not be reformatted.
|
||||
print({"also won't be reformatted"})
|
||||
|
||||
|
||||
# fmt: off
|
||||
def myfunc( ): # This will not be reformatted.
|
||||
print( {"also won't be reformatted"} )
|
||||
def myfunc( ): # This will not be reformatted.
|
||||
print( {"also won't be reformatted"} )
|
||||
# fmt: on
|
||||
|
||||
|
||||
def myfunc(): # This will be reformatted.
|
||||
print({"this will be reformatted"})
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```python
|
||||
# flags: --line-ranges=7-7 --line-ranges=17-23
|
||||
# NOTE: If you need to modify this file, pay special attention to the --line-ranges=
|
||||
# flag above as it's formatting specifically these lines.
|
||||
|
||||
# fmt: off
|
||||
import os
|
||||
def myfunc( ): # Intentionally unformatted.
|
||||
pass
|
||||
# fmt: on
|
||||
|
||||
|
||||
def myfunc( ): # This will not be reformatted.
|
||||
print( {"also won't be reformatted"} )
|
||||
# fmt: off
|
||||
def myfunc( ): # This will not be reformatted.
|
||||
print( {"also won't be reformatted"} )
|
||||
def myfunc( ): # This will not be reformatted.
|
||||
print( {"also won't be reformatted"} )
|
||||
# fmt: on
|
||||
|
||||
|
||||
def myfunc(): # This will be reformatted.
|
||||
print({"this will be reformatted"})
|
||||
```
|
||||
|
||||
|
|
@ -33,12 +33,7 @@ class MyClass:
|
|||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -4,12 +4,11 @@
|
||||
|
||||
# Regression test for an edge case involving decorators and fmt: off/on.
|
||||
class MyClass:
|
||||
-
|
||||
# fmt: off
|
||||
@@ -9,7 +9,7 @@
|
||||
@decorator ( )
|
||||
# fmt: on
|
||||
def method():
|
||||
|
@ -47,7 +42,7 @@ class MyClass:
|
|||
|
||||
@decor(
|
||||
a=1,
|
||||
@@ -18,4 +17,4 @@
|
||||
@@ -18,4 +18,4 @@
|
||||
# fmt: on
|
||||
)
|
||||
def func():
|
||||
|
@ -64,6 +59,7 @@ class MyClass:
|
|||
|
||||
# Regression test for an edge case involving decorators and fmt: off/on.
|
||||
class MyClass:
|
||||
|
||||
# fmt: off
|
||||
@decorator ( )
|
||||
# fmt: on
|
||||
|
|
|
@ -1,93 +0,0 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/line_ranges_fmt_off_overlap.py
|
||||
---
|
||||
## Input
|
||||
|
||||
```python
|
||||
# flags: --line-ranges=11-17
|
||||
# NOTE: If you need to modify this file, pay special attention to the --line-ranges=
|
||||
# flag above as it's formatting specifically these lines.
|
||||
|
||||
|
||||
def myfunc( ): # This will not be reformatted.
|
||||
print( {"also won't be reformatted"} )
|
||||
# fmt: off
|
||||
def myfunc( ): # This will not be reformatted.
|
||||
print( {"also won't be reformatted"} )
|
||||
def myfunc( ): # This will not be reformatted.
|
||||
print( {"also won't be reformatted"} )
|
||||
# fmt: on
|
||||
|
||||
|
||||
def myfunc( ): # This will be reformatted.
|
||||
print( {"this will be reformatted"} )
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -3,8 +3,10 @@
|
||||
# flag above as it's formatting specifically these lines.
|
||||
|
||||
|
||||
-def myfunc( ): # This will not be reformatted.
|
||||
- print( {"also won't be reformatted"} )
|
||||
+def myfunc(): # This will not be reformatted.
|
||||
+ print({"also won't be reformatted"})
|
||||
+
|
||||
+
|
||||
# fmt: off
|
||||
def myfunc( ): # This will not be reformatted.
|
||||
print( {"also won't be reformatted"} )
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```python
|
||||
# flags: --line-ranges=11-17
|
||||
# NOTE: If you need to modify this file, pay special attention to the --line-ranges=
|
||||
# flag above as it's formatting specifically these lines.
|
||||
|
||||
|
||||
def myfunc(): # This will not be reformatted.
|
||||
print({"also won't be reformatted"})
|
||||
|
||||
|
||||
# fmt: off
|
||||
def myfunc( ): # This will not be reformatted.
|
||||
print( {"also won't be reformatted"} )
|
||||
def myfunc( ): # This will not be reformatted.
|
||||
print( {"also won't be reformatted"} )
|
||||
# fmt: on
|
||||
|
||||
|
||||
def myfunc(): # This will be reformatted.
|
||||
print({"this will be reformatted"})
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```python
|
||||
# flags: --line-ranges=11-17
|
||||
# NOTE: If you need to modify this file, pay special attention to the --line-ranges=
|
||||
# flag above as it's formatting specifically these lines.
|
||||
|
||||
|
||||
def myfunc( ): # This will not be reformatted.
|
||||
print( {"also won't be reformatted"} )
|
||||
# fmt: off
|
||||
def myfunc( ): # This will not be reformatted.
|
||||
print( {"also won't be reformatted"} )
|
||||
def myfunc( ): # This will not be reformatted.
|
||||
print( {"also won't be reformatted"} )
|
||||
# fmt: on
|
||||
|
||||
|
||||
def myfunc(): # This will be reformatted.
|
||||
print({"this will be reformatted"})
|
||||
```
|
||||
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/line_ranges_indentation.py
|
||||
---
|
||||
## Input
|
||||
|
||||
```python
|
||||
# flags: --line-ranges=5-5
|
||||
# NOTE: If you need to modify this file, pay special attention to the --line-ranges=
|
||||
# flag above as it's formatting specifically these lines.
|
||||
if cond1:
|
||||
print("first")
|
||||
if cond2:
|
||||
print("second")
|
||||
else:
|
||||
print("else")
|
||||
|
||||
if another_cond:
|
||||
print("will not be changed")
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -9,4 +9,4 @@
|
||||
print("else")
|
||||
|
||||
if another_cond:
|
||||
- print("will not be changed")
|
||||
+ print("will not be changed")
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```python
|
||||
# flags: --line-ranges=5-5
|
||||
# NOTE: If you need to modify this file, pay special attention to the --line-ranges=
|
||||
# flag above as it's formatting specifically these lines.
|
||||
if cond1:
|
||||
print("first")
|
||||
if cond2:
|
||||
print("second")
|
||||
else:
|
||||
print("else")
|
||||
|
||||
if another_cond:
|
||||
print("will not be changed")
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```python
|
||||
# flags: --line-ranges=5-5
|
||||
# NOTE: If you need to modify this file, pay special attention to the --line-ranges=
|
||||
# flag above as it's formatting specifically these lines.
|
||||
if cond1:
|
||||
print("first")
|
||||
if cond2:
|
||||
print("second")
|
||||
else:
|
||||
print("else")
|
||||
|
||||
if another_cond:
|
||||
print("will not be changed")
|
||||
```
|
||||
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/line_ranges_two_passes.py
|
||||
---
|
||||
## Input
|
||||
|
||||
```python
|
||||
# flags: --line-ranges=9-11
|
||||
# NOTE: If you need to modify this file, pay special attention to the --line-ranges=
|
||||
# flag above as it's formatting specifically these lines.
|
||||
|
||||
# This is a specific case for Black's two-pass formatting behavior in `format_str`.
|
||||
# The second pass must respect the line ranges before the first pass.
|
||||
|
||||
|
||||
def restrict_to_this_line(arg1,
|
||||
arg2,
|
||||
arg3):
|
||||
print ( "This should not be formatted." )
|
||||
print ( "Note that in the second pass, the original line range 9-11 will cover these print lines.")
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -7,5 +7,7 @@
|
||||
|
||||
|
||||
def restrict_to_this_line(arg1, arg2, arg3):
|
||||
- print ( "This should not be formatted." )
|
||||
- print ( "Note that in the second pass, the original line range 9-11 will cover these print lines.")
|
||||
+ print("This should not be formatted.")
|
||||
+ print(
|
||||
+ "Note that in the second pass, the original line range 9-11 will cover these print lines."
|
||||
+ )
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```python
|
||||
# flags: --line-ranges=9-11
|
||||
# NOTE: If you need to modify this file, pay special attention to the --line-ranges=
|
||||
# flag above as it's formatting specifically these lines.
|
||||
|
||||
# This is a specific case for Black's two-pass formatting behavior in `format_str`.
|
||||
# The second pass must respect the line ranges before the first pass.
|
||||
|
||||
|
||||
def restrict_to_this_line(arg1, arg2, arg3):
|
||||
print("This should not be formatted.")
|
||||
print(
|
||||
"Note that in the second pass, the original line range 9-11 will cover these print lines."
|
||||
)
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```python
|
||||
# flags: --line-ranges=9-11
|
||||
# NOTE: If you need to modify this file, pay special attention to the --line-ranges=
|
||||
# flag above as it's formatting specifically these lines.
|
||||
|
||||
# This is a specific case for Black's two-pass formatting behavior in `format_str`.
|
||||
# The second pass must respect the line ranges before the first pass.
|
||||
|
||||
|
||||
def restrict_to_this_line(arg1, arg2, arg3):
|
||||
print ( "This should not be formatted." )
|
||||
print ( "Note that in the second pass, the original line range 9-11 will cover these print lines.")
|
||||
```
|
||||
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/ancestory.py
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
def test ():
|
||||
if True:
|
||||
print(<RANGE_START>1 + 2)
|
||||
|
||||
else:
|
||||
print(3 + 4)<RANGE_END>
|
||||
|
||||
print(" Do not format this")
|
||||
|
||||
|
||||
|
||||
def test_empty_lines ():
|
||||
if True:
|
||||
print(<RANGE_START>1 + 2)
|
||||
|
||||
|
||||
else:
|
||||
print(3 + 4)<RANGE_END>
|
||||
|
||||
print(" Do not format this")
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
def test ():
|
||||
if True:
|
||||
print(1 + 2)
|
||||
|
||||
else:
|
||||
print(3 + 4)
|
||||
|
||||
print(" Do not format this")
|
||||
|
||||
|
||||
|
||||
def test_empty_lines ():
|
||||
if True:
|
||||
print(1 + 2)
|
||||
|
||||
else:
|
||||
print(3 + 4)
|
||||
|
||||
print(" Do not format this")
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/clause_header.py
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
def test(<RANGE_START>a, b, c: str<RANGE_END>, d):
|
||||
print ( "Don't format the body when only making changes to the clause header")
|
||||
|
||||
|
||||
print( "Should not get formatted")
|
||||
|
||||
|
||||
class <RANGE_START> Test(OtherClass<RANGE_END>)\
|
||||
: # comment
|
||||
|
||||
# Should not get formatted
|
||||
def __init__( self):
|
||||
print("hello")
|
||||
|
||||
print( "dont' format this")
|
||||
|
||||
|
||||
def test2(<RANGE_START>a, b, c: str, d):<RANGE_END>
|
||||
print ( "Don't format the body when only making changes to the clause header")
|
||||
|
||||
|
||||
print( "Should not get formatted")
|
||||
|
||||
|
||||
def test3(<RANGE_START>a, b, c: str, d):<RANGE_END> # fmt: skip
|
||||
print ( "Don't format the body when only making changes to the clause header")
|
||||
|
||||
|
||||
|
||||
def test4(<RANGE_START> a):
|
||||
print("Format this" )
|
||||
|
||||
if True:
|
||||
print( "and this")<RANGE_END>
|
||||
|
||||
print("Not this" )
|
||||
|
||||
|
||||
<RANGE_START>if a + b : # trailing clause header comment<RANGE_END>
|
||||
print("Not formatted" )
|
||||
|
||||
|
||||
<RANGE_START>if b + c :<RANGE_END> # trailing clause header comment
|
||||
print("Not formatted" )
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
def test(a, b, c: str, d):
|
||||
print ( "Don't format the body when only making changes to the clause header")
|
||||
|
||||
|
||||
print( "Should not get formatted")
|
||||
|
||||
|
||||
class Test(OtherClass): # comment
|
||||
|
||||
# Should not get formatted
|
||||
def __init__( self):
|
||||
print("hello")
|
||||
|
||||
print( "dont' format this")
|
||||
|
||||
|
||||
def test2(a, b, c: str, d):
|
||||
print ( "Don't format the body when only making changes to the clause header")
|
||||
|
||||
|
||||
print( "Should not get formatted")
|
||||
|
||||
|
||||
def test3(a, b, c: str, d): # fmt: skip
|
||||
print ( "Don't format the body when only making changes to the clause header")
|
||||
|
||||
|
||||
|
||||
def test4(a):
|
||||
print("Format this")
|
||||
|
||||
if True:
|
||||
print("and this")
|
||||
|
||||
print("Not this" )
|
||||
|
||||
|
||||
if a + b: # trailing clause header comment
|
||||
print("Not formatted" )
|
||||
|
||||
|
||||
if b + c: # trailing clause header comment
|
||||
print("Not formatted" )
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/comment_only_range.py
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
def test ():
|
||||
<RANGE_START># Some leading comment
|
||||
# that spans multiple lines
|
||||
<RANGE_END>
|
||||
print("Do not format this" )
|
||||
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
def test ():
|
||||
# Some leading comment
|
||||
# that spans multiple lines
|
||||
|
||||
print("Do not format this" )
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/decorators.py
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
def test():
|
||||
|
||||
print("before" )
|
||||
|
||||
@<RANGE_START> decorator( aa)
|
||||
|
||||
<RANGE_END>def func ():
|
||||
print("Do not format this" )
|
||||
|
||||
|
||||
<RANGE_START>@ decorator( a)
|
||||
def test( a):<RANGE_END>
|
||||
print( "body")
|
||||
|
||||
print("after" )
|
||||
|
||||
|
||||
<RANGE_START>@ decorator( a)<RANGE_END>
|
||||
def test( a):
|
||||
print( "body")
|
||||
|
||||
print("after" )
|
||||
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
def test():
|
||||
|
||||
print("before" )
|
||||
|
||||
@decorator(aa)
|
||||
def func():
|
||||
print("Do not format this" )
|
||||
|
||||
|
||||
@decorator(a)
|
||||
def test(a):
|
||||
print( "body")
|
||||
|
||||
print("after" )
|
||||
|
||||
|
||||
@decorator(a)
|
||||
def test( a):
|
||||
print( "body")
|
||||
|
||||
print("after" )
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,391 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/docstring_code_examples.py
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
def doctest_simple ():
|
||||
<RANGE_START>"""
|
||||
Do cool stuff.
|
||||
|
||||
>>> cool_stuff( 1 )
|
||||
2
|
||||
"""
|
||||
pass<RANGE_END>
|
||||
|
||||
|
||||
def doctest_only ():
|
||||
<RANGE_START>"""
|
||||
Do cool stuff.
|
||||
|
||||
>>> def cool_stuff( x ):
|
||||
... print( f"hi {x}" );
|
||||
hi 2
|
||||
"""<RANGE_END>
|
||||
pass
|
||||
|
||||
|
||||
def in_doctest ():
|
||||
"""
|
||||
Do cool stuff.
|
||||
<RANGE_START>
|
||||
>>> cool_stuff( x )
|
||||
>>> cool_stuff( y )
|
||||
2<RANGE_END>
|
||||
"""
|
||||
pass
|
||||
|
||||
def suppressed_doctest ():
|
||||
"""
|
||||
Do cool stuff.
|
||||
<RANGE_START>
|
||||
>>> cool_stuff( x )
|
||||
>>> cool_stuff( y )
|
||||
2<RANGE_END>
|
||||
""" # fmt: skip
|
||||
pass
|
||||
|
||||
|
||||
def fmt_off_doctest ():
|
||||
# fmt: off
|
||||
"""
|
||||
Do cool stuff.
|
||||
<RANGE_START>
|
||||
>>> cool_stuff( x )
|
||||
>>> cool_stuff( y )
|
||||
2<RANGE_END>
|
||||
"""
|
||||
# fmt: on
|
||||
pass
|
||||
|
||||
|
||||
|
||||
if True:
|
||||
def doctest_long_lines():
|
||||
<RANGE_START>
|
||||
'''
|
||||
This won't get wrapped even though it exceeds our configured
|
||||
line width because it doesn't exceed the line width within this
|
||||
docstring. e.g, the `f` in `foo` is treated as the first column.
|
||||
>>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey)
|
||||
|
||||
But this one is long enough to get wrapped.
|
||||
>>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard)
|
||||
|
||||
This one doesn't get wrapped with an indent width of 4 even with `dynamic` line width
|
||||
>>> a = this_is_a_long_line(lion, giraffe, hippo, zebra, lemur, penguin, monkey)
|
||||
|
||||
This one gets wrapped with `dynamic` line width and an indent width of 4 because it exceeds the width by 1
|
||||
>>> ab = this_is_a_long_line(lion, giraffe, hippo, zebra, lemur, penguin, monkey)
|
||||
'''
|
||||
# This demonstrates a normal line that will get wrapped but won't
|
||||
# get wrapped in the docstring above because of how the line-width
|
||||
# setting gets reset at the first column in each code snippet.
|
||||
foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey)
|
||||
<RANGE_END>
|
||||
|
||||
|
||||
if True:
|
||||
def doctest_long_lines():
|
||||
<RANGE_START>'''
|
||||
This won't get wrapped even though it exceeds our configured
|
||||
line width because it doesn't exceed the line width within this
|
||||
docstring. e.g, the `f` in `foo` is treated as the first column.
|
||||
>>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey)
|
||||
|
||||
But this one is long enough to get wrapped.
|
||||
>>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard)
|
||||
|
||||
This one doesn't get wrapped with an indent width of 4 even with `dynamic` line width
|
||||
>>> a = this_is_a_long_line(lion, giraffe, hippo, zebra, lemur, penguin, monkey)
|
||||
|
||||
This one gets wrapped with `dynamic` line width and an indent width of 4 because it exceeds the width by 1
|
||||
>>> ab = this_is_a_long_line(lion, giraffe, hippo, zebra, lemur, penguin, monkey)
|
||||
'''<RANGE_END>
|
||||
# This demonstrates a normal line that will get wrapped but won't
|
||||
# get wrapped in the docstring above because of how the line-width
|
||||
# setting gets reset at the first column in each code snippet.
|
||||
foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey)
|
||||
|
||||
```
|
||||
|
||||
## Outputs
|
||||
### Output 1
|
||||
```
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
line-ending = LineFeed
|
||||
magic-trailing-comma = Respect
|
||||
docstring-code = Enabled
|
||||
docstring-code-line-width = "dynamic"
|
||||
preview = Disabled
|
||||
target_version = Py38
|
||||
source_type = Python
|
||||
```
|
||||
|
||||
```python
|
||||
def doctest_simple ():
|
||||
"""
|
||||
Do cool stuff.
|
||||
|
||||
>>> cool_stuff(1)
|
||||
2
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def doctest_only ():
|
||||
"""
|
||||
Do cool stuff.
|
||||
|
||||
>>> def cool_stuff(x):
|
||||
... print(f"hi {x}")
|
||||
hi 2
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def in_doctest ():
|
||||
"""
|
||||
Do cool stuff.
|
||||
|
||||
>>> cool_stuff(x)
|
||||
>>> cool_stuff(y)
|
||||
2
|
||||
"""
|
||||
pass
|
||||
|
||||
def suppressed_doctest ():
|
||||
"""
|
||||
Do cool stuff.
|
||||
|
||||
>>> cool_stuff( x )
|
||||
>>> cool_stuff( y )
|
||||
2
|
||||
""" # fmt: skip
|
||||
pass
|
||||
|
||||
|
||||
def fmt_off_doctest ():
|
||||
# fmt: off
|
||||
"""
|
||||
Do cool stuff.
|
||||
|
||||
>>> cool_stuff( x )
|
||||
>>> cool_stuff( y )
|
||||
2
|
||||
"""
|
||||
# fmt: on
|
||||
pass
|
||||
|
||||
|
||||
|
||||
if True:
|
||||
def doctest_long_lines():
|
||||
"""
|
||||
This won't get wrapped even though it exceeds our configured
|
||||
line width because it doesn't exceed the line width within this
|
||||
docstring. e.g, the `f` in `foo` is treated as the first column.
|
||||
>>> foo, bar, quux = this_is_a_long_line(
|
||||
... lion, giraffe, hippo, zeba, lemur, penguin, monkey
|
||||
... )
|
||||
|
||||
But this one is long enough to get wrapped.
|
||||
>>> foo, bar, quux = this_is_a_long_line(
|
||||
... lion,
|
||||
... giraffe,
|
||||
... hippo,
|
||||
... zeba,
|
||||
... lemur,
|
||||
... penguin,
|
||||
... monkey,
|
||||
... spider,
|
||||
... bear,
|
||||
... leopard,
|
||||
... )
|
||||
|
||||
This one doesn't get wrapped with an indent width of 4 even with `dynamic` line width
|
||||
>>> a = this_is_a_long_line(lion, giraffe, hippo, zebra, lemur, penguin, monkey)
|
||||
|
||||
This one gets wrapped with `dynamic` line width and an indent width of 4 because it exceeds the width by 1
|
||||
>>> ab = this_is_a_long_line(
|
||||
... lion, giraffe, hippo, zebra, lemur, penguin, monkey
|
||||
... )
|
||||
"""
|
||||
# This demonstrates a normal line that will get wrapped but won't
|
||||
# get wrapped in the docstring above because of how the line-width
|
||||
# setting gets reset at the first column in each code snippet.
|
||||
foo, bar, quux = this_is_a_long_line(
|
||||
lion, giraffe, hippo, zeba, lemur, penguin, monkey
|
||||
)
|
||||
|
||||
|
||||
if True:
|
||||
def doctest_long_lines():
|
||||
"""
|
||||
This won't get wrapped even though it exceeds our configured
|
||||
line width because it doesn't exceed the line width within this
|
||||
docstring. e.g, the `f` in `foo` is treated as the first column.
|
||||
>>> foo, bar, quux = this_is_a_long_line(
|
||||
... lion, giraffe, hippo, zeba, lemur, penguin, monkey
|
||||
... )
|
||||
|
||||
But this one is long enough to get wrapped.
|
||||
>>> foo, bar, quux = this_is_a_long_line(
|
||||
... lion,
|
||||
... giraffe,
|
||||
... hippo,
|
||||
... zeba,
|
||||
... lemur,
|
||||
... penguin,
|
||||
... monkey,
|
||||
... spider,
|
||||
... bear,
|
||||
... leopard,
|
||||
... )
|
||||
|
||||
This one doesn't get wrapped with an indent width of 4 even with `dynamic` line width
|
||||
>>> a = this_is_a_long_line(lion, giraffe, hippo, zebra, lemur, penguin, monkey)
|
||||
|
||||
This one gets wrapped with `dynamic` line width and an indent width of 4 because it exceeds the width by 1
|
||||
>>> ab = this_is_a_long_line(
|
||||
... lion, giraffe, hippo, zebra, lemur, penguin, monkey
|
||||
... )
|
||||
"""
|
||||
# This demonstrates a normal line that will get wrapped but won't
|
||||
# get wrapped in the docstring above because of how the line-width
|
||||
# setting gets reset at the first column in each code snippet.
|
||||
foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey)
|
||||
|
||||
```
|
||||
|
||||
|
||||
### Output 2
|
||||
```
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
line-ending = LineFeed
|
||||
magic-trailing-comma = Respect
|
||||
docstring-code = Enabled
|
||||
docstring-code-line-width = 88
|
||||
preview = Disabled
|
||||
target_version = Py38
|
||||
source_type = Python
|
||||
```
|
||||
|
||||
```python
|
||||
def doctest_simple ():
|
||||
"""
|
||||
Do cool stuff.
|
||||
|
||||
>>> cool_stuff(1)
|
||||
2
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def doctest_only ():
|
||||
"""
|
||||
Do cool stuff.
|
||||
|
||||
>>> def cool_stuff(x):
|
||||
... print(f"hi {x}")
|
||||
hi 2
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def in_doctest ():
|
||||
"""
|
||||
Do cool stuff.
|
||||
|
||||
>>> cool_stuff(x)
|
||||
>>> cool_stuff(y)
|
||||
2
|
||||
"""
|
||||
pass
|
||||
|
||||
def suppressed_doctest ():
|
||||
"""
|
||||
Do cool stuff.
|
||||
|
||||
>>> cool_stuff( x )
|
||||
>>> cool_stuff( y )
|
||||
2
|
||||
""" # fmt: skip
|
||||
pass
|
||||
|
||||
|
||||
def fmt_off_doctest ():
|
||||
# fmt: off
|
||||
"""
|
||||
Do cool stuff.
|
||||
|
||||
>>> cool_stuff( x )
|
||||
>>> cool_stuff( y )
|
||||
2
|
||||
"""
|
||||
# fmt: on
|
||||
pass
|
||||
|
||||
|
||||
|
||||
if True:
|
||||
def doctest_long_lines():
|
||||
"""
|
||||
This won't get wrapped even though it exceeds our configured
|
||||
line width because it doesn't exceed the line width within this
|
||||
docstring. e.g, the `f` in `foo` is treated as the first column.
|
||||
>>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey)
|
||||
|
||||
But this one is long enough to get wrapped.
|
||||
>>> foo, bar, quux = this_is_a_long_line(
|
||||
... lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard
|
||||
... )
|
||||
|
||||
This one doesn't get wrapped with an indent width of 4 even with `dynamic` line width
|
||||
>>> a = this_is_a_long_line(lion, giraffe, hippo, zebra, lemur, penguin, monkey)
|
||||
|
||||
This one gets wrapped with `dynamic` line width and an indent width of 4 because it exceeds the width by 1
|
||||
>>> ab = this_is_a_long_line(lion, giraffe, hippo, zebra, lemur, penguin, monkey)
|
||||
"""
|
||||
# This demonstrates a normal line that will get wrapped but won't
|
||||
# get wrapped in the docstring above because of how the line-width
|
||||
# setting gets reset at the first column in each code snippet.
|
||||
foo, bar, quux = this_is_a_long_line(
|
||||
lion, giraffe, hippo, zeba, lemur, penguin, monkey
|
||||
)
|
||||
|
||||
|
||||
if True:
|
||||
def doctest_long_lines():
|
||||
"""
|
||||
This won't get wrapped even though it exceeds our configured
|
||||
line width because it doesn't exceed the line width within this
|
||||
docstring. e.g, the `f` in `foo` is treated as the first column.
|
||||
>>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey)
|
||||
|
||||
But this one is long enough to get wrapped.
|
||||
>>> foo, bar, quux = this_is_a_long_line(
|
||||
... lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard
|
||||
... )
|
||||
|
||||
This one doesn't get wrapped with an indent width of 4 even with `dynamic` line width
|
||||
>>> a = this_is_a_long_line(lion, giraffe, hippo, zebra, lemur, penguin, monkey)
|
||||
|
||||
This one gets wrapped with `dynamic` line width and an indent width of 4 because it exceeds the width by 1
|
||||
>>> ab = this_is_a_long_line(lion, giraffe, hippo, zebra, lemur, penguin, monkey)
|
||||
"""
|
||||
# This demonstrates a normal line that will get wrapped but won't
|
||||
# get wrapped in the docstring above because of how the line-width
|
||||
# setting gets reset at the first column in each code snippet.
|
||||
foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey)
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/empty_file.py
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
<RANGE_START><RANGE_END>```
|
||||
|
||||
## Output
|
||||
```python
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/empty_range.py
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
def test():
|
||||
<RANGE_START><RANGE_END>print( "test" )
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
def test():
|
||||
print( "test" )
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/fmt_on_off.py
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
class MyClass:
|
||||
|
||||
# Range that falls entirely in a suppressed range
|
||||
# fmt: off<RANGE_START>
|
||||
def method( self ):
|
||||
print ( "str" )
|
||||
<RANGE_END># fmt: on
|
||||
|
||||
# This should net get formatted because it isn't in a formatting range.
|
||||
def not_in_formatting_range ( self): ...
|
||||
|
||||
|
||||
# Range that starts in a suppressed range and ends in a formatting range
|
||||
# fmt: off<RANGE_START>
|
||||
def other( self):
|
||||
print ( "str" )
|
||||
|
||||
# fmt: on
|
||||
|
||||
def formatted ( self):
|
||||
pass
|
||||
<RANGE_END>
|
||||
def outside_formatting_range (self): pass
|
||||
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
class MyClass:
|
||||
|
||||
# Range that falls entirely in a suppressed range
|
||||
# fmt: off
|
||||
def method( self ):
|
||||
print ( "str" )
|
||||
# fmt: on
|
||||
|
||||
# This should net get formatted because it isn't in a formatting range.
|
||||
def not_in_formatting_range ( self): ...
|
||||
|
||||
|
||||
# Range that starts in a suppressed range and ends in a formatting range
|
||||
# fmt: off
|
||||
def other( self):
|
||||
print ( "str" )
|
||||
|
||||
# fmt: on
|
||||
|
||||
def formatted(self):
|
||||
pass
|
||||
|
||||
def outside_formatting_range (self): pass
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,310 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/indent.py
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
# Formats the entire function with tab or 4 space indentation
|
||||
# because the statement indentations don't match the preferred indentation.
|
||||
def test ():
|
||||
print("before" )
|
||||
<RANGE_START>1 + 2
|
||||
if True:
|
||||
pass
|
||||
print("Done" )<RANGE_END>
|
||||
|
||||
print("formatted" )
|
||||
|
||||
print("not formatted" )
|
||||
|
||||
def test2 ():
|
||||
print("before" )
|
||||
<RANGE_START>1 + 2
|
||||
(
|
||||
3 + 2
|
||||
)
|
||||
print("Done" )<RANGE_END>
|
||||
|
||||
print("formatted" )
|
||||
|
||||
print("not formatted" )
|
||||
|
||||
def test3 ():
|
||||
print("before" )
|
||||
<RANGE_START>1 + 2
|
||||
"""A Multiline string
|
||||
that starts at the beginning of the line and we need to preserve the leading spaces"""
|
||||
|
||||
"""A Multiline string
|
||||
that has some indentation on the second line and we need to preserve the leading spaces"""
|
||||
|
||||
print("Done" )<RANGE_END>
|
||||
|
||||
|
||||
def test4 ():
|
||||
print("before" )
|
||||
<RANGE_START>1 + 2
|
||||
"""A Multiline string
|
||||
that uses the same indentation as the formatted code will. This should not be dedented."""
|
||||
|
||||
print("Done" )<RANGE_END>
|
||||
|
||||
def test5 ():
|
||||
print("before" )
|
||||
if True:
|
||||
print("Format to fix indentation" )
|
||||
print(<RANGE_START>1 + 2)
|
||||
|
||||
else:
|
||||
print(3 + 4)<RANGE_END>
|
||||
print("Format to fix indentation" )
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def test6 ():
|
||||
<RANGE_START>
|
||||
print("Format" )
|
||||
print(3 + 4)<RANGE_END>
|
||||
print("Format to fix indentation" )
|
||||
```
|
||||
|
||||
## Outputs
|
||||
### Output 1
|
||||
```
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
line-ending = LineFeed
|
||||
magic-trailing-comma = Respect
|
||||
docstring-code = Disabled
|
||||
docstring-code-line-width = "dynamic"
|
||||
preview = Disabled
|
||||
target_version = Py38
|
||||
source_type = Python
|
||||
```
|
||||
|
||||
```python
|
||||
# Formats the entire function with tab or 4 space indentation
|
||||
# because the statement indentations don't match the preferred indentation.
|
||||
def test ():
|
||||
print("before")
|
||||
1 + 2
|
||||
if True:
|
||||
pass
|
||||
print("Done")
|
||||
|
||||
print("formatted")
|
||||
|
||||
print("not formatted" )
|
||||
|
||||
def test2 ():
|
||||
print("before")
|
||||
1 + 2
|
||||
(3 + 2)
|
||||
print("Done")
|
||||
|
||||
print("formatted")
|
||||
|
||||
print("not formatted" )
|
||||
|
||||
def test3 ():
|
||||
print("before")
|
||||
1 + 2
|
||||
"""A Multiline string
|
||||
that starts at the beginning of the line and we need to preserve the leading spaces"""
|
||||
|
||||
"""A Multiline string
|
||||
that has some indentation on the second line and we need to preserve the leading spaces"""
|
||||
|
||||
print("Done")
|
||||
|
||||
|
||||
def test4 ():
|
||||
print("before")
|
||||
1 + 2
|
||||
"""A Multiline string
|
||||
that uses the same indentation as the formatted code will. This should not be dedented."""
|
||||
|
||||
print("Done")
|
||||
|
||||
def test5 ():
|
||||
print("before")
|
||||
if True:
|
||||
print("Format to fix indentation")
|
||||
print(1 + 2)
|
||||
|
||||
else:
|
||||
print(3 + 4)
|
||||
print("Format to fix indentation")
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def test6 ():
|
||||
print("Format")
|
||||
print(3 + 4)
|
||||
print("Format to fix indentation" )
|
||||
```
|
||||
|
||||
|
||||
### Output 2
|
||||
```
|
||||
indent-style = tab
|
||||
line-width = 88
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
line-ending = LineFeed
|
||||
magic-trailing-comma = Respect
|
||||
docstring-code = Disabled
|
||||
docstring-code-line-width = "dynamic"
|
||||
preview = Disabled
|
||||
target_version = Py38
|
||||
source_type = Python
|
||||
```
|
||||
|
||||
```python
|
||||
# Formats the entire function with tab or 4 space indentation
|
||||
# because the statement indentations don't match the preferred indentation.
|
||||
def test ():
|
||||
print("before")
|
||||
1 + 2
|
||||
if True:
|
||||
pass
|
||||
print("Done")
|
||||
|
||||
print("formatted")
|
||||
|
||||
print("not formatted" )
|
||||
|
||||
def test2 ():
|
||||
print("before")
|
||||
1 + 2
|
||||
(3 + 2)
|
||||
print("Done")
|
||||
|
||||
print("formatted")
|
||||
|
||||
print("not formatted" )
|
||||
|
||||
def test3 ():
|
||||
print("before")
|
||||
1 + 2
|
||||
"""A Multiline string
|
||||
that starts at the beginning of the line and we need to preserve the leading spaces"""
|
||||
|
||||
"""A Multiline string
|
||||
that has some indentation on the second line and we need to preserve the leading spaces"""
|
||||
|
||||
print("Done")
|
||||
|
||||
|
||||
def test4 ():
|
||||
print("before")
|
||||
1 + 2
|
||||
"""A Multiline string
|
||||
that uses the same indentation as the formatted code will. This should not be dedented."""
|
||||
|
||||
print("Done")
|
||||
|
||||
def test5 ():
|
||||
print("before")
|
||||
if True:
|
||||
print("Format to fix indentation")
|
||||
print(1 + 2)
|
||||
|
||||
else:
|
||||
print(3 + 4)
|
||||
print("Format to fix indentation")
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def test6 ():
|
||||
print("Format")
|
||||
print(3 + 4)
|
||||
print("Format to fix indentation")
|
||||
```
|
||||
|
||||
|
||||
### Output 3
|
||||
```
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
indent-width = 2
|
||||
quote-style = Double
|
||||
line-ending = LineFeed
|
||||
magic-trailing-comma = Respect
|
||||
docstring-code = Disabled
|
||||
docstring-code-line-width = "dynamic"
|
||||
preview = Disabled
|
||||
target_version = Py38
|
||||
source_type = Python
|
||||
```
|
||||
|
||||
```python
|
||||
# Formats the entire function with tab or 4 space indentation
|
||||
# because the statement indentations don't match the preferred indentation.
|
||||
def test ():
|
||||
print("before" )
|
||||
1 + 2
|
||||
if True:
|
||||
pass
|
||||
print("Done")
|
||||
|
||||
print("formatted" )
|
||||
|
||||
print("not formatted" )
|
||||
|
||||
def test2 ():
|
||||
print("before" )
|
||||
1 + 2
|
||||
(3 + 2)
|
||||
print("Done")
|
||||
|
||||
print("formatted" )
|
||||
|
||||
print("not formatted" )
|
||||
|
||||
def test3 ():
|
||||
print("before" )
|
||||
1 + 2
|
||||
"""A Multiline string
|
||||
that starts at the beginning of the line and we need to preserve the leading spaces"""
|
||||
|
||||
"""A Multiline string
|
||||
that has some indentation on the second line and we need to preserve the leading spaces"""
|
||||
|
||||
print("Done")
|
||||
|
||||
|
||||
def test4 ():
|
||||
print("before" )
|
||||
1 + 2
|
||||
"""A Multiline string
|
||||
that uses the same indentation as the formatted code will. This should not be dedented."""
|
||||
|
||||
print("Done")
|
||||
|
||||
def test5 ():
|
||||
print("before" )
|
||||
if True:
|
||||
print("Format to fix indentation")
|
||||
print(1 + 2)
|
||||
|
||||
else:
|
||||
print(3 + 4)
|
||||
print("Format to fix indentation")
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def test6 ():
|
||||
print("Format")
|
||||
print(3 + 4)
|
||||
print("Format to fix indentation")
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/leading_comments.py
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
def test ():
|
||||
print( "hello" )
|
||||
<RANGE_START># leading comment
|
||||
1 + 2
|
||||
|
||||
print( "world" )<RANGE_END>
|
||||
|
||||
print( "unformatted" )
|
||||
|
||||
|
||||
print( "Hy" )
|
||||
|
||||
|
||||
def test2 ():
|
||||
print( "hello" )
|
||||
# Leading comments don't get formatted. That's why Ruff won't fixup
|
||||
# the indentation here. That's something we might want to explore in the future
|
||||
# leading comment 1<RANGE_START>
|
||||
# leading comment 2
|
||||
1 + 2
|
||||
|
||||
print( "world" )<RANGE_END>
|
||||
|
||||
print( "unformatted" )
|
||||
|
||||
def test3 ():
|
||||
<RANGE_START>print( "hello" )
|
||||
# leading comment 1
|
||||
# leading comment 2
|
||||
1 + 2
|
||||
|
||||
print( "world" )<RANGE_END>
|
||||
|
||||
print( "unformatted" )
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
def test ():
|
||||
print( "hello" )
|
||||
# leading comment
|
||||
1 + 2
|
||||
|
||||
print("world")
|
||||
|
||||
print( "unformatted" )
|
||||
|
||||
|
||||
print( "Hy" )
|
||||
|
||||
|
||||
def test2 ():
|
||||
print( "hello" )
|
||||
# Leading comments don't get formatted. That's why Ruff won't fixup
|
||||
# the indentation here. That's something we might want to explore in the future
|
||||
# leading comment 1
|
||||
# leading comment 2
|
||||
1 + 2
|
||||
|
||||
print("world")
|
||||
|
||||
print( "unformatted" )
|
||||
|
||||
def test3 ():
|
||||
print("hello")
|
||||
# leading comment 1
|
||||
# leading comment 2
|
||||
1 + 2
|
||||
|
||||
print("world")
|
||||
|
||||
print( "unformatted" )
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/leading_trailing_comments.py
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
def test ():
|
||||
# Leading comments before the statements that should be formatted
|
||||
# Don't duplicate the comments
|
||||
<RANGE_START>print( "format this")<RANGE_END> # trailing end of line comment
|
||||
# here's some trailing comment as well
|
||||
|
||||
print("Do not format this" )
|
||||
|
||||
def test ():
|
||||
# Leading comments before the statements that should be formatted
|
||||
# Don't duplicate the comments
|
||||
<RANGE_START>print( "format this")
|
||||
# here's some trailing comment as well
|
||||
<RANGE_END>
|
||||
|
||||
print("Do not format this 2" )
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
def test ():
|
||||
# Leading comments before the statements that should be formatted
|
||||
# Don't duplicate the comments
|
||||
print("format this") # trailing end of line comment
|
||||
# here's some trailing comment as well
|
||||
|
||||
print("Do not format this" )
|
||||
|
||||
def test ():
|
||||
# Leading comments before the statements that should be formatted
|
||||
# Don't duplicate the comments
|
||||
print("format this")
|
||||
# here's some trailing comment as well
|
||||
|
||||
|
||||
print("Do not format this 2" )
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/module.py
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
print("Before range start" )
|
||||
|
||||
<RANGE_START>
|
||||
if a + b :
|
||||
print("formatted" )
|
||||
|
||||
print("still in range" )
|
||||
<RANGE_END>
|
||||
|
||||
print("After range end" )
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
print("Before range start" )
|
||||
|
||||
|
||||
if a + b:
|
||||
print("formatted")
|
||||
|
||||
print("still in range")
|
||||
|
||||
|
||||
print("After range end" )
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/range_narrowing.py
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
def test ():
|
||||
<RANGE_START>if True:
|
||||
print( "format")
|
||||
elif False:
|
||||
print ( "and this")<RANGE_END>
|
||||
print("not this" )
|
||||
|
||||
print("nor this" )
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
def test ():
|
||||
if True:
|
||||
print("format")
|
||||
elif False:
|
||||
print("and this")
|
||||
print("not this" )
|
||||
|
||||
print("nor this" )
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/regressions.py
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
class Event:
|
||||
event_name: ClassVar[str]
|
||||
|
||||
@staticmethod
|
||||
def cls_for(event_name: str) -> type[Event]:
|
||||
event_cls = _CONCRETE_EVENT_CLASSES.get(event_name)
|
||||
if event_cls is not <RANGE_START>None:
|
||||
return event_cls<RANGE_END>
|
||||
else:
|
||||
raise ValueError(f"unknown event name '{event_name}'")
|
||||
|
||||
class Event:
|
||||
event_name: ClassVar[str]
|
||||
|
||||
@staticmethod
|
||||
def cls_for(event_name: str) -> type[Event]:
|
||||
event_cls = _CONCRETE_EVENT_CLASSES.get(event_name)
|
||||
if event_cls is not None:
|
||||
<RANGE_START>
|
||||
|
||||
<RANGE_END>
|
||||
return event_cls
|
||||
else:
|
||||
raise ValueError(f"unknown event name '{event_name}'")
|
||||
|
||||
|
||||
# The user starts adding items to a list and then hits save.
|
||||
# Ruff should trim the empty lines
|
||||
a = [
|
||||
1,
|
||||
2,
|
||||
3,<RANGE_START>
|
||||
|
||||
|
||||
<RANGE_END>
|
||||
]
|
||||
|
||||
print("Don't format this" )
|
||||
|
||||
|
||||
# The user removed an argument from a call. Ruff should reformat the entire call
|
||||
call(
|
||||
a,
|
||||
<RANGE_START>
|
||||
<RANGE_END>b,
|
||||
c,
|
||||
d
|
||||
)
|
||||
|
||||
print("Don't format this" )
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# The user adds a new comment at the end:
|
||||
<RANGE_START># <RANGE_END>
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
print("Don't format this" )
|
||||
|
||||
|
||||
def convert_str(value: str) -> str: # Trailing comment
|
||||
"""Return a string as-is."""
|
||||
|
||||
<RANGE_START>
|
||||
|
||||
return value # Trailing comment
|
||||
<RANGE_END>
|
||||
def test ():
|
||||
pass
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
class Event:
|
||||
event_name: ClassVar[str]
|
||||
|
||||
@staticmethod
|
||||
def cls_for(event_name: str) -> type[Event]:
|
||||
event_cls = _CONCRETE_EVENT_CLASSES.get(event_name)
|
||||
if event_cls is not None:
|
||||
return event_cls
|
||||
else:
|
||||
raise ValueError(f"unknown event name '{event_name}'")
|
||||
|
||||
class Event:
|
||||
event_name: ClassVar[str]
|
||||
|
||||
@staticmethod
|
||||
def cls_for(event_name: str) -> type[Event]:
|
||||
event_cls = _CONCRETE_EVENT_CLASSES.get(event_name)
|
||||
if event_cls is not None:
|
||||
return event_cls
|
||||
else:
|
||||
raise ValueError(f"unknown event name '{event_name}'")
|
||||
|
||||
|
||||
# The user starts adding items to a list and then hits save.
|
||||
# Ruff should trim the empty lines
|
||||
a = [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
]
|
||||
|
||||
print("Don't format this" )
|
||||
|
||||
|
||||
# The user removed an argument from a call. Ruff should reformat the entire call
|
||||
call(a, b, c, d)
|
||||
|
||||
print("Don't format this" )
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# The user adds a new comment at the end:
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
print("Don't format this" )
|
||||
|
||||
|
||||
def convert_str(value: str) -> str: # Trailing comment
|
||||
"""Return a string as-is."""
|
||||
|
||||
return value # Trailing comment
|
||||
|
||||
|
||||
def test ():
|
||||
pass
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/same_line_body.py
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
def test(<RANGE_START>a ): <RANGE_END>print("body" )
|
||||
|
||||
|
||||
def test2( a): <RANGE_START>print("body" )<RANGE_END>
|
||||
|
||||
|
||||
def test3( a): <RANGE_START>print("body" )
|
||||
|
||||
print("more" )<RANGE_END>
|
||||
print("after" )
|
||||
|
||||
|
||||
# The if header and the print statement together are longer than 100 characters.
|
||||
# The print statement should either be wrapped to fit at the end of the if statement, or be converted to a
|
||||
# suite body
|
||||
if aaaaaaaaaaaa + bbbbbbbbbbbbbb + cccccccccccccccccc + ddd: <RANGE_START>print("aaaa long body, should wrap or be intented" )<RANGE_END>
|
||||
|
||||
# This print statement is too-long even when intented. It should be wrapped
|
||||
if aaaaaaaaaaaa + bbbbbbbbbbbbbb + cccccccccccccccccc + ddd: <RANGE_START>print("aaaa long body, should wrap or be intented", "more content to make it exceed the 88 chars limit")<RANGE_END>
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
def test(a):
|
||||
print("body")
|
||||
|
||||
|
||||
def test2( a):
|
||||
print("body")
|
||||
|
||||
|
||||
def test3( a):
|
||||
print("body")
|
||||
|
||||
|
||||
print("more")
|
||||
print("after" )
|
||||
|
||||
|
||||
# The if header and the print statement together are longer than 100 characters.
|
||||
# The print statement should either be wrapped to fit at the end of the if statement, or be converted to a
|
||||
# suite body
|
||||
if aaaaaaaaaaaa + bbbbbbbbbbbbbb + cccccccccccccccccc + ddd:
|
||||
print("aaaa long body, should wrap or be intented")
|
||||
|
||||
# This print statement is too-long even when intented. It should be wrapped
|
||||
if aaaaaaaaaaaa + bbbbbbbbbbbbbb + cccccccccccccccccc + ddd:
|
||||
print(
|
||||
"aaaa long body, should wrap or be intented",
|
||||
"more content to make it exceed the 88 chars limit",
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/stub.pyi
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
# Don't collapse the ellipsis if only formatting the ellipsis line.
|
||||
class Test:
|
||||
<RANGE_START>...<RANGE_END>
|
||||
|
||||
class Test2: <RANGE_START>pass<RANGE_END>
|
||||
|
||||
class Test3: <RANGE_START>...<RANGE_END>
|
||||
|
||||
class Test4:
|
||||
# leading comment
|
||||
<RANGE_START>...<RANGE_END>
|
||||
# trailing comment
|
||||
|
||||
|
||||
class Test4:
|
||||
<RANGE_START> ...<RANGE_END>
|
||||
```
|
||||
|
||||
## Outputs
|
||||
### Output 1
|
||||
```
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
line-ending = LineFeed
|
||||
magic-trailing-comma = Respect
|
||||
docstring-code = Disabled
|
||||
docstring-code-line-width = "dynamic"
|
||||
preview = Enabled
|
||||
target_version = Py38
|
||||
source_type = Stub
|
||||
```
|
||||
|
||||
```python
|
||||
# Don't collapse the ellipsis if only formatting the ellipsis line.
|
||||
class Test:
|
||||
...
|
||||
|
||||
class Test2:
|
||||
pass
|
||||
|
||||
class Test3: ...
|
||||
|
||||
class Test4:
|
||||
# leading comment
|
||||
...
|
||||
# trailing comment
|
||||
|
||||
|
||||
class Test4: ...
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/trailing_comments.py
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
def test1 ():
|
||||
print("hello" )
|
||||
|
||||
<RANGE_START>1 + 2<RANGE_END> # trailing comment
|
||||
print ("world" )
|
||||
|
||||
def test2 ():
|
||||
print("hello" )
|
||||
# FIXME: For some reason the trailing comment here gets not formatted
|
||||
# but is correctly formatted above
|
||||
<RANGE_START>1 + 2 # trailing comment<RANGE_END>
|
||||
print ("world" )
|
||||
|
||||
def test3 ():
|
||||
print("hellO" )
|
||||
|
||||
<RANGE_START>1 + 2 # trailing comment
|
||||
|
||||
# trailing section comment
|
||||
<RANGE_END>
|
||||
|
||||
def test3 ():
|
||||
print("hellO" )
|
||||
|
||||
<RANGE_START>1 + 2 # trailing comment
|
||||
print("more" ) # trailing comment 2
|
||||
# trailing section comment
|
||||
<RANGE_END>
|
||||
|
||||
print( "world" )
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
def test1 ():
|
||||
print("hello" )
|
||||
|
||||
1 + 2 # trailing comment
|
||||
print ("world" )
|
||||
|
||||
def test2 ():
|
||||
print("hello" )
|
||||
# FIXME: For some reason the trailing comment here gets not formatted
|
||||
# but is correctly formatted above
|
||||
1 + 2 # trailing comment
|
||||
print ("world" )
|
||||
|
||||
def test3 ():
|
||||
print("hellO" )
|
||||
|
||||
1 + 2 # trailing comment
|
||||
|
||||
# trailing section comment
|
||||
|
||||
|
||||
def test3 ():
|
||||
print("hellO" )
|
||||
|
||||
1 + 2 # trailing comment
|
||||
print("more") # trailing comment 2
|
||||
# trailing section comment
|
||||
|
||||
|
||||
print( "world" )
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/whitespace_only_range.py
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
def test():
|
||||
pass <RANGE_START>
|
||||
|
||||
<RANGE_END>
|
||||
|
||||
def test_formatted(): pass
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
def test():
|
||||
pass
|
||||
|
||||
|
||||
def test_formatted(): pass
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
use std::fmt;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::num::NonZeroUsize;
|
||||
use std::num::{NonZeroUsize, ParseIntError};
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
|
@ -325,6 +326,13 @@ const fn unwrap<T: Copy>(option: Option<T>) -> T {
|
|||
}
|
||||
}
|
||||
|
||||
impl FromStr for OneIndexed {
|
||||
type Err = ParseIntError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(OneIndexed(NonZeroUsize::from_str(s)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_text_size::TextSize;
|
||||
|
|
|
@ -4,6 +4,7 @@ use js_sys::Error;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use ruff_formatter::printer::SourceMapGeneration;
|
||||
use ruff_formatter::{FormatResult, Formatted, IndentStyle};
|
||||
use ruff_linter::directives;
|
||||
use ruff_linter::line_width::{IndentWidth, LineLength};
|
||||
|
@ -292,7 +293,8 @@ impl<'a> ParsedModule<'a> {
|
|||
// TODO(konstin): Add an options for py/pyi to the UI (2/2)
|
||||
let options = settings
|
||||
.formatter
|
||||
.to_format_options(PySourceType::default(), self.source_code);
|
||||
.to_format_options(PySourceType::default(), self.source_code)
|
||||
.with_source_map_generation(SourceMapGeneration::Enabled);
|
||||
|
||||
format_module_ast(
|
||||
&self.module,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue