Skip to content

Commit c71ff7e

Browse files
Avoid printing continuations within import identifiers (#7744)
## Summary It turns out that _some_ identifiers can contain newlines -- specifically, dot-delimited import identifiers, like: ```python import foo\ .bar ``` At present, we print all identifiers verbatim, which causes us to retain the `\` in the formatted output. This also leads to violating some debug assertions (see the linked issue, though that's a symptom of this formatting failure). This PR adds detection for import identifiers that contain newlines, and formats them via `text` (slow) rather than `source_code_slice` (fast) in those cases. Closes #7734. ## Test Plan `cargo test`
1 parent 0df2737 commit c71ff7e

File tree

5 files changed

+63
-2
lines changed

5 files changed

+63
-2
lines changed

crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import.py

+7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa
33
from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as dfgsdfgsd, aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as sdkjflsdjlahlfd
44

5+
# Continuations.
6+
import foo\
7+
.bar
8+
9+
from foo\
10+
.bar import baz
11+
512
# At the top-level, force one empty line after an import, but allow up to two empty
613
# lines.
714
import os

crates/ruff_python_formatter/src/other/alias.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use ruff_formatter::write;
22
use ruff_python_ast::Alias;
33

4+
use crate::other::identifier::DotDelimitedIdentifier;
45
use crate::prelude::*;
56

67
#[derive(Default)]
@@ -13,7 +14,7 @@ impl FormatNodeRule<Alias> for FormatAlias {
1314
name,
1415
asname,
1516
} = item;
16-
name.format().fmt(f)?;
17+
DotDelimitedIdentifier::new(name).fmt(f)?;
1718
if let Some(asname) = asname {
1819
write!(f, [space(), token("as"), space(), asname.format()])?;
1920
}

crates/ruff_python_formatter/src/other/identifier.rs

+40
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,43 @@ impl<'ast> IntoFormat<PyFormatContext<'ast>> for Identifier {
2727
FormatOwnedWithRule::new(self, FormatIdentifier)
2828
}
2929
}
30+
31+
/// A formatter for a dot-delimited identifier, as seen in import statements:
32+
/// ```python
33+
/// import foo.bar
34+
/// ```
35+
///
36+
/// Dot-delimited identifiers can contain newlines via continuations (backslashes) after the
37+
/// dot-delimited segment, as in:
38+
/// ```python
39+
/// import foo\
40+
/// .bar
41+
/// ```
42+
///
43+
/// While identifiers can typically be formatted via verbatim source code slices, dot-delimited
44+
/// identifiers with newlines must be formatted via `text`. This struct implements both the fast
45+
/// and slow paths for such identifiers.
46+
#[derive(Debug)]
47+
pub(crate) struct DotDelimitedIdentifier<'a>(&'a Identifier);
48+
49+
impl<'a> DotDelimitedIdentifier<'a> {
50+
pub(crate) fn new(identifier: &'a Identifier) -> Self {
51+
Self(identifier)
52+
}
53+
}
54+
55+
impl Format<PyFormatContext<'_>> for DotDelimitedIdentifier<'_> {
56+
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
57+
// An import identifier can contain newlines by inserting continuations (backslashes) after
58+
// a dot-delimited segment, as in:
59+
// ```python
60+
// import foo\
61+
// .bar
62+
// ```
63+
if memchr::memchr(b'\\', f.context().source()[self.0.range()].as_bytes()).is_some() {
64+
text(self.0.as_str(), Some(self.0.start())).fmt(f)
65+
} else {
66+
source_text_slice(self.0.range()).fmt(f)
67+
}
68+
}
69+
}

crates/ruff_python_formatter/src/statement/stmt_import_from.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use ruff_text_size::Ranged;
66
use crate::builders::{parenthesize_if_expands, PyFormatterExtensions, TrailingComma};
77
use crate::comments::{SourceComment, SuppressionKind};
88
use crate::expression::parentheses::parenthesized;
9+
use crate::other::identifier::DotDelimitedIdentifier;
910
use crate::prelude::*;
1011

1112
#[derive(Default)]
@@ -31,7 +32,7 @@ impl FormatNodeRule<StmtImportFrom> for FormatStmtImportFrom {
3132
}
3233
Ok(())
3334
}),
34-
module.as_ref().map(AsFormat::format),
35+
module.as_ref().map(DotDelimitedIdentifier::new),
3536
space(),
3637
token("import"),
3738
space(),

crates/ruff_python_formatter/tests/snapshots/format@statement__import.py.snap

+12
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjf
88
from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa
99
from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as dfgsdfgsd, aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as sdkjflsdjlahlfd
1010
11+
# Continuations.
12+
import foo\
13+
.bar
14+
15+
from foo\
16+
.bar import baz
17+
1118
# At the top-level, force one empty line after an import, but allow up to two empty
1219
# lines.
1320
import os
@@ -98,6 +105,11 @@ from a import (
98105
aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as sdkjflsdjlahlfd,
99106
)
100107
108+
# Continuations.
109+
import foo.bar
110+
111+
from foo.bar import baz
112+
101113
# At the top-level, force one empty line after an import, but allow up to two empty
102114
# lines.
103115
import os

0 commit comments

Comments
 (0)