Skip to content

Commit 22cb7d4

Browse files
committed
Unify StringPart and AnyStringPart
1 parent eda9a72 commit 22cb7d4

File tree

4 files changed

+97
-117
lines changed

4 files changed

+97
-117
lines changed

crates/ruff_python_formatter/src/expression/expr_f_string.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,10 @@ impl FormatNodeRule<ExprFString> for FormatExprFString {
1717
let ExprFString { value, .. } = item;
1818

1919
match value.as_slice() {
20-
[f_string_part] => FormatFStringPart::new(
21-
f_string_part,
22-
f_string_quoting(item, f.context().locator()),
23-
)
24-
.fmt(f),
20+
[f_string_part] => {
21+
FormatFStringPart::new(f_string_part, f_string_quoting(item, f.context().locator()))
22+
.fmt(f)
23+
}
2524
_ => in_parentheses_only_group(&FormatImplicitConcatenatedString::new(item)).fmt(f),
2625
}
2726
}

crates/ruff_python_formatter/src/string/any.rs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,8 @@ impl FusedIterator for AnyStringPartsIter<'_> {}
165165
/// string. This could be either a string, bytes or f-string.
166166
///
167167
/// This is constructed from the [`AnyString::parts`] method on [`AnyString`].
168-
#[derive(Clone, Debug)]
169-
pub(super) enum AnyStringPart<'a> {
168+
#[derive(Clone, Copy, Debug)]
169+
pub(crate) enum AnyStringPart<'a> {
170170
String(&'a ast::StringLiteral),
171171
Bytes(&'a ast::BytesLiteral),
172172
FString(&'a ast::FString),
@@ -180,6 +180,15 @@ impl AnyStringPart<'_> {
180180
Self::FString(part) => part.flags.into(),
181181
}
182182
}
183+
184+
/// Returns the range of the string's content in the source (minus prefix and quotes).
185+
pub(super) fn content_range(self) -> TextRange {
186+
let kind = self.flags();
187+
TextRange::new(
188+
self.start() + kind.opener_len(),
189+
self.end() - kind.closer_len(),
190+
)
191+
}
183192
}
184193

185194
impl<'a> From<&AnyStringPart<'a>> for AnyNodeRef<'a> {
@@ -201,3 +210,21 @@ impl Ranged for AnyStringPart<'_> {
201210
}
202211
}
203212
}
213+
214+
impl<'a> From<&'a ast::StringLiteral> for AnyStringPart<'a> {
215+
fn from(value: &'a ast::StringLiteral) -> Self {
216+
Self::String(value)
217+
}
218+
}
219+
220+
impl<'a> From<&'a ast::BytesLiteral> for AnyStringPart<'a> {
221+
fn from(value: &'a ast::BytesLiteral) -> Self {
222+
Self::Bytes(value)
223+
}
224+
}
225+
226+
impl<'a> From<&'a ast::FString> for AnyStringPart<'a> {
227+
fn from(value: &'a ast::FString) -> Self {
228+
Self::FString(value)
229+
}
230+
}

crates/ruff_python_formatter/src/string/mod.rs

Lines changed: 16 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@ pub(crate) use normalize::{normalize_string, NormalizedString, StringNormalizer}
33
use ruff_formatter::format_args;
44
use ruff_python_ast::str::Quote;
55
use ruff_python_ast::{
6-
self as ast,
76
str_prefix::{AnyStringPrefix, StringLiteralPrefix},
87
AnyStringFlags, StringFlags,
98
};
10-
use ruff_text_size::{Ranged, TextRange};
9+
use ruff_text_size::Ranged;
1110

1211
use crate::comments::{leading_comments, trailing_comments};
1312
use crate::expression::parentheses::in_parentheses_only_soft_line_break_or_space;
@@ -42,7 +41,7 @@ impl<'a> FormatImplicitConcatenatedString<'a> {
4241
}
4342
}
4443

45-
fn merged_prefix(&self, context: &PyFormatContext) -> Option<AnyStringPrefix> {
44+
fn merged_prefix(&self, context: &PyFormatContext) -> Option<Quote> {
4645
// Early exit if it's known that this string can't be joined to avoid
4746
// unnecessary computation of the quote metadata.
4847
if self.string.parts().any(|part| {
@@ -75,6 +74,7 @@ impl<'a> FormatImplicitConcatenatedString<'a> {
7574
}
7675

7776
// The string is either a regular string, f-string, or bytes string.
77+
let normalizer = StringNormalizer::from_context(context);
7878

7979
// TODO: Do we need to respect the quoting?
8080
let mut merged_quotes: Option<QuoteMetadata> = None;
@@ -83,28 +83,24 @@ impl<'a> FormatImplicitConcatenatedString<'a> {
8383
// Possibly run directly on entire string?
8484

8585
for part in self.string.parts() {
86-
let flags = part.flags();
87-
88-
let comments = context.comments().leading_dangling_trailing(&part);
89-
90-
// Can't merge parts with comments.
91-
if comments.has_leading() || comments.has_trailing() {
86+
let Ok(preferred_quote) = Quote::try_from(normalizer.preferred_quote_style(part))
87+
else {
88+
// TOOD: Handle preserve in some way or another
9289
return None;
93-
}
90+
};
9491

95-
// let part_quote_metadata = QuoteMetadata::from_part(part, context);
92+
// Again, this takes a StringPart and not a `AnyStringPart`.
93+
let part_quote_metadata = QuoteMetadata::from_part(part, preferred_quote, context);
9694

97-
// if let Some(merged) = merged_quotes.as_mut() {
98-
// // FIXME: this is not correct.
99-
// *merged = merged.with_prefix(flags.prefix());
100-
// } else {
101-
// merged_flags = Some(part.flags());
102-
// }
95+
if let Some(merged) = merged_quotes.as_mut() {
96+
// FIXME: this is not correct.
97+
*merged = part_quote_metadata.merge(merged)?;
98+
} else {
99+
merged_quotes = Some(part_quote_metadata);
100+
}
103101
}
104102

105-
// merged_flags.map(StringFlags::prefix)
106-
107-
todo!()
103+
Some(merged_quotes?.choose(Quote::Double))
108104
}
109105
}
110106

@@ -214,58 +210,3 @@ impl From<Quote> for QuoteStyle {
214210
}
215211
}
216212
}
217-
218-
#[derive(Debug, Clone, Copy)]
219-
pub(crate) struct StringPart {
220-
flags: AnyStringFlags,
221-
range: TextRange,
222-
}
223-
224-
impl Ranged for StringPart {
225-
fn range(&self) -> TextRange {
226-
self.range
227-
}
228-
}
229-
230-
impl StringPart {
231-
/// Use the `kind()` method to retrieve information about the
232-
fn flags(self) -> AnyStringFlags {
233-
self.flags
234-
}
235-
236-
/// Returns the range of the string's content in the source (minus prefix and quotes).
237-
fn content_range(self) -> TextRange {
238-
let kind = self.flags();
239-
TextRange::new(
240-
self.start() + kind.opener_len(),
241-
self.end() - kind.closer_len(),
242-
)
243-
}
244-
}
245-
246-
impl From<&ast::StringLiteral> for StringPart {
247-
fn from(value: &ast::StringLiteral) -> Self {
248-
Self {
249-
range: value.range,
250-
flags: value.flags.into(),
251-
}
252-
}
253-
}
254-
255-
impl From<&ast::BytesLiteral> for StringPart {
256-
fn from(value: &ast::BytesLiteral) -> Self {
257-
Self {
258-
range: value.range,
259-
flags: value.flags.into(),
260-
}
261-
}
262-
}
263-
264-
impl From<&ast::FString> for StringPart {
265-
fn from(value: &ast::FString) -> Self {
266-
Self {
267-
range: value.range,
268-
flags: value.flags.into(),
269-
}
270-
}
271-
}

crates/ruff_python_formatter/src/string/normalize.rs

Lines changed: 48 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ use ruff_text_size::{Ranged, TextRange};
99
use crate::context::FStringState;
1010
use crate::prelude::*;
1111
use crate::preview::is_f_string_formatting_enabled;
12-
use crate::string::{Quoting, StringPart, StringQuotes};
12+
use crate::string::any::AnyStringPart;
13+
use crate::string::{Quoting, StringQuotes};
1314
use crate::QuoteStyle;
1415

1516
pub(crate) struct StringNormalizer<'a, 'src> {
@@ -37,7 +38,7 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
3738
self
3839
}
3940

40-
fn quoting(&self, string: StringPart) -> Quoting {
41+
fn quoting(&self, string: AnyStringPart) -> Quoting {
4142
match (self.quoting, self.context.f_string_state()) {
4243
(Quoting::Preserve, _) => Quoting::Preserve,
4344

@@ -70,24 +71,21 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
7071
}
7172
}
7273

73-
/// Computes the strings preferred quotes.
74-
pub(crate) fn choose_quotes(&self, string: StringPart) -> QuoteSelection {
75-
let raw_content = self.context.locator().slice(string.content_range());
76-
let first_quote_or_normalized_char_offset = raw_content
77-
.bytes()
78-
.position(|b| matches!(b, b'\\' | b'"' | b'\'' | b'\r' | b'{'));
79-
let string_flags = string.flags();
80-
81-
let new_kind = match self.quoting(string) {
82-
Quoting::Preserve => string_flags,
74+
/// Determines the preferred quote style for `string`.
75+
/// The formatter should use the preferred quote style unless
76+
/// it can't because the string contains the preferred quotes OR
77+
/// it leads to more escaping.
78+
pub(super) fn preferred_quote_style(&self, string: AnyStringPart) -> QuoteStyle {
79+
match self.quoting(string) {
80+
Quoting::Preserve => QuoteStyle::Preserve,
8381
Quoting::CanChange => {
8482
let preferred_quote_style = self
8583
.preferred_quote_style
8684
.unwrap_or(self.context.options().quote_style());
8785

8886
// Per PEP 8, always prefer double quotes for triple-quoted strings.
8987
// Except when using quote-style-preserve.
90-
let preferred_style = if string_flags.is_triple_quoted() {
88+
if string.flags().is_triple_quoted() {
9189
// ... unless we're formatting a code snippet inside a docstring,
9290
// then we specifically want to invert our quote style to avoid
9391
// writing out invalid Python.
@@ -142,26 +140,41 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
142140
}
143141
} else {
144142
preferred_quote_style
145-
};
146-
147-
if let Ok(preferred_quote) = Quote::try_from(preferred_style) {
148-
if let Some(first_quote_or_normalized_char_offset) =
149-
first_quote_or_normalized_char_offset
150-
{
151-
let quote = QuoteMetadata::from_str(
152-
&raw_content[first_quote_or_normalized_char_offset..],
153-
string.flags(),
154-
preferred_quote,
155-
)
156-
.choose(preferred_quote);
157-
string_flags.with_quote_style(quote)
158-
} else {
159-
string_flags.with_quote_style(preferred_quote)
160-
}
161-
} else {
162-
string_flags
163143
}
164144
}
145+
}
146+
}
147+
148+
/// Computes the strings preferred quotes.
149+
pub(crate) fn choose_quotes(&self, string: AnyStringPart) -> QuoteSelection {
150+
let raw_content = self.context.locator().slice(string.content_range());
151+
let first_quote_or_normalized_char_offset = raw_content
152+
.bytes()
153+
.position(|b| matches!(b, b'\\' | b'"' | b'\'' | b'\r' | b'{'));
154+
let string_flags = string.flags();
155+
let preferred_style = self.preferred_quote_style(string);
156+
157+
let new_kind = match (
158+
Quote::try_from(preferred_style),
159+
first_quote_or_normalized_char_offset,
160+
) {
161+
// The string contains no quotes so it's safe to use the preferred quote style
162+
(Ok(preferred_quote), None) => string_flags.with_quote_style(preferred_quote),
163+
164+
// The preferred quote style is single or double quotes, and the string contains a quote or
165+
// another character that may require escaping
166+
(Ok(preferred_quote), Some(first_quote_or_normalized_char_offset)) => {
167+
let quote = QuoteMetadata::from_str(
168+
&raw_content[first_quote_or_normalized_char_offset..],
169+
string.flags(),
170+
preferred_quote,
171+
)
172+
.choose(preferred_quote);
173+
string_flags.with_quote_style(quote)
174+
}
175+
176+
// The preferred quote style is to preserve the quotes, so let's do that.
177+
(Err(_), _) => string_flags,
165178
};
166179

167180
QuoteSelection {
@@ -171,7 +184,7 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
171184
}
172185

173186
/// Computes the strings preferred quotes and normalizes its content.
174-
pub(crate) fn normalize(&self, string: StringPart) -> NormalizedString<'src> {
187+
pub(crate) fn normalize(&self, string: AnyStringPart) -> NormalizedString<'src> {
175188
let raw_content = self.context.locator().slice(string.content_range());
176189
let quote_selection = self.choose_quotes(string);
177190

@@ -224,7 +237,7 @@ pub(crate) struct QuoteMetadata {
224237
/// to choose the quotes for a part.
225238
impl QuoteMetadata {
226239
pub(crate) fn from_part(
227-
part: StringPart,
240+
part: AnyStringPart,
228241
preferred_quote: Quote,
229242
context: &PyFormatContext,
230243
) -> Self {
@@ -248,7 +261,7 @@ impl QuoteMetadata {
248261
}
249262
}
250263

251-
fn choose(&self, preferred_quote: Quote) -> Quote {
264+
pub(super) fn choose(&self, preferred_quote: Quote) -> Quote {
252265
match self.kind {
253266
QuoteMetadataKind::Raw { contains_preferred } => {
254267
if contains_preferred {
@@ -275,7 +288,7 @@ impl QuoteMetadata {
275288
}
276289
}
277290

278-
fn merge(self, other: &QuoteMetadata) -> Option<QuoteMetadata> {
291+
pub(super) fn merge(self, other: &QuoteMetadata) -> Option<QuoteMetadata> {
279292
match (self.kind, other.kind) {
280293
(
281294
QuoteMetadataKind::Regular {

0 commit comments

Comments
 (0)