Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disallow single-line implicit concatenated strings #13928

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,60 @@
a = """\x1F"""
a = """\\x1F"""
a = """\\\x1F"""


##############################################################################
# Implicit concatenated strings
##############################################################################

# In preview, don't collapse implicit concatenated strings that can't be joined into a single string
# and that are multiline in the source.

(
r"aaaaaaaaa"
r"bbbbbbbbbbbbbbbbbbbb"
)

(
r"aaaaaaaaa" r"bbbbbbbbbbbbbbbbbbbb"
"cccccccccccccccccccccc"
)

(
"""aaaaaaaaa"""
"""bbbbbbbbbbbbbbbbbbbb"""
)

(
f"""aaaa{
10}aaaaa"""
fr"""bbbbbbbbbbbbbbbbbbbb"""
)

if (
r"aaaaaaaaa"
r"bbbbbbbbbbbbbbbbbbbb"
) + ["aaaaaaaaaa", "bbbbbbbbbbbbbbbb"]: ...



# In preview, keep implicit concatenated strings on a single line if they fit and are not separate by line breaks in the source
(
r"aaaaaaaaa" r"bbbbbbbbbbbbbbbbbbbb"
)

r"aaaaaaaaa" r"bbbbbbbbbbbbbbbbbbbb"

(
f"aaaa{
10}aaaaa" fr"bbbbbbbbbbbbbbbbbbbb"
)

(
r"""aaaaaaaaa""" r"""bbbbbbbbbbbbbbbbbbbb"""
)

(
f"""aaaa{
10}aaaaa""" fr"""bbbbbbbbbbbbbbbbbbbb"""
)
10 changes: 9 additions & 1 deletion crates/ruff_python_formatter/src/statement/stmt_assign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::preview::is_join_implicit_concatenated_string_enabled;
use crate::statement::trailing_semicolon;
use crate::string::implicit::{
FormatImplicitConcatenatedStringExpanded, FormatImplicitConcatenatedStringFlat,
ImplicitConcatenatedLayout,
};
use crate::{has_skip_comment, prelude::*};

Expand Down Expand Up @@ -375,7 +376,13 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
let f =
&mut WithNodeLevel::new(NodeLevel::Expression(Some(group_id)), f);

write!(f, [FormatImplicitConcatenatedStringExpanded::new(string)])
write!(
f,
[FormatImplicitConcatenatedStringExpanded::new(
string,
ImplicitConcatenatedLayout::MaybeFlat
)]
)
});

// Join the implicit concatenated string if it fits on a single line
Expand Down Expand Up @@ -686,6 +693,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {

FormatImplicitConcatenatedStringExpanded::new(
StringLike::try_from(*value).unwrap(),
ImplicitConcatenatedLayout::MaybeFlat,
)
.fmt(f)
})
Expand Down
42 changes: 36 additions & 6 deletions crates/ruff_python_formatter/src/string/implicit.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use std::borrow::Cow;

use itertools::Itertools;
use ruff_formatter::{format_args, write, FormatContext};
use ruff_python_ast::str::Quote;
use ruff_python_ast::str_prefix::{
Expand All @@ -8,6 +7,7 @@ use ruff_python_ast::str_prefix::{
use ruff_python_ast::{AnyStringFlags, FStringElement, StringFlags, StringLike, StringLikePart};
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextRange};
use std::borrow::Cow;

use crate::comments::{leading_comments, trailing_comments};
use crate::expression::parentheses::in_parentheses_only_soft_line_break_or_space;
Expand Down Expand Up @@ -41,12 +41,20 @@ impl<'a> FormatImplicitConcatenatedString<'a> {

impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedString<'_> {
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
let expanded = FormatImplicitConcatenatedStringExpanded::new(self.string);
let flat = FormatImplicitConcatenatedStringFlat::new(self.string, f.context());
let expanded = FormatImplicitConcatenatedStringExpanded::new(
self.string,
if flat.is_some() {
ImplicitConcatenatedLayout::MaybeFlat
} else {
ImplicitConcatenatedLayout::Multipart
},
);

// If the string can be joined, try joining the implicit concatenated string into a single string
// if it fits on the line. Otherwise, parenthesize the string parts and format each part on its
// own line.
if let Some(flat) = FormatImplicitConcatenatedStringFlat::new(self.string, f.context()) {
if let Some(flat) = flat {
write!(
f,
[if_group_fits_on_line(&flat), if_group_breaks(&expanded)]
Expand All @@ -60,13 +68,14 @@ impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedString<'_> {
/// Formats an implicit concatenated string where parts are separated by a space or line break.
pub(crate) struct FormatImplicitConcatenatedStringExpanded<'a> {
string: StringLike<'a>,
layout: ImplicitConcatenatedLayout,
}

impl<'a> FormatImplicitConcatenatedStringExpanded<'a> {
pub(crate) fn new(string: StringLike<'a>) -> Self {
pub(crate) fn new(string: StringLike<'a>, layout: ImplicitConcatenatedLayout) -> Self {
assert!(string.is_implicit_concatenated());

Self { string }
Self { string, layout }
}
}

Expand All @@ -77,6 +86,19 @@ impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedStringExpanded<'_

let join_implicit_concatenated_string_enabled =
is_join_implicit_concatenated_string_enabled(f.context());

// Keep implicit concatenated strings expanded unless they're already written on a single line.
if matches!(self.layout, ImplicitConcatenatedLayout::Multipart)
&& join_implicit_concatenated_string_enabled
&& self.string.parts().tuple_windows().any(|(a, b)| {
f.context()
.source()
.contains_line_break(TextRange::new(a.end(), b.start()))
})
{
expand_parent().fmt(f)?;
}

let mut joiner = f.join_with(in_parentheses_only_soft_line_break_or_space());

for part in self.string.parts() {
Expand Down Expand Up @@ -108,6 +130,14 @@ impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedStringExpanded<'_
}
}

#[derive(Copy, Clone, Debug)]
pub(crate) enum ImplicitConcatenatedLayout {
/// The string might get joined into a single string if it fits on a single line.
MaybeFlat,
/// The string will remain a multipart string.
Multipart,
}

/// Formats an implicit concatenated string where parts are joined into a single string if possible.
pub(crate) struct FormatImplicitConcatenatedStringFlat<'a> {
string: StringLike<'a>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/long_strings_flag_disabled.py
snapshot_kind: text
---
## Input

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/preview_long_strings.py
snapshot_kind: text
---
## Input

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/preview_long_strings__regression.py
snapshot_kind: text
---
## Input

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py
snapshot_kind: text
---
## Input
```python
Expand Down
Loading
Loading