mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 13:51:37 +00:00
Split string formatting to individual nodes (#9058)
This PR splits the string formatting code in the formatter to be handled by the respective nodes. Previously, the string formatting was done through a single `FormatString` interface. Now, the nodes themselves are responsible for formatting. The following changes were made: 1. Remove `StringLayout::ImplicitStringConcatenationInBinaryLike` and inline the call to `FormatStringContinuation`. After the refactor, the binary like formatting would delegate to `FormatString` which would then delegate to `FormatStringContinuation`. This removes the intermediary steps. 2. Add formatter implementation for `FStringPart` which delegates it to the respective string literal or f-string node. 3. Add `ExprStringLiteralKind` which is either `String` or `Docstring`. If it's a docstring variant, then the string expression would not be implicitly concatenated. This is guaranteed by the `DocstringStmt::try_from_expression` constructor. 4. Add `StringLiteralKind` which is either a `String`, `Docstring` or `InImplicitlyConcatenatedFString`. The last variant is for when the string literal is implicitly concatenated with an f-string (`"foo" f"bar {x}"`). 5. Remove `FormatString`. 6. Extract the f-string quote detection as a standalone function which is public to the crate. This is used to detect the quote to be used for an f-string at the expression level (`ExprFString` or `FormatStringContinuation`). ### Formatter ecosystem result **This PR** | project | similarity index | total files | changed files | |----------------|------------------:|------------------:|------------------:| | cpython | 0.75804 | 1799 | 1648 | | django | 0.99984 | 2772 | 34 | | home-assistant | 0.99955 | 10596 | 214 | | poetry | 0.99905 | 321 | 15 | | transformers | 0.99967 | 2657 | 324 | | twine | 1.00000 | 33 | 0 | | typeshed | 0.99980 | 3669 | 18 | | warehouse | 0.99976 | 654 | 14 | | zulip | 0.99958 | 1459 | 36 | **main** | project | similarity index | total files | changed files | |----------------|------------------:|------------------:|------------------:| | cpython | 0.75804 | 1799 | 1648 | | django | 0.99984 | 2772 | 34 | | home-assistant | 0.99955 | 10596 | 214 | | poetry | 0.99905 | 321 | 15 | | transformers | 0.99967 | 2657 | 324 | | twine | 1.00000 | 33 | 0 | | typeshed | 0.99980 | 3669 | 18 | | warehouse | 0.99976 | 654 | 14 | | zulip | 0.99958 | 1459 | 36 |
This commit is contained in:
parent
28b1aa201b
commit
189e947808
17 changed files with 364 additions and 266 deletions
|
@ -1,12 +1,72 @@
|
|||
use ruff_python_ast::StringLiteral;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::string::{docstring, Quoting, StringPart};
|
||||
use crate::QuoteStyle;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStringLiteral;
|
||||
pub(crate) struct FormatStringLiteral<'a> {
|
||||
value: &'a StringLiteral,
|
||||
layout: StringLiteralKind,
|
||||
}
|
||||
|
||||
impl FormatNodeRule<StringLiteral> for FormatStringLiteral {
|
||||
fn fmt_fields(&self, _item: &StringLiteral, _f: &mut PyFormatter) -> FormatResult<()> {
|
||||
unreachable!("Handled inside of `FormatExprStringLiteral`");
|
||||
impl<'a> FormatStringLiteral<'a> {
|
||||
pub(crate) fn new(value: &'a StringLiteral, layout: StringLiteralKind) -> Self {
|
||||
Self { value, layout }
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind of a string literal.
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub(crate) enum StringLiteralKind {
|
||||
/// A normal string literal e.g., `"foo"`.
|
||||
#[default]
|
||||
String,
|
||||
/// A string literal used as a docstring.
|
||||
Docstring,
|
||||
/// 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).
|
||||
InImplicitlyConcatenatedFString(Quoting),
|
||||
}
|
||||
|
||||
impl StringLiteralKind {
|
||||
/// Checks if this string literal is a docstring.
|
||||
pub(crate) const fn is_docstring(self) -> bool {
|
||||
matches!(self, StringLiteralKind::Docstring)
|
||||
}
|
||||
|
||||
/// Returns the quoting to be used for this string literal.
|
||||
fn quoting(self) -> Quoting {
|
||||
match self {
|
||||
StringLiteralKind::String | StringLiteralKind::Docstring => Quoting::CanChange,
|
||||
StringLiteralKind::InImplicitlyConcatenatedFString(quoting) => quoting,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for FormatStringLiteral<'_> {
|
||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
let locator = f.context().locator();
|
||||
|
||||
let quote_style = if self.layout.is_docstring() {
|
||||
// Per PEP 8 and PEP 257, always prefer double quotes for docstrings
|
||||
QuoteStyle::Double
|
||||
} else {
|
||||
f.options().quote_style()
|
||||
};
|
||||
|
||||
let normalized = StringPart::from_source(self.value.range(), &locator).normalize(
|
||||
self.layout.quoting(),
|
||||
&locator,
|
||||
quote_style,
|
||||
f.context().docstring(),
|
||||
);
|
||||
|
||||
if self.layout.is_docstring() {
|
||||
docstring::format(&normalized, f)
|
||||
} else {
|
||||
normalized.fmt(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue