ruff/crates/ruff_python_formatter/src/other/f_string.rs

144 lines
4.9 KiB
Rust

use ruff_formatter::write;
use ruff_python_ast::{AnyStringFlags, FString};
use ruff_source_file::Locator;
use crate::prelude::*;
use crate::preview::is_f_string_formatting_enabled;
use crate::string::{Quoting, StringNormalizer, StringQuotes};
use super::f_string_element::FormatFStringElement;
/// Formats an f-string which is part of a larger f-string expression.
///
/// For example, this would be used to format the f-string part in `"foo" f"bar {x}"`
/// or the standalone f-string in `f"foo {x} bar"`.
pub(crate) struct FormatFString<'a> {
value: &'a FString,
/// The quoting of an f-string. This is determined by the parent node
/// (f-string expression) and is required to format an f-string correctly.
quoting: Quoting,
}
impl<'a> FormatFString<'a> {
pub(crate) fn new(value: &'a FString, quoting: Quoting) -> Self {
Self { value, quoting }
}
}
impl Format<PyFormatContext<'_>> for FormatFString<'_> {
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
let locator = f.context().locator();
let normalizer = StringNormalizer::from_context(f.context())
.with_quoting(self.quoting)
.with_preferred_quote_style(f.options().quote_style());
// If f-string formatting is disabled (not in preview), then we will
// fall back to the previous behavior of normalizing the f-string.
if !is_f_string_formatting_enabled(f.context()) {
let result = normalizer.normalize(self.value.into(), &locator).fmt(f);
let comments = f.context().comments();
self.value.elements.iter().for_each(|value| {
comments.mark_verbatim_node_comments_formatted(value.into());
// Above method doesn't mark the trailing comments of the f-string elements
// as formatted, so we need to do it manually. For example,
//
// ```python
// f"""foo {
// x:.3f
// # comment
// }"""
// ```
for trailing_comment in comments.trailing(value) {
trailing_comment.mark_formatted();
}
});
return result;
}
let string_kind = normalizer
.choose_quotes(self.value.into(), &locator)
.flags();
let context = FStringContext::new(
string_kind,
FStringLayout::from_f_string(self.value, &locator),
);
// Starting prefix and quote
let quotes = StringQuotes::from(string_kind);
write!(f, [string_kind.prefix(), quotes])?;
f.join()
.entries(
self.value
.elements
.iter()
.map(|element| FormatFStringElement::new(element, context)),
)
.finish()?;
// Ending quote
quotes.fmt(f)
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct FStringContext {
flags: AnyStringFlags,
layout: FStringLayout,
}
impl FStringContext {
const fn new(flags: AnyStringFlags, layout: FStringLayout) -> Self {
Self { flags, layout }
}
pub(crate) fn flags(self) -> AnyStringFlags {
self.flags
}
pub(crate) const fn layout(self) -> FStringLayout {
self.layout
}
}
#[derive(Copy, Clone, Debug)]
pub(crate) enum FStringLayout {
/// Original f-string is flat.
/// Don't break expressions to keep the string flat.
Flat,
/// Original f-string has multiline expressions in the replacement fields.
/// Allow breaking expressions across multiple lines.
Multiline,
}
impl FStringLayout {
fn from_f_string(f_string: &FString, locator: &Locator) -> Self {
// Heuristic: Allow breaking the f-string expressions across multiple lines
// only if there already is at least one multiline expression. This puts the
// control in the hands of the user to decide if they want to break the
// f-string expressions across multiple lines or not. This is similar to
// how Prettier does it for template literals in JavaScript.
//
// If it's single quoted f-string and it contains a multiline expression, then we
// assume that the target version of Python supports it (3.12+). If there are comments
// used in any of the expression of the f-string, then it's always going to be multiline
// and we assume that the target version of Python supports it (3.12+).
//
// Reference: https://prettier.io/docs/en/next/rationale.html#template-literals
if f_string
.expressions()
.any(|expr| memchr::memchr2(b'\n', b'\r', locator.slice(expr).as_bytes()).is_some())
{
Self::Multiline
} else {
Self::Flat
}
}
pub(crate) const fn is_multiline(self) -> bool {
matches!(self, FStringLayout::Multiline)
}
}