mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-28 21:05:08 +00:00
Normalize implicit concatenated f-string quotes per part (#13539)
This commit is contained in:
parent
42fcbef876
commit
fc661e193a
11 changed files with 171 additions and 62 deletions
|
@ -1,10 +1,12 @@
|
|||
use crate::prelude::*;
|
||||
use crate::preview::{
|
||||
is_f_string_formatting_enabled, is_f_string_implicit_concatenated_string_literal_quotes_enabled,
|
||||
};
|
||||
use crate::string::{Quoting, StringNormalizer, StringQuotes};
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::{AnyStringFlags, FString, StringFlags};
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_f_string_formatting_enabled;
|
||||
use crate::string::{Quoting, StringNormalizer, StringQuotes};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use super::f_string_element::FormatFStringElement;
|
||||
|
||||
|
@ -29,8 +31,17 @@ impl Format<PyFormatContext<'_>> for FormatFString<'_> {
|
|||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
let locator = f.context().locator();
|
||||
|
||||
// If the preview style is enabled, make the decision on what quotes to use locally for each
|
||||
// f-string instead of globally for the entire f-string expression.
|
||||
let quoting =
|
||||
if is_f_string_implicit_concatenated_string_literal_quotes_enabled(f.context()) {
|
||||
f_string_quoting(self.value, &locator)
|
||||
} else {
|
||||
self.quoting
|
||||
};
|
||||
|
||||
let normalizer = StringNormalizer::from_context(f.context())
|
||||
.with_quoting(self.quoting)
|
||||
.with_quoting(quoting)
|
||||
.with_preferred_quote_style(f.options().quote_style());
|
||||
|
||||
// If f-string formatting is disabled (not in preview), then we will
|
||||
|
@ -140,3 +151,20 @@ impl FStringLayout {
|
|||
matches!(self, FStringLayout::Multiline)
|
||||
}
|
||||
}
|
||||
|
||||
fn f_string_quoting(f_string: &FString, locator: &Locator) -> Quoting {
|
||||
let triple_quoted = f_string.flags.is_triple_quoted();
|
||||
|
||||
if f_string.elements.expressions().any(|expression| {
|
||||
let string_content = locator.slice(expression.range());
|
||||
if triple_quoted {
|
||||
string_content.contains(r#"""""#) || string_content.contains("'''")
|
||||
} else {
|
||||
string_content.contains(['"', '\''])
|
||||
}
|
||||
}) {
|
||||
Quoting::Preserve
|
||||
} else {
|
||||
Quoting::CanChange
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use ruff_python_ast::FStringPart;
|
||||
|
||||
use crate::other::f_string::FormatFString;
|
||||
use crate::other::string_literal::{FormatStringLiteral, StringLiteralKind};
|
||||
use crate::other::string_literal::StringLiteralKind;
|
||||
use crate::prelude::*;
|
||||
use crate::string::Quoting;
|
||||
|
||||
|
@ -25,14 +25,13 @@ impl<'a> FormatFStringPart<'a> {
|
|||
impl Format<PyFormatContext<'_>> for FormatFStringPart<'_> {
|
||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
match self.part {
|
||||
FStringPart::Literal(string_literal) => FormatStringLiteral::new(
|
||||
string_literal,
|
||||
// If an f-string part is a string literal, the f-string is always
|
||||
// implicitly concatenated e.g., `"foo" f"bar {x}"`. A standalone
|
||||
// string literal would be a string expression, not an f-string.
|
||||
StringLiteralKind::InImplicitlyConcatenatedFString(self.quoting),
|
||||
)
|
||||
.fmt(f),
|
||||
#[allow(deprecated)]
|
||||
FStringPart::Literal(string_literal) => string_literal
|
||||
.format()
|
||||
.with_options(StringLiteralKind::InImplicitlyConcatenatedFString(
|
||||
self.quoting,
|
||||
))
|
||||
.fmt(f),
|
||||
FStringPart::FString(f_string) => FormatFString::new(f_string, self.quoting).fmt(f),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,28 @@
|
|||
use ruff_formatter::FormatRuleWithOptions;
|
||||
use ruff_python_ast::StringLiteral;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_f_string_implicit_concatenated_string_literal_quotes_enabled;
|
||||
use crate::string::{docstring, Quoting, StringNormalizer};
|
||||
use crate::QuoteStyle;
|
||||
|
||||
pub(crate) struct FormatStringLiteral<'a> {
|
||||
value: &'a StringLiteral,
|
||||
#[derive(Default)]
|
||||
pub struct FormatStringLiteral {
|
||||
layout: StringLiteralKind,
|
||||
}
|
||||
|
||||
impl<'a> FormatStringLiteral<'a> {
|
||||
pub(crate) fn new(value: &'a StringLiteral, layout: StringLiteralKind) -> Self {
|
||||
Self { value, layout }
|
||||
impl FormatRuleWithOptions<StringLiteral, PyFormatContext<'_>> for FormatStringLiteral {
|
||||
type Options = StringLiteralKind;
|
||||
|
||||
fn with_options(mut self, layout: StringLiteralKind) -> Self {
|
||||
self.layout = layout;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind of a string literal.
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub(crate) enum StringLiteralKind {
|
||||
pub enum StringLiteralKind {
|
||||
/// A normal string literal e.g., `"foo"`.
|
||||
#[default]
|
||||
String,
|
||||
|
@ -26,6 +31,8 @@ pub(crate) enum StringLiteralKind {
|
|||
/// A string literal that is implicitly concatenated with an f-string. This
|
||||
/// makes the overall expression an f-string whose quoting detection comes
|
||||
/// from the parent node (f-string expression).
|
||||
#[deprecated]
|
||||
#[allow(private_interfaces)]
|
||||
InImplicitlyConcatenatedFString(Quoting),
|
||||
}
|
||||
|
||||
|
@ -36,16 +43,28 @@ impl StringLiteralKind {
|
|||
}
|
||||
|
||||
/// Returns the quoting to be used for this string literal.
|
||||
fn quoting(self) -> Quoting {
|
||||
fn quoting(self, context: &PyFormatContext) -> Quoting {
|
||||
match self {
|
||||
StringLiteralKind::String | StringLiteralKind::Docstring => Quoting::CanChange,
|
||||
StringLiteralKind::InImplicitlyConcatenatedFString(quoting) => quoting,
|
||||
#[allow(deprecated)]
|
||||
StringLiteralKind::InImplicitlyConcatenatedFString(quoting) => {
|
||||
// Allow string literals to pick the "optimal" quote character
|
||||
// even if any other fstring in the implicit concatenation uses an expression
|
||||
// containing a quote character.
|
||||
// TODO: Remove StringLiteralKind::InImplicitlyConcatenatedFString when promoting
|
||||
// this style to stable and remove the layout from `AnyStringPart::String`.
|
||||
if is_f_string_implicit_concatenated_string_literal_quotes_enabled(context) {
|
||||
Quoting::CanChange
|
||||
} else {
|
||||
quoting
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for FormatStringLiteral<'_> {
|
||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
impl FormatNodeRule<StringLiteral> for FormatStringLiteral {
|
||||
fn fmt_fields(&self, item: &StringLiteral, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
let quote_style = f.options().quote_style();
|
||||
let quote_style = if self.layout.is_docstring() && !quote_style.is_preserve() {
|
||||
// Per PEP 8 and PEP 257, always prefer double quotes for docstrings,
|
||||
|
@ -56,9 +75,9 @@ impl Format<PyFormatContext<'_>> for FormatStringLiteral<'_> {
|
|||
};
|
||||
|
||||
let normalized = StringNormalizer::from_context(f.context())
|
||||
.with_quoting(self.layout.quoting())
|
||||
.with_quoting(self.layout.quoting(f.context()))
|
||||
.with_preferred_quote_style(quote_style)
|
||||
.normalize(self.value.into());
|
||||
.normalize(item.into());
|
||||
|
||||
if self.layout.is_docstring() {
|
||||
docstring::format(&normalized, f)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue