mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-09 21:28:04 +00:00
parent
a95deec00f
commit
424b720c19
62 changed files with 1799 additions and 3890 deletions
|
@ -1,15 +1,14 @@
|
|||
use ruff_python_ast::{AnyNodeRef, ExprFString, StringLike};
|
||||
use ruff_text_size::TextSlice;
|
||||
|
||||
use crate::expression::parentheses::{
|
||||
in_parentheses_only_group, NeedsParentheses, OptionalParentheses,
|
||||
};
|
||||
use crate::other::f_string::{FStringLayout, FormatFString};
|
||||
use crate::other::f_string::FStringLayout;
|
||||
use crate::prelude::*;
|
||||
use crate::string::implicit::{
|
||||
FormatImplicitConcatenatedString, FormatImplicitConcatenatedStringFlat,
|
||||
};
|
||||
use crate::string::{Quoting, StringLikeExtensions};
|
||||
use crate::string::StringLikeExtensions;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatExprFString;
|
||||
|
@ -23,7 +22,7 @@ impl FormatNodeRule<ExprFString> for FormatExprFString {
|
|||
// [`ruff_python_ast::FStringValue::single`] constructor.
|
||||
let f_string = f_string_part.as_f_string().unwrap();
|
||||
|
||||
FormatFString::new(f_string, f_string_quoting(item, f.context().source())).fmt(f)
|
||||
f_string.format().fmt(f)
|
||||
} else {
|
||||
// Always join fstrings that aren't parenthesized and thus, are always on a single line.
|
||||
if !f.context().node_level().is_parenthesized() {
|
||||
|
@ -58,28 +57,3 @@ impl NeedsParentheses for ExprFString {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn f_string_quoting(f_string: &ExprFString, source: &str) -> Quoting {
|
||||
let unprefixed = source
|
||||
.slice(f_string)
|
||||
.trim_start_matches(|c| c != '"' && c != '\'');
|
||||
let triple_quoted = unprefixed.starts_with(r#"""""#) || unprefixed.starts_with(r"'''");
|
||||
|
||||
if f_string
|
||||
.value
|
||||
.elements()
|
||||
.filter_map(|element| element.as_expression())
|
||||
.any(|expression| {
|
||||
let string_content = source.slice(expression);
|
||||
if triple_quoted {
|
||||
string_content.contains(r#"""""#) || string_content.contains("'''")
|
||||
} else {
|
||||
string_content.contains(['"', '\''])
|
||||
}
|
||||
})
|
||||
{
|
||||
Quoting::Preserve
|
||||
} else {
|
||||
Quoting::CanChange
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ use crate::expression::parentheses::{
|
|||
};
|
||||
use crate::expression::CallChainLayout;
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatExprSubscript {
|
||||
|
@ -108,13 +107,14 @@ impl NeedsParentheses for ExprSubscript {
|
|||
if function.returns.as_deref().is_some_and(|returns| {
|
||||
AnyNodeRef::ptr_eq(returns.into(), self.into())
|
||||
}) {
|
||||
if is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled(context) &&
|
||||
function.parameters.is_empty() && !context.comments().has(&*function.parameters) {
|
||||
if function.parameters.is_empty()
|
||||
&& !context.comments().has(&*function.parameters)
|
||||
{
|
||||
// Apply the `optional_parentheses` layout when the subscript
|
||||
// is in a return type position of a function without parameters.
|
||||
// This ensures the subscript is parenthesized if it has a very
|
||||
// long name that goes over the line length limit.
|
||||
return OptionalParentheses::Multiline
|
||||
return OptionalParentheses::Multiline;
|
||||
}
|
||||
|
||||
// Don't use the best fitting layout for return type annotation because it results in the
|
||||
|
|
|
@ -19,10 +19,7 @@ use crate::expression::parentheses::{
|
|||
OptionalParentheses, Parentheses, Parenthesize,
|
||||
};
|
||||
use crate::prelude::*;
|
||||
use crate::preview::{
|
||||
is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled,
|
||||
is_f_string_formatting_enabled, is_hug_parens_with_braces_and_square_brackets_enabled,
|
||||
};
|
||||
use crate::preview::is_hug_parens_with_braces_and_square_brackets_enabled;
|
||||
|
||||
mod binary_like;
|
||||
pub(crate) mod expr_attribute;
|
||||
|
@ -388,18 +385,12 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
|
|||
// is parenthesized. Unless, it's the `Parenthesize::IfBreaksParenthesizedNested` layout
|
||||
// where parenthesizing nested `maybe_parenthesized_expression` is explicitly desired.
|
||||
_ if f.context().node_level().is_parenthesized() => {
|
||||
if !is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled(
|
||||
f.context(),
|
||||
) {
|
||||
OptionalParentheses::Never
|
||||
} else if matches!(parenthesize, Parenthesize::IfBreaksParenthesizedNested) {
|
||||
return parenthesize_if_expands(
|
||||
&expression.format().with_options(Parentheses::Never),
|
||||
)
|
||||
.with_indent(!is_expression_huggable(expression, f.context()))
|
||||
.fmt(f);
|
||||
return if matches!(parenthesize, Parenthesize::IfBreaksParenthesizedNested) {
|
||||
parenthesize_if_expands(&expression.format().with_options(Parentheses::Never))
|
||||
.with_indent(!is_expression_huggable(expression, f.context()))
|
||||
.fmt(f)
|
||||
} else {
|
||||
return expression.format().with_options(Parentheses::Never).fmt(f);
|
||||
expression.format().with_options(Parentheses::Never).fmt(f)
|
||||
}
|
||||
}
|
||||
needs_parentheses => needs_parentheses,
|
||||
|
@ -409,13 +400,12 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
|
|||
|
||||
match needs_parentheses {
|
||||
OptionalParentheses::Multiline => match parenthesize {
|
||||
Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested if !is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled(f.context()) => {
|
||||
parenthesize_if_expands(&unparenthesized).fmt(f)
|
||||
}
|
||||
|
||||
Parenthesize::IfRequired => unparenthesized.fmt(f),
|
||||
|
||||
Parenthesize::Optional | Parenthesize::IfBreaks | Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested => {
|
||||
Parenthesize::Optional
|
||||
| Parenthesize::IfBreaks
|
||||
| Parenthesize::IfBreaksParenthesized
|
||||
| Parenthesize::IfBreaksParenthesizedNested => {
|
||||
if can_omit_optional_parentheses(expression, f.context()) {
|
||||
optional_parentheses(&unparenthesized).fmt(f)
|
||||
} else {
|
||||
|
@ -424,9 +414,6 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
|
|||
}
|
||||
},
|
||||
OptionalParentheses::BestFit => match parenthesize {
|
||||
Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested if !is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled(f.context()) =>
|
||||
parenthesize_if_expands(&unparenthesized).fmt(f),
|
||||
|
||||
Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested => {
|
||||
// Can-omit layout is relevant for `"abcd".call`. We don't want to add unnecessary
|
||||
// parentheses in this case.
|
||||
|
@ -454,15 +441,11 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
|
|||
}
|
||||
},
|
||||
OptionalParentheses::Never => match parenthesize {
|
||||
Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested if !is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled(f.context()) => {
|
||||
parenthesize_if_expands(&unparenthesized)
|
||||
.with_indent(!is_expression_huggable(expression, f.context()))
|
||||
.fmt(f)
|
||||
}
|
||||
|
||||
Parenthesize::Optional | Parenthesize::IfBreaks | Parenthesize::IfRequired | Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested => {
|
||||
unparenthesized.fmt(f)
|
||||
}
|
||||
Parenthesize::Optional
|
||||
| Parenthesize::IfBreaks
|
||||
| Parenthesize::IfRequired
|
||||
| Parenthesize::IfBreaksParenthesized
|
||||
| Parenthesize::IfBreaksParenthesizedNested => unparenthesized.fmt(f),
|
||||
},
|
||||
|
||||
OptionalParentheses::Always => {
|
||||
|
@ -766,32 +749,6 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> {
|
|||
return;
|
||||
}
|
||||
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. })
|
||||
if value.is_implicit_concatenated() =>
|
||||
{
|
||||
if !is_f_string_formatting_enabled(self.context) {
|
||||
self.update_max_precedence(OperatorPrecedence::String);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. })
|
||||
if value.is_implicit_concatenated() =>
|
||||
{
|
||||
if !is_f_string_formatting_enabled(self.context) {
|
||||
self.update_max_precedence(OperatorPrecedence::String);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
Expr::FString(ast::ExprFString { value, .. }) if value.is_implicit_concatenated() => {
|
||||
if !is_f_string_formatting_enabled(self.context) {
|
||||
self.update_max_precedence(OperatorPrecedence::String);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Non terminal nodes that don't have a termination token.
|
||||
Expr::Named(_) | Expr::Generator(_) | Expr::Tuple(_) => {}
|
||||
|
||||
|
@ -1193,8 +1150,6 @@ enum OperatorPrecedence {
|
|||
BitwiseXor,
|
||||
BitwiseOr,
|
||||
Comparator,
|
||||
// Implicit string concatenation
|
||||
String,
|
||||
BooleanOperation,
|
||||
Conditional,
|
||||
}
|
||||
|
|
|
@ -2935,6 +2935,34 @@ impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::TypeParamParamSpec {
|
|||
}
|
||||
}
|
||||
|
||||
impl FormatRule<ast::FString, PyFormatContext<'_>> for crate::other::f_string::FormatFString {
|
||||
#[inline]
|
||||
fn fmt(&self, node: &ast::FString, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
FormatNodeRule::<ast::FString>::fmt(self, node, f)
|
||||
}
|
||||
}
|
||||
impl<'ast> AsFormat<PyFormatContext<'ast>> for ast::FString {
|
||||
type Format<'a> = FormatRefWithRule<
|
||||
'a,
|
||||
ast::FString,
|
||||
crate::other::f_string::FormatFString,
|
||||
PyFormatContext<'ast>,
|
||||
>;
|
||||
fn format(&self) -> Self::Format<'_> {
|
||||
FormatRefWithRule::new(self, crate::other::f_string::FormatFString::default())
|
||||
}
|
||||
}
|
||||
impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::FString {
|
||||
type Format = FormatOwnedWithRule<
|
||||
ast::FString,
|
||||
crate::other::f_string::FormatFString,
|
||||
PyFormatContext<'ast>,
|
||||
>;
|
||||
fn into_format(self) -> Self::Format {
|
||||
FormatOwnedWithRule::new(self, crate::other::f_string::FormatFString::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatRule<ast::StringLiteral, PyFormatContext<'_>>
|
||||
for crate::other::string_literal::FormatStringLiteral
|
||||
{
|
||||
|
|
|
@ -7,7 +7,6 @@ use crate::comments::{leading_comments, trailing_comments};
|
|||
use crate::expression::expr_tuple::TupleParentheses;
|
||||
use crate::expression::parentheses::is_expression_parenthesized;
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_comprehension_leading_expression_comments_same_line_enabled;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatComprehension;
|
||||
|
@ -36,14 +35,12 @@ impl FormatNodeRule<Comprehension> for FormatComprehension {
|
|||
// )
|
||||
// ]
|
||||
// ```
|
||||
let will_be_parenthesized =
|
||||
is_comprehension_leading_expression_comments_same_line_enabled(f.context())
|
||||
&& self.preserve_parentheses
|
||||
&& is_expression_parenthesized(
|
||||
self.expression.into(),
|
||||
f.context().comments().ranges(),
|
||||
f.context().source(),
|
||||
);
|
||||
let will_be_parenthesized = self.preserve_parentheses
|
||||
&& is_expression_parenthesized(
|
||||
self.expression.into(),
|
||||
f.context().comments().ranges(),
|
||||
f.context().source(),
|
||||
);
|
||||
|
||||
if has_leading_comments && !will_be_parenthesized {
|
||||
soft_line_break_or_space().fmt(f)
|
||||
|
|
|
@ -4,8 +4,7 @@ use ruff_source_file::LineRanges;
|
|||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_f_string_formatting_enabled;
|
||||
use crate::string::{Quoting, StringNormalizer, StringQuotes};
|
||||
use crate::string::{StringNormalizer, StringQuotes};
|
||||
|
||||
use super::f_string_element::FormatFStringElement;
|
||||
|
||||
|
@ -13,66 +12,25 @@ use super::f_string_element::FormatFStringElement;
|
|||
///
|
||||
/// 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,
|
||||
}
|
||||
#[derive(Default)]
|
||||
pub struct FormatFString;
|
||||
|
||||
impl<'a> FormatFString<'a> {
|
||||
pub(crate) fn new(value: &'a FString, quoting: Quoting) -> Self {
|
||||
Self { value, quoting }
|
||||
}
|
||||
}
|
||||
impl FormatNodeRule<FString> for FormatFString {
|
||||
fn fmt_fields(&self, item: &FString, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
let normalizer = StringNormalizer::from_context(f.context());
|
||||
|
||||
impl Format<PyFormatContext<'_>> for FormatFString<'_> {
|
||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
// 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_formatting_enabled(f.context()) {
|
||||
Quoting::CanChange
|
||||
} else {
|
||||
self.quoting
|
||||
};
|
||||
|
||||
let normalizer = StringNormalizer::from_context(f.context()).with_quoting(quoting);
|
||||
|
||||
// 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()).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()).flags();
|
||||
let string_kind = normalizer.choose_quotes(item.into()).flags();
|
||||
|
||||
let context = FStringContext::new(
|
||||
string_kind,
|
||||
FStringLayout::from_f_string(self.value, f.context().source()),
|
||||
FStringLayout::from_f_string(item, f.context().source()),
|
||||
);
|
||||
|
||||
// Starting prefix and quote
|
||||
let quotes = StringQuotes::from(string_kind);
|
||||
write!(f, [string_kind.prefix(), quotes])?;
|
||||
|
||||
for element in &self.value.elements {
|
||||
for element in &item.elements {
|
||||
FormatFStringElement::new(element, context).fmt(f)?;
|
||||
}
|
||||
|
||||
|
|
|
@ -61,8 +61,7 @@ impl<'a> FormatFStringLiteralElement<'a> {
|
|||
impl Format<PyFormatContext<'_>> for FormatFStringLiteralElement<'_> {
|
||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
let literal_content = f.context().source().slice(self.element);
|
||||
let normalized =
|
||||
normalize_string(literal_content, 0, self.fstring_flags, false, false, true);
|
||||
let normalized = normalize_string(literal_content, 0, self.fstring_flags, false);
|
||||
match &normalized {
|
||||
Cow::Borrowed(_) => source_text_slice(self.element.range()).fmt(f),
|
||||
Cow::Owned(normalized) => text(normalized).fmt(f),
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
use ruff_formatter::{write, FormatRuleWithOptions};
|
||||
use ruff_python_ast::AstNode;
|
||||
use ruff_formatter::{format_args, write, FormatRuleWithOptions};
|
||||
use ruff_python_ast::MatchCase;
|
||||
|
||||
use crate::builders::parenthesize_if_expands;
|
||||
use crate::expression::maybe_parenthesize_expression;
|
||||
use crate::expression::parentheses::{
|
||||
NeedsParentheses, OptionalParentheses, Parentheses, Parenthesize,
|
||||
};
|
||||
use crate::expression::parentheses::Parenthesize;
|
||||
use crate::pattern::maybe_parenthesize_pattern;
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_match_case_parentheses_enabled;
|
||||
use crate::statement::clause::{clause_body, clause_header, ClauseHeader};
|
||||
use crate::statement::suite::SuiteKind;
|
||||
|
||||
|
@ -39,42 +34,12 @@ impl FormatNodeRule<MatchCase> for FormatMatchCase {
|
|||
let comments = f.context().comments().clone();
|
||||
let dangling_item_comments = comments.dangling(item);
|
||||
|
||||
let format_pattern = format_with(|f| {
|
||||
if is_match_case_parentheses_enabled(f.context()) {
|
||||
maybe_parenthesize_pattern(pattern, item).fmt(f)
|
||||
} else {
|
||||
let has_comments =
|
||||
comments.has_leading(pattern) || comments.has_trailing_own_line(pattern);
|
||||
|
||||
if has_comments {
|
||||
pattern.format().with_options(Parentheses::Always).fmt(f)
|
||||
} else {
|
||||
match pattern.needs_parentheses(item.as_any_node_ref(), f.context()) {
|
||||
OptionalParentheses::Multiline => parenthesize_if_expands(
|
||||
&pattern.format().with_options(Parentheses::Never),
|
||||
)
|
||||
.fmt(f),
|
||||
OptionalParentheses::Always => {
|
||||
pattern.format().with_options(Parentheses::Always).fmt(f)
|
||||
}
|
||||
OptionalParentheses::Never | OptionalParentheses::BestFit => {
|
||||
pattern.format().with_options(Parentheses::Never).fmt(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let format_guard = guard.as_deref().map(|guard| {
|
||||
format_with(|f| {
|
||||
write!(f, [space(), token("if"), space()])?;
|
||||
|
||||
if is_match_case_parentheses_enabled(f.context()) {
|
||||
maybe_parenthesize_expression(guard, item, Parenthesize::IfBreaksParenthesized)
|
||||
.fmt(f)
|
||||
} else {
|
||||
guard.format().fmt(f)
|
||||
}
|
||||
maybe_parenthesize_expression(guard, item, Parenthesize::IfBreaksParenthesized)
|
||||
.fmt(f)
|
||||
})
|
||||
});
|
||||
|
||||
|
@ -84,9 +49,12 @@ impl FormatNodeRule<MatchCase> for FormatMatchCase {
|
|||
clause_header(
|
||||
ClauseHeader::MatchCase(item),
|
||||
dangling_item_comments,
|
||||
&format_with(|f| {
|
||||
write!(f, [token("case"), space(), format_pattern, format_guard])
|
||||
}),
|
||||
&format_args![
|
||||
token("case"),
|
||||
space(),
|
||||
maybe_parenthesize_pattern(pattern, item),
|
||||
format_guard
|
||||
],
|
||||
),
|
||||
clause_body(
|
||||
body,
|
||||
|
|
|
@ -2,8 +2,7 @@ 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::string::{docstring, StringNormalizer};
|
||||
use crate::QuoteStyle;
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -28,12 +27,6 @@ pub enum StringLiteralKind {
|
|||
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).
|
||||
#[deprecated]
|
||||
#[allow(private_interfaces)]
|
||||
InImplicitlyConcatenatedFString(Quoting),
|
||||
}
|
||||
|
||||
impl StringLiteralKind {
|
||||
|
@ -41,26 +34,6 @@ impl StringLiteralKind {
|
|||
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, context: &PyFormatContext) -> Quoting {
|
||||
match self {
|
||||
StringLiteralKind::String | StringLiteralKind::Docstring => Quoting::CanChange,
|
||||
#[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 FormatNodeRule<StringLiteral> for FormatStringLiteral {
|
||||
|
@ -75,7 +48,6 @@ impl FormatNodeRule<StringLiteral> for FormatStringLiteral {
|
|||
};
|
||||
|
||||
let normalized = StringNormalizer::from_context(f.context())
|
||||
.with_quoting(self.layout.quoting(f.context()))
|
||||
.with_preferred_quote_style(quote_style)
|
||||
.normalize(item.into());
|
||||
|
||||
|
|
|
@ -6,9 +6,7 @@ use crate::expression::parentheses::{
|
|||
is_expression_parenthesized, parenthesized, Parentheses, Parenthesize,
|
||||
};
|
||||
use crate::prelude::*;
|
||||
use crate::preview::{
|
||||
is_with_single_item_pre_39_enabled, is_with_single_target_parentheses_enabled,
|
||||
};
|
||||
use crate::preview::is_with_single_target_parentheses_enabled;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum WithItemLayout {
|
||||
|
@ -154,9 +152,7 @@ impl FormatNodeRule<WithItem> for FormatWithItem {
|
|||
}
|
||||
|
||||
WithItemLayout::Python38OrOlder { single } => {
|
||||
let parenthesize = if (single && is_with_single_item_pre_39_enabled(f.context()))
|
||||
|| is_parenthesized
|
||||
{
|
||||
let parenthesize = if single || is_parenthesized {
|
||||
Parenthesize::IfBreaks
|
||||
} else {
|
||||
Parenthesize::IfRequired
|
||||
|
|
|
@ -14,7 +14,6 @@ use crate::expression::parentheses::{
|
|||
optional_parentheses, parenthesized, NeedsParentheses, OptionalParentheses, Parentheses,
|
||||
};
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_join_implicit_concatenated_string_enabled;
|
||||
|
||||
pub(crate) mod pattern_arguments;
|
||||
pub(crate) mod pattern_keyword;
|
||||
|
@ -227,7 +226,7 @@ pub(crate) fn can_pattern_omit_optional_parentheses(
|
|||
pattern: &Pattern,
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
let mut visitor = CanOmitOptionalParenthesesVisitor::new(context);
|
||||
let mut visitor = CanOmitOptionalParenthesesVisitor::default();
|
||||
visitor.visit_pattern(pattern, context);
|
||||
|
||||
if !visitor.any_parenthesized_expressions {
|
||||
|
@ -272,32 +271,16 @@ pub(crate) fn can_pattern_omit_optional_parentheses(
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Default)]
|
||||
struct CanOmitOptionalParenthesesVisitor<'input> {
|
||||
max_precedence: OperatorPrecedence,
|
||||
max_precedence_count: usize,
|
||||
any_parenthesized_expressions: bool,
|
||||
join_implicit_concatenated_strings: bool,
|
||||
last: Option<&'input Pattern>,
|
||||
first: First<'input>,
|
||||
}
|
||||
|
||||
impl<'a> CanOmitOptionalParenthesesVisitor<'a> {
|
||||
fn new(context: &PyFormatContext) -> Self {
|
||||
Self {
|
||||
max_precedence: OperatorPrecedence::default(),
|
||||
max_precedence_count: 0,
|
||||
any_parenthesized_expressions: false,
|
||||
// TODO: Derive default for `CanOmitOptionalParenthesesVisitor` when removing the `join_implicit_concatenated_strings`
|
||||
// preview style.
|
||||
join_implicit_concatenated_strings: is_join_implicit_concatenated_string_enabled(
|
||||
context,
|
||||
),
|
||||
last: None,
|
||||
first: First::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_pattern(&mut self, pattern: &'a Pattern, context: &PyFormatContext) {
|
||||
match pattern {
|
||||
Pattern::MatchSequence(_) | Pattern::MatchMapping(_) => {
|
||||
|
@ -305,27 +288,11 @@ impl<'a> CanOmitOptionalParenthesesVisitor<'a> {
|
|||
}
|
||||
|
||||
Pattern::MatchValue(value) => match &*value.value {
|
||||
Expr::StringLiteral(string) => {
|
||||
if !self.join_implicit_concatenated_strings {
|
||||
self.update_max_precedence(OperatorPrecedence::String, string.value.len());
|
||||
}
|
||||
}
|
||||
Expr::BytesLiteral(bytes) => {
|
||||
if !self.join_implicit_concatenated_strings {
|
||||
self.update_max_precedence(OperatorPrecedence::String, bytes.value.len());
|
||||
}
|
||||
}
|
||||
Expr::StringLiteral(_) |
|
||||
Expr::BytesLiteral(_) |
|
||||
// F-strings are allowed according to python's grammar but fail with a syntax error at runtime.
|
||||
// That's why we need to support them for formatting.
|
||||
Expr::FString(string) => {
|
||||
if !self.join_implicit_concatenated_strings {
|
||||
self.update_max_precedence(
|
||||
OperatorPrecedence::String,
|
||||
string.value.as_slice().len(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Expr::FString(_) |
|
||||
Expr::NumberLiteral(_) | Expr::Attribute(_) | Expr::UnaryOp(_) => {
|
||||
// require no state update other than visit_pattern does.
|
||||
}
|
||||
|
@ -397,8 +364,6 @@ enum OperatorPrecedence {
|
|||
None,
|
||||
Additive,
|
||||
Or,
|
||||
// Implicit string concatenation
|
||||
String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
|
|
|
@ -5,7 +5,6 @@ use ruff_python_ast::PatternMatchAs;
|
|||
use crate::comments::dangling_comments;
|
||||
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_match_case_parentheses_enabled;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatPatternMatchAs;
|
||||
|
@ -55,16 +54,12 @@ impl NeedsParentheses for PatternMatchAs {
|
|||
fn needs_parentheses(
|
||||
&self,
|
||||
_parent: AnyNodeRef,
|
||||
context: &PyFormatContext,
|
||||
_context: &PyFormatContext,
|
||||
) -> OptionalParentheses {
|
||||
if is_match_case_parentheses_enabled(context) {
|
||||
if self.name.is_some() {
|
||||
OptionalParentheses::Multiline
|
||||
} else {
|
||||
OptionalParentheses::BestFit
|
||||
}
|
||||
} else {
|
||||
if self.name.is_some() {
|
||||
OptionalParentheses::Multiline
|
||||
} else {
|
||||
OptionalParentheses::BestFit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ use crate::expression::parentheses::{
|
|||
OptionalParentheses,
|
||||
};
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_match_case_parentheses_enabled;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatPatternMatchOr;
|
||||
|
@ -43,11 +42,7 @@ impl FormatNodeRule<PatternMatchOr> for FormatPatternMatchOr {
|
|||
Ok(())
|
||||
});
|
||||
|
||||
if is_match_case_parentheses_enabled(f.context()) {
|
||||
in_parentheses_only_group(&inner).fmt(f)
|
||||
} else {
|
||||
inner.fmt(f)
|
||||
}
|
||||
in_parentheses_only_group(&inner).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ use ruff_python_ast::{PatternMatchSingleton, Singleton};
|
|||
|
||||
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_match_case_parentheses_enabled;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatPatternMatchSingleton;
|
||||
|
@ -22,12 +21,8 @@ impl NeedsParentheses for PatternMatchSingleton {
|
|||
fn needs_parentheses(
|
||||
&self,
|
||||
_parent: AnyNodeRef,
|
||||
context: &PyFormatContext,
|
||||
_context: &PyFormatContext,
|
||||
) -> OptionalParentheses {
|
||||
if is_match_case_parentheses_enabled(context) {
|
||||
OptionalParentheses::BestFit
|
||||
} else {
|
||||
OptionalParentheses::Never
|
||||
}
|
||||
OptionalParentheses::BestFit
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ use ruff_python_ast::PatternMatchValue;
|
|||
|
||||
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses, Parentheses};
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_match_case_parentheses_enabled;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatPatternMatchValue;
|
||||
|
@ -21,10 +20,6 @@ impl NeedsParentheses for PatternMatchValue {
|
|||
parent: AnyNodeRef,
|
||||
context: &PyFormatContext,
|
||||
) -> OptionalParentheses {
|
||||
if is_match_case_parentheses_enabled(context) {
|
||||
self.value.needs_parentheses(parent, context)
|
||||
} else {
|
||||
OptionalParentheses::Never
|
||||
}
|
||||
self.value.needs_parentheses(parent, context)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,65 +14,6 @@ pub(crate) const fn is_hug_parens_with_braces_and_square_brackets_enabled(
|
|||
context.is_preview()
|
||||
}
|
||||
|
||||
/// Returns `true` if the [`f-string formatting`](https://github.com/astral-sh/ruff/issues/7594) preview style is enabled.
|
||||
/// WARNING: This preview style depends on `is_f_string_implicit_concatenated_string_literal_quotes_enabled`.
|
||||
/// TODO: Remove `Quoting` when promoting this preview style and convert `FormatStringPart` etc. regular `FormatWithRule` implementations.
|
||||
/// TODO: Remove `format_f_string` from `normalize_string` when promoting this preview style.
|
||||
pub(crate) fn is_f_string_formatting_enabled(context: &PyFormatContext) -> bool {
|
||||
context.is_preview()
|
||||
}
|
||||
|
||||
/// See [#13539](https://github.com/astral-sh/ruff/pull/13539)
|
||||
/// Remove `Quoting` when stabilizing this preview style.
|
||||
pub(crate) fn is_f_string_implicit_concatenated_string_literal_quotes_enabled(
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
context.is_preview()
|
||||
}
|
||||
|
||||
pub(crate) fn is_with_single_item_pre_39_enabled(context: &PyFormatContext) -> bool {
|
||||
context.is_preview()
|
||||
}
|
||||
|
||||
/// See [#12282](https://github.com/astral-sh/ruff/pull/12282).
|
||||
pub(crate) fn is_comprehension_leading_expression_comments_same_line_enabled(
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
context.is_preview()
|
||||
}
|
||||
|
||||
/// See [#9447](https://github.com/astral-sh/ruff/issues/9447)
|
||||
pub(crate) fn is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled(
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
context.is_preview()
|
||||
}
|
||||
|
||||
/// See [#6933](https://github.com/astral-sh/ruff/issues/6933).
|
||||
/// This style also covers the black preview styles `remove_redundant_guard_parens` and `parens_for_long_if_clauses_in_case_block `.
|
||||
/// WARNING: This preview style depends on `is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled`
|
||||
/// because it relies on the new semantic of `IfBreaksParenthesized`.
|
||||
pub(crate) fn is_match_case_parentheses_enabled(context: &PyFormatContext) -> bool {
|
||||
context.is_preview()
|
||||
}
|
||||
|
||||
/// This preview style fixes a bug with the docstring's `line-length` calculation when using the `dynamic` mode.
|
||||
/// The new style now respects the indent **inside** the docstring and reduces the `line-length` accordingly
|
||||
/// so that the docstring's code block fits into the global line-length setting.
|
||||
pub(crate) fn is_docstring_code_block_in_docstring_indent_enabled(
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
context.is_preview()
|
||||
}
|
||||
|
||||
/// Returns `true` if implicitly concatenated strings should be joined if they all fit on a single line.
|
||||
/// See [#9457](https://github.com/astral-sh/ruff/issues/9457)
|
||||
/// WARNING: This preview style depends on `is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled`
|
||||
/// because it relies on the new semantic of `IfBreaksParenthesized`.
|
||||
pub(crate) fn is_join_implicit_concatenated_string_enabled(context: &PyFormatContext) -> bool {
|
||||
context.is_preview()
|
||||
}
|
||||
|
||||
/// Returns `true` if the bugfix for single-with items with a trailing comment targeting Python 3.9 or newer is enabled.
|
||||
///
|
||||
/// See [#14001](https://github.com/astral-sh/ruff/issues/14001)
|
||||
|
|
|
@ -3,10 +3,8 @@ use ruff_formatter::write;
|
|||
use ruff_python_ast::StmtAssert;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
|
||||
use crate::expression::maybe_parenthesize_expression;
|
||||
use crate::expression::parentheses::Parenthesize;
|
||||
use crate::preview::is_join_implicit_concatenated_string_enabled;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -30,18 +28,12 @@ impl FormatNodeRule<StmtAssert> for FormatStmtAssert {
|
|||
)?;
|
||||
|
||||
if let Some(msg) = msg {
|
||||
let parenthesize = if is_join_implicit_concatenated_string_enabled(f.context()) {
|
||||
Parenthesize::IfBreaksParenthesized
|
||||
} else {
|
||||
Parenthesize::IfBreaks
|
||||
};
|
||||
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
token(","),
|
||||
space(),
|
||||
maybe_parenthesize_expression(msg, item, parenthesize),
|
||||
maybe_parenthesize_expression(msg, item, Parenthesize::IfBreaksParenthesized),
|
||||
]
|
||||
)?;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use ruff_formatter::{format_args, write, FormatError, RemoveSoftLinesBuffer};
|
||||
use ruff_python_ast::{
|
||||
AnyNodeRef, Expr, ExprAttribute, ExprCall, FStringPart, Operator, StmtAssign, StringLike,
|
||||
TypeParams,
|
||||
AnyNodeRef, Expr, ExprAttribute, ExprCall, FString, FStringPart, Operator, StmtAssign,
|
||||
StringLike, TypeParams,
|
||||
};
|
||||
|
||||
use crate::builders::parenthesize_if_expands;
|
||||
|
@ -9,7 +9,6 @@ use crate::comments::{
|
|||
trailing_comments, Comments, LeadingDanglingTrailingComments, SourceComment,
|
||||
};
|
||||
use crate::context::{NodeLevel, WithNodeLevel};
|
||||
use crate::expression::expr_f_string::f_string_quoting;
|
||||
use crate::expression::parentheses::{
|
||||
is_expression_parenthesized, optional_parentheses, NeedsParentheses, OptionalParentheses,
|
||||
Parentheses, Parenthesize,
|
||||
|
@ -18,10 +17,7 @@ use crate::expression::{
|
|||
can_omit_optional_parentheses, has_own_parentheses, has_parentheses,
|
||||
maybe_parenthesize_expression,
|
||||
};
|
||||
use crate::other::f_string::{FStringLayout, FormatFString};
|
||||
use crate::preview::{
|
||||
is_f_string_formatting_enabled, is_join_implicit_concatenated_string_enabled,
|
||||
};
|
||||
use crate::other::f_string::FStringLayout;
|
||||
use crate::statement::trailing_semicolon;
|
||||
use crate::string::implicit::{
|
||||
FormatImplicitConcatenatedStringExpanded, FormatImplicitConcatenatedStringFlat,
|
||||
|
@ -456,7 +452,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
|||
let f_string_flat = format_with(|f| {
|
||||
let mut buffer = RemoveSoftLinesBuffer::new(&mut *f);
|
||||
|
||||
write!(buffer, [format_f_string])
|
||||
write!(buffer, [format_f_string.format()])
|
||||
})
|
||||
.memoized();
|
||||
|
||||
|
@ -518,7 +514,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
|||
// }moreeeeeeeeeeeeeeeee"
|
||||
// ```
|
||||
let format_f_string =
|
||||
format_with(|f| write!(f, [format_f_string, inline_comments]));
|
||||
format_with(|f| write!(f, [format_f_string.format(), inline_comments]));
|
||||
|
||||
best_fitting![single_line, joined_parenthesized, format_f_string]
|
||||
.with_mode(BestFittingMode::AllLines)
|
||||
|
@ -668,7 +664,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
|||
// Similar to above, remove any soft line breaks emitted by the f-string
|
||||
// formatting.
|
||||
let mut buffer = RemoveSoftLinesBuffer::new(&mut *f);
|
||||
write!(buffer, [format_f_string])
|
||||
write!(buffer, [format_f_string.format()])
|
||||
} else {
|
||||
value.format().with_options(Parentheses::Never).fmt(f)
|
||||
}
|
||||
|
@ -701,20 +697,22 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
|||
// )
|
||||
// ```
|
||||
let flat_target_parenthesize_value = format_with(|f| {
|
||||
write!(f, [last_target, space(), operator, space(), token("("),])?;
|
||||
|
||||
if is_join_implicit_concatenated_string_enabled(f.context()) {
|
||||
group(&soft_block_indent(&format_args![
|
||||
format_value,
|
||||
inline_comments
|
||||
]))
|
||||
.should_expand(true)
|
||||
.fmt(f)?;
|
||||
} else {
|
||||
block_indent(&format_args![format_value, inline_comments]).fmt(f)?;
|
||||
}
|
||||
|
||||
token(")").fmt(f)
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
last_target,
|
||||
space(),
|
||||
operator,
|
||||
space(),
|
||||
token("("),
|
||||
group(&soft_block_indent(&format_args![
|
||||
format_value,
|
||||
inline_comments
|
||||
]))
|
||||
.should_expand(true),
|
||||
token(")")
|
||||
]
|
||||
)
|
||||
});
|
||||
|
||||
// Fall back to parenthesizing (or splitting) the last target part if we can't make the value
|
||||
|
@ -726,15 +724,16 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
|||
// ] = c
|
||||
// ```
|
||||
let split_target_flat_value = format_with(|f| {
|
||||
if is_join_implicit_concatenated_string_enabled(f.context()) {
|
||||
group(&last_target).should_expand(true).fmt(f)?;
|
||||
} else {
|
||||
last_target.fmt(f)?;
|
||||
}
|
||||
|
||||
write!(
|
||||
f,
|
||||
[space(), operator, space(), format_value, inline_comments]
|
||||
[
|
||||
group(&last_target).should_expand(true),
|
||||
space(),
|
||||
operator,
|
||||
space(),
|
||||
format_value,
|
||||
inline_comments
|
||||
]
|
||||
)
|
||||
});
|
||||
|
||||
|
@ -935,7 +934,8 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
|||
}
|
||||
|
||||
let format_f_string =
|
||||
format_with(|f| write!(f, [format_f_string, inline_comments])).memoized();
|
||||
format_with(|f| write!(f, [format_f_string.format(), inline_comments]))
|
||||
.memoized();
|
||||
|
||||
// Considering the following initial source:
|
||||
//
|
||||
|
@ -1102,11 +1102,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
|||
fn format_f_string_assignment<'a>(
|
||||
string: StringLike<'a>,
|
||||
context: &PyFormatContext,
|
||||
) -> Option<FormatFString<'a>> {
|
||||
if !is_f_string_formatting_enabled(context) {
|
||||
return None;
|
||||
}
|
||||
|
||||
) -> Option<&'a FString> {
|
||||
let StringLike::FString(expr) = string else {
|
||||
return None;
|
||||
};
|
||||
|
@ -1128,10 +1124,7 @@ fn format_f_string_assignment<'a>(
|
|||
return None;
|
||||
}
|
||||
|
||||
Some(FormatFString::new(
|
||||
f_string,
|
||||
f_string_quoting(expr, context.source()),
|
||||
))
|
||||
Some(f_string)
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
|
|
@ -18,10 +18,6 @@ use {
|
|||
ruff_text_size::{Ranged, TextLen, TextRange, TextSize},
|
||||
};
|
||||
|
||||
use crate::preview::{
|
||||
is_docstring_code_block_in_docstring_indent_enabled,
|
||||
is_join_implicit_concatenated_string_enabled,
|
||||
};
|
||||
use crate::string::StringQuotes;
|
||||
use crate::{prelude::*, DocstringCodeLineWidth, FormatModuleError};
|
||||
|
||||
|
@ -171,7 +167,7 @@ pub(crate) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> Form
|
|||
if docstring[first.len()..].trim().is_empty() {
|
||||
// For `"""\n"""` or other whitespace between the quotes, black keeps a single whitespace,
|
||||
// but `""""""` doesn't get one inserted.
|
||||
if needs_chaperone_space(normalized.flags(), trim_end, f.context())
|
||||
if needs_chaperone_space(normalized.flags(), trim_end)
|
||||
|| (trim_end.is_empty() && !docstring.is_empty())
|
||||
{
|
||||
space().fmt(f)?;
|
||||
|
@ -211,7 +207,7 @@ pub(crate) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> Form
|
|||
let trim_end = docstring
|
||||
.as_ref()
|
||||
.trim_end_matches(|c: char| c.is_whitespace() && c != '\n');
|
||||
if needs_chaperone_space(normalized.flags(), trim_end, f.context()) {
|
||||
if needs_chaperone_space(normalized.flags(), trim_end) {
|
||||
space().fmt(f)?;
|
||||
}
|
||||
|
||||
|
@ -508,17 +504,15 @@ impl<'src> DocstringLinePrinter<'_, '_, '_, 'src> {
|
|||
.to_ascii_spaces(indent_width)
|
||||
.saturating_add(kind.extra_indent_ascii_spaces());
|
||||
|
||||
if is_docstring_code_block_in_docstring_indent_enabled(self.f.context()) {
|
||||
// Add the in-docstring indentation
|
||||
current_indent = current_indent.saturating_add(
|
||||
u16::try_from(
|
||||
kind.indent()
|
||||
.columns()
|
||||
.saturating_sub(self.stripped_indentation.columns()),
|
||||
)
|
||||
.unwrap_or(u16::MAX),
|
||||
);
|
||||
}
|
||||
// Add the in-docstring indentation
|
||||
current_indent = current_indent.saturating_add(
|
||||
u16::try_from(
|
||||
kind.indent()
|
||||
.columns()
|
||||
.saturating_sub(self.stripped_indentation.columns()),
|
||||
)
|
||||
.unwrap_or(u16::MAX),
|
||||
);
|
||||
|
||||
let width = std::cmp::max(1, global_line_width.saturating_sub(current_indent));
|
||||
LineWidth::try_from(width).expect("width should be capped at a minimum of 1")
|
||||
|
@ -1607,17 +1601,11 @@ fn docstring_format_source(
|
|||
/// If the last line of the docstring is `content" """` or `content\ """`, we need a chaperone space
|
||||
/// that avoids `content""""` and `content\"""`. This does only applies to un-escaped backslashes,
|
||||
/// so `content\\ """` doesn't need a space while `content\\\ """` does.
|
||||
pub(super) fn needs_chaperone_space(
|
||||
flags: AnyStringFlags,
|
||||
trim_end: &str,
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
pub(super) fn needs_chaperone_space(flags: AnyStringFlags, trim_end: &str) -> bool {
|
||||
if trim_end.chars().rev().take_while(|c| *c == '\\').count() % 2 == 1 {
|
||||
true
|
||||
} else if is_join_implicit_concatenated_string_enabled(context) {
|
||||
flags.is_triple_quoted() && trim_end.ends_with(flags.quote_style().as_char())
|
||||
} else {
|
||||
trim_end.ends_with(flags.quote_style().as_char())
|
||||
flags.is_triple_quoted() && trim_end.ends_with(flags.quote_style().as_char())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,13 +11,9 @@ use std::borrow::Cow;
|
|||
|
||||
use crate::comments::{leading_comments, trailing_comments};
|
||||
use crate::expression::parentheses::in_parentheses_only_soft_line_break_or_space;
|
||||
use crate::other::f_string::{FStringContext, FStringLayout, FormatFString};
|
||||
use crate::other::f_string::{FStringContext, FStringLayout};
|
||||
use crate::other::f_string_element::FormatFStringExpressionElement;
|
||||
use crate::other::string_literal::StringLiteralKind;
|
||||
use crate::prelude::*;
|
||||
use crate::preview::{
|
||||
is_f_string_formatting_enabled, is_join_implicit_concatenated_string_enabled,
|
||||
};
|
||||
use crate::string::docstring::needs_chaperone_space;
|
||||
use crate::string::normalize::{
|
||||
is_fstring_with_quoted_debug_expression, is_fstring_with_quoted_format_spec_and_debug,
|
||||
|
@ -82,14 +78,9 @@ impl<'a> FormatImplicitConcatenatedStringExpanded<'a> {
|
|||
impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedStringExpanded<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
|
||||
let comments = f.context().comments().clone();
|
||||
let quoting = self.string.quoting(f.context().source());
|
||||
|
||||
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()
|
||||
|
@ -103,23 +94,13 @@ impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedStringExpanded<'_
|
|||
|
||||
for part in self.string.parts() {
|
||||
let format_part = format_with(|f: &mut PyFormatter| match part {
|
||||
StringLikePart::String(part) => {
|
||||
let kind = if self.string.is_fstring() {
|
||||
#[allow(deprecated)]
|
||||
StringLiteralKind::InImplicitlyConcatenatedFString(quoting)
|
||||
} else {
|
||||
StringLiteralKind::String
|
||||
};
|
||||
|
||||
part.format().with_options(kind).fmt(f)
|
||||
}
|
||||
StringLikePart::String(part) => part.format().fmt(f),
|
||||
StringLikePart::Bytes(bytes_literal) => bytes_literal.format().fmt(f),
|
||||
StringLikePart::FString(part) => FormatFString::new(part, quoting).fmt(f),
|
||||
StringLikePart::FString(part) => part.format().fmt(f),
|
||||
});
|
||||
|
||||
let part_comments = comments.leading_dangling_trailing(&part);
|
||||
joiner.entry(&format_args![
|
||||
(!join_implicit_concatenated_string_enabled).then_some(line_suffix_boundary()),
|
||||
leading_comments(part_comments.leading),
|
||||
format_part,
|
||||
trailing_comments(part_comments.trailing)
|
||||
|
@ -149,10 +130,6 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> {
|
|||
/// Creates a new formatter. Returns `None` if the string can't be merged into a single string.
|
||||
pub(crate) fn new(string: StringLike<'a>, context: &PyFormatContext) -> Option<Self> {
|
||||
fn merge_flags(string: StringLike, context: &PyFormatContext) -> Option<AnyStringFlags> {
|
||||
if !is_join_implicit_concatenated_string_enabled(context) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Multiline strings can never fit on a single line.
|
||||
if string.is_multiline(context) {
|
||||
return None;
|
||||
|
@ -323,44 +300,29 @@ impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedStringFlat<'_> {
|
|||
}
|
||||
|
||||
StringLikePart::FString(f_string) => {
|
||||
if is_f_string_formatting_enabled(f.context()) {
|
||||
for element in &f_string.elements {
|
||||
match element {
|
||||
FStringElement::Literal(literal) => {
|
||||
FormatLiteralContent {
|
||||
range: literal.range(),
|
||||
flags: self.flags,
|
||||
is_fstring: true,
|
||||
trim_end: false,
|
||||
trim_start: false,
|
||||
}
|
||||
.fmt(f)?;
|
||||
for element in &f_string.elements {
|
||||
match element {
|
||||
FStringElement::Literal(literal) => {
|
||||
FormatLiteralContent {
|
||||
range: literal.range(),
|
||||
flags: self.flags,
|
||||
is_fstring: true,
|
||||
trim_end: false,
|
||||
trim_start: false,
|
||||
}
|
||||
// Formatting the expression here and in the expanded version is safe **only**
|
||||
// because we assert that the f-string never contains any comments.
|
||||
FStringElement::Expression(expression) => {
|
||||
let context = FStringContext::new(
|
||||
self.flags,
|
||||
FStringLayout::from_f_string(
|
||||
f_string,
|
||||
f.context().source(),
|
||||
),
|
||||
);
|
||||
.fmt(f)?;
|
||||
}
|
||||
// Formatting the expression here and in the expanded version is safe **only**
|
||||
// because we assert that the f-string never contains any comments.
|
||||
FStringElement::Expression(expression) => {
|
||||
let context = FStringContext::new(
|
||||
self.flags,
|
||||
FStringLayout::from_f_string(f_string, f.context().source()),
|
||||
);
|
||||
|
||||
FormatFStringExpressionElement::new(expression, context)
|
||||
.fmt(f)?;
|
||||
}
|
||||
FormatFStringExpressionElement::new(expression, context).fmt(f)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FormatLiteralContent {
|
||||
range: part.content_range(),
|
||||
flags: self.flags,
|
||||
is_fstring: true,
|
||||
trim_end: false,
|
||||
trim_start: false,
|
||||
}
|
||||
.fmt(f)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -386,9 +348,6 @@ impl Format<PyFormatContext<'_>> for FormatLiteralContent {
|
|||
0,
|
||||
self.flags,
|
||||
self.flags.is_f_string() && !self.is_fstring,
|
||||
// TODO: Remove the argument from `normalize_string` when promoting the `is_f_string_formatting_enabled` preview style.
|
||||
self.flags.is_f_string() && !is_f_string_formatting_enabled(f.context()),
|
||||
is_f_string_formatting_enabled(f.context()),
|
||||
);
|
||||
|
||||
// Trim the start and end of the string if it's the first or last part of a docstring.
|
||||
|
@ -413,7 +372,7 @@ impl Format<PyFormatContext<'_>> for FormatLiteralContent {
|
|||
Cow::Owned(normalized) => text(normalized).fmt(f)?,
|
||||
}
|
||||
|
||||
if self.trim_end && needs_chaperone_space(self.flags, &normalized, f.context()) {
|
||||
if self.trim_end && needs_chaperone_space(self.flags, &normalized) {
|
||||
space().fmt(f)?;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,22 +10,13 @@ use ruff_python_ast::{
|
|||
use ruff_source_file::LineRanges;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::expression::expr_f_string::f_string_quoting;
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_f_string_formatting_enabled;
|
||||
use crate::QuoteStyle;
|
||||
|
||||
pub(crate) mod docstring;
|
||||
pub(crate) mod implicit;
|
||||
mod normalize;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub(crate) enum Quoting {
|
||||
#[default]
|
||||
CanChange,
|
||||
Preserve,
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for AnyStringPrefix {
|
||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
// Remove the unicode prefix `u` if any because it is meaningless in Python 3+.
|
||||
|
@ -90,19 +81,10 @@ impl From<Quote> for QuoteStyle {
|
|||
|
||||
// Extension trait that adds formatter specific helper methods to `StringLike`.
|
||||
pub(crate) trait StringLikeExtensions {
|
||||
fn quoting(&self, source: &str) -> Quoting;
|
||||
|
||||
fn is_multiline(&self, context: &PyFormatContext) -> bool;
|
||||
}
|
||||
|
||||
impl StringLikeExtensions for ast::StringLike<'_> {
|
||||
fn quoting(&self, source: &str) -> Quoting {
|
||||
match self {
|
||||
Self::String(_) | Self::Bytes(_) => Quoting::CanChange,
|
||||
Self::FString(f_string) => f_string_quoting(f_string, source),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_multiline(&self, context: &PyFormatContext) -> bool {
|
||||
self.parts().any(|part| match part {
|
||||
StringLikePart::String(_) | StringLikePart::Bytes(_) => {
|
||||
|
@ -149,15 +131,11 @@ impl StringLikeExtensions for ast::StringLike<'_> {
|
|||
})
|
||||
}
|
||||
|
||||
if is_f_string_formatting_enabled(context) {
|
||||
contains_line_break_or_comments(
|
||||
&f_string.elements,
|
||||
context,
|
||||
f_string.flags.is_triple_quoted(),
|
||||
)
|
||||
} else {
|
||||
context.source().contains_line_break(f_string.range())
|
||||
}
|
||||
contains_line_break_or_comments(
|
||||
&f_string.elements,
|
||||
context,
|
||||
f_string.flags.is_triple_quoted(),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -12,12 +12,10 @@ use ruff_text_size::{Ranged, TextRange, TextSlice};
|
|||
|
||||
use crate::context::FStringState;
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_f_string_formatting_enabled;
|
||||
use crate::string::{Quoting, StringQuotes};
|
||||
use crate::string::StringQuotes;
|
||||
use crate::QuoteStyle;
|
||||
|
||||
pub(crate) struct StringNormalizer<'a, 'src> {
|
||||
quoting: Quoting,
|
||||
preferred_quote_style: Option<QuoteStyle>,
|
||||
context: &'a PyFormatContext<'src>,
|
||||
}
|
||||
|
@ -25,7 +23,6 @@ pub(crate) struct StringNormalizer<'a, 'src> {
|
|||
impl<'a, 'src> StringNormalizer<'a, 'src> {
|
||||
pub(crate) fn from_context(context: &'a PyFormatContext<'src>) -> Self {
|
||||
Self {
|
||||
quoting: Quoting::default(),
|
||||
preferred_quote_style: None,
|
||||
context,
|
||||
}
|
||||
|
@ -36,11 +33,6 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
|
|||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_quoting(mut self, quoting: Quoting) -> Self {
|
||||
self.quoting = quoting;
|
||||
self
|
||||
}
|
||||
|
||||
/// Determines the preferred quote style for `string`.
|
||||
/// The formatter should use the preferred quote style unless
|
||||
/// it can't because the string contains the preferred quotes OR
|
||||
|
@ -49,112 +41,106 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
|
|||
/// Note: If you add more cases here where we return `QuoteStyle::Preserve`,
|
||||
/// make sure to also add them to [`FormatImplicitConcatenatedStringFlat::new`].
|
||||
pub(super) fn preferred_quote_style(&self, string: StringLikePart) -> QuoteStyle {
|
||||
match self.quoting {
|
||||
Quoting::Preserve => QuoteStyle::Preserve,
|
||||
Quoting::CanChange => {
|
||||
let preferred_quote_style = self
|
||||
.preferred_quote_style
|
||||
.unwrap_or(self.context.options().quote_style());
|
||||
let preferred_quote_style = self
|
||||
.preferred_quote_style
|
||||
.unwrap_or(self.context.options().quote_style());
|
||||
|
||||
if preferred_quote_style.is_preserve() {
|
||||
if preferred_quote_style.is_preserve() {
|
||||
return QuoteStyle::Preserve;
|
||||
}
|
||||
|
||||
if let StringLikePart::FString(fstring) = string {
|
||||
// There are two cases where it's necessary to preserve the quotes if the
|
||||
// target version is pre 3.12 and the part is an f-string.
|
||||
if !self.context.options().target_version().supports_pep_701() {
|
||||
// An f-string expression contains a debug text with a quote character
|
||||
// because the formatter will emit the debug expression **exactly** the
|
||||
// same as in the source text.
|
||||
if is_fstring_with_quoted_debug_expression(fstring, self.context) {
|
||||
return QuoteStyle::Preserve;
|
||||
}
|
||||
|
||||
if let StringLikePart::FString(fstring) = string {
|
||||
// There are two cases where it's necessary to preserve the quotes if the
|
||||
// target version is pre 3.12 and the part is an f-string.
|
||||
if !self.context.options().target_version().supports_pep_701() {
|
||||
// An f-string expression contains a debug text with a quote character
|
||||
// because the formatter will emit the debug expression **exactly** the
|
||||
// same as in the source text.
|
||||
if is_fstring_with_quoted_debug_expression(fstring, self.context) {
|
||||
return QuoteStyle::Preserve;
|
||||
}
|
||||
|
||||
// An f-string expression that contains a triple quoted string literal
|
||||
// expression that contains a quote.
|
||||
if is_fstring_with_triple_quoted_literal_expression_containing_quotes(
|
||||
fstring,
|
||||
self.context,
|
||||
) {
|
||||
return QuoteStyle::Preserve;
|
||||
}
|
||||
}
|
||||
|
||||
// An f-string expression element contains a debug text and the corresponding
|
||||
// format specifier has a literal element with a quote character.
|
||||
if is_fstring_with_quoted_format_spec_and_debug(fstring, self.context) {
|
||||
return QuoteStyle::Preserve;
|
||||
}
|
||||
}
|
||||
|
||||
// For f-strings prefer alternating the quotes unless The outer string is triple quoted and the inner isn't.
|
||||
if let FStringState::InsideExpressionElement(parent_context) =
|
||||
self.context.f_string_state()
|
||||
{
|
||||
let parent_flags = parent_context.f_string().flags();
|
||||
|
||||
if !parent_flags.is_triple_quoted() || string.flags().is_triple_quoted() {
|
||||
return QuoteStyle::from(parent_flags.quote_style().opposite());
|
||||
}
|
||||
}
|
||||
|
||||
// Per PEP 8, always prefer double quotes for triple-quoted strings.
|
||||
if string.flags().is_triple_quoted() {
|
||||
// ... unless we're formatting a code snippet inside a docstring,
|
||||
// then we specifically want to invert our quote style to avoid
|
||||
// writing out invalid Python.
|
||||
//
|
||||
// It's worth pointing out that we can actually wind up being
|
||||
// somewhat out of sync with PEP8 in this case. Consider this
|
||||
// example:
|
||||
//
|
||||
// def foo():
|
||||
// '''
|
||||
// Something.
|
||||
//
|
||||
// >>> """tricksy"""
|
||||
// '''
|
||||
// pass
|
||||
//
|
||||
// Ideally, this would be reformatted as:
|
||||
//
|
||||
// def foo():
|
||||
// """
|
||||
// Something.
|
||||
//
|
||||
// >>> '''tricksy'''
|
||||
// """
|
||||
// pass
|
||||
//
|
||||
// But the logic here results in the original quoting being
|
||||
// preserved. This is because the quoting style of the outer
|
||||
// docstring is determined, in part, by looking at its contents. In
|
||||
// this case, it notices that it contains a `"""` and thus infers
|
||||
// that using `'''` would overall read better because it avoids
|
||||
// the need to escape the interior `"""`. Except... in this case,
|
||||
// the `"""` is actually part of a code snippet that could get
|
||||
// reformatted to using a different quoting style itself.
|
||||
//
|
||||
// Fixing this would, I believe, require some fairly seismic
|
||||
// changes to how formatting strings works. Namely, we would need
|
||||
// to look for code snippets before normalizing the docstring, and
|
||||
// then figure out the quoting style more holistically by looking
|
||||
// at the various kinds of quotes used in the code snippets and
|
||||
// what reformatting them might look like.
|
||||
//
|
||||
// Overall this is a bit of a corner case and just inverting the
|
||||
// style from what the parent ultimately decided upon works, even
|
||||
// if it doesn't have perfect alignment with PEP8.
|
||||
if let Some(quote) = self.context.docstring() {
|
||||
QuoteStyle::from(quote.opposite())
|
||||
} else {
|
||||
QuoteStyle::Double
|
||||
}
|
||||
} else {
|
||||
preferred_quote_style
|
||||
// An f-string expression that contains a triple quoted string literal
|
||||
// expression that contains a quote.
|
||||
if is_fstring_with_triple_quoted_literal_expression_containing_quotes(
|
||||
fstring,
|
||||
self.context,
|
||||
) {
|
||||
return QuoteStyle::Preserve;
|
||||
}
|
||||
}
|
||||
|
||||
// An f-string expression element contains a debug text and the corresponding
|
||||
// format specifier has a literal element with a quote character.
|
||||
if is_fstring_with_quoted_format_spec_and_debug(fstring, self.context) {
|
||||
return QuoteStyle::Preserve;
|
||||
}
|
||||
}
|
||||
|
||||
// For f-strings prefer alternating the quotes unless The outer string is triple quoted and the inner isn't.
|
||||
if let FStringState::InsideExpressionElement(parent_context) = self.context.f_string_state()
|
||||
{
|
||||
let parent_flags = parent_context.f_string().flags();
|
||||
|
||||
if !parent_flags.is_triple_quoted() || string.flags().is_triple_quoted() {
|
||||
return QuoteStyle::from(parent_flags.quote_style().opposite());
|
||||
}
|
||||
}
|
||||
|
||||
// Per PEP 8, always prefer double quotes for triple-quoted strings.
|
||||
if string.flags().is_triple_quoted() {
|
||||
// ... unless we're formatting a code snippet inside a docstring,
|
||||
// then we specifically want to invert our quote style to avoid
|
||||
// writing out invalid Python.
|
||||
//
|
||||
// It's worth pointing out that we can actually wind up being
|
||||
// somewhat out of sync with PEP8 in this case. Consider this
|
||||
// example:
|
||||
//
|
||||
// def foo():
|
||||
// '''
|
||||
// Something.
|
||||
//
|
||||
// >>> """tricksy"""
|
||||
// '''
|
||||
// pass
|
||||
//
|
||||
// Ideally, this would be reformatted as:
|
||||
//
|
||||
// def foo():
|
||||
// """
|
||||
// Something.
|
||||
//
|
||||
// >>> '''tricksy'''
|
||||
// """
|
||||
// pass
|
||||
//
|
||||
// But the logic here results in the original quoting being
|
||||
// preserved. This is because the quoting style of the outer
|
||||
// docstring is determined, in part, by looking at its contents. In
|
||||
// this case, it notices that it contains a `"""` and thus infers
|
||||
// that using `'''` would overall read better because it avoids
|
||||
// the need to escape the interior `"""`. Except... in this case,
|
||||
// the `"""` is actually part of a code snippet that could get
|
||||
// reformatted to using a different quoting style itself.
|
||||
//
|
||||
// Fixing this would, I believe, require some fairly seismic
|
||||
// changes to how formatting strings works. Namely, we would need
|
||||
// to look for code snippets before normalizing the docstring, and
|
||||
// then figure out the quoting style more holistically by looking
|
||||
// at the various kinds of quotes used in the code snippets and
|
||||
// what reformatting them might look like.
|
||||
//
|
||||
// Overall this is a bit of a corner case and just inverting the
|
||||
// style from what the parent ultimately decided upon works, even
|
||||
// if it doesn't have perfect alignment with PEP8.
|
||||
if let Some(quote) = self.context.docstring() {
|
||||
QuoteStyle::from(quote.opposite())
|
||||
} else {
|
||||
QuoteStyle::Double
|
||||
}
|
||||
} else {
|
||||
preferred_quote_style
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -163,7 +149,7 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
|
|||
let raw_content = &self.context.source()[string.content_range()];
|
||||
let first_quote_or_normalized_char_offset = raw_content
|
||||
.bytes()
|
||||
.position(|b| matches!(b, b'\\' | b'"' | b'\'' | b'\r' | b'{'));
|
||||
.position(|b| matches!(b, b'\\' | b'"' | b'\'' | b'\r'));
|
||||
let string_flags = string.flags();
|
||||
let preferred_style = self.preferred_quote_style(string);
|
||||
|
||||
|
@ -214,11 +200,7 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
|
|||
raw_content,
|
||||
first_quote_or_escape_offset,
|
||||
quote_selection.flags,
|
||||
// TODO: Remove the `b'{'` in `choose_quotes` when promoting the
|
||||
// `format_fstring` preview style
|
||||
false,
|
||||
false,
|
||||
is_f_string_formatting_enabled(self.context),
|
||||
)
|
||||
} else {
|
||||
Cow::Borrowed(raw_content)
|
||||
|
@ -269,20 +251,14 @@ impl QuoteMetadata {
|
|||
Self::from_str(text, part.flags(), preferred_quote)
|
||||
}
|
||||
StringLikePart::FString(fstring) => {
|
||||
if is_f_string_formatting_enabled(context) {
|
||||
let metadata = QuoteMetadata::from_str("", part.flags(), preferred_quote);
|
||||
let metadata = QuoteMetadata::from_str("", part.flags(), preferred_quote);
|
||||
|
||||
metadata.merge_fstring_elements(
|
||||
&fstring.elements,
|
||||
fstring.flags,
|
||||
context,
|
||||
preferred_quote,
|
||||
)
|
||||
} else {
|
||||
let text = &context.source()[part.content_range()];
|
||||
|
||||
Self::from_str(text, part.flags(), preferred_quote)
|
||||
}
|
||||
metadata.merge_fstring_elements(
|
||||
&fstring.elements,
|
||||
fstring.flags,
|
||||
context,
|
||||
preferred_quote,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -635,8 +611,6 @@ pub(crate) fn normalize_string(
|
|||
start_offset: usize,
|
||||
new_flags: AnyStringFlags,
|
||||
escape_braces: bool,
|
||||
flip_nested_fstring_quotes: bool,
|
||||
format_f_string: bool,
|
||||
) -> Cow<str> {
|
||||
// The normalized string if `input` is not yet normalized.
|
||||
// `output` must remain empty if `input` is already normalized.
|
||||
|
@ -653,9 +627,6 @@ pub(crate) fn normalize_string(
|
|||
|
||||
let is_raw = new_flags.is_raw_string();
|
||||
|
||||
let is_fstring = !format_f_string && new_flags.is_f_string();
|
||||
let mut formatted_value_nesting = 0u32;
|
||||
|
||||
while let Some((index, c)) = chars.next() {
|
||||
if matches!(c, '{' | '}') {
|
||||
if escape_braces {
|
||||
|
@ -664,17 +635,6 @@ pub(crate) fn normalize_string(
|
|||
output.push(c);
|
||||
last_index = index + c.len_utf8();
|
||||
continue;
|
||||
} else if is_fstring {
|
||||
if chars.peek().copied().is_some_and(|(_, next)| next == c) {
|
||||
// Skip over the second character of the double braces
|
||||
chars.next();
|
||||
} else if c == '{' {
|
||||
formatted_value_nesting += 1;
|
||||
} else {
|
||||
// Safe to assume that `c == '}'` here because of the matched pattern above
|
||||
formatted_value_nesting = formatted_value_nesting.saturating_sub(1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -723,7 +683,7 @@ pub(crate) fn normalize_string(
|
|||
|
||||
if !new_flags.is_triple_quoted() {
|
||||
#[allow(clippy::if_same_then_else)]
|
||||
if next == opposite_quote && formatted_value_nesting == 0 {
|
||||
if next == opposite_quote {
|
||||
// Remove the escape by ending before the backslash and starting again with the quote
|
||||
chars.next();
|
||||
output.push_str(&input[last_index..index]);
|
||||
|
@ -734,23 +694,12 @@ pub(crate) fn normalize_string(
|
|||
}
|
||||
}
|
||||
}
|
||||
} else if !new_flags.is_triple_quoted()
|
||||
&& c == preferred_quote
|
||||
&& formatted_value_nesting == 0
|
||||
{
|
||||
} else if !new_flags.is_triple_quoted() && c == preferred_quote {
|
||||
// Escape the quote
|
||||
output.push_str(&input[last_index..index]);
|
||||
output.push('\\');
|
||||
output.push(c);
|
||||
last_index = index + preferred_quote.len_utf8();
|
||||
} else if c == preferred_quote
|
||||
&& flip_nested_fstring_quotes
|
||||
&& formatted_value_nesting > 0
|
||||
{
|
||||
// Flip the quotes
|
||||
output.push_str(&input[last_index..index]);
|
||||
output.push(opposite_quote);
|
||||
last_index = index + preferred_quote.len_utf8();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1099,7 +1048,6 @@ fn contains_opposite_quote(content: &str, flags: AnyStringFlags) -> bool {
|
|||
mod tests {
|
||||
use std::borrow::Cow;
|
||||
|
||||
use ruff_python_ast::str_prefix::FStringPrefix;
|
||||
use ruff_python_ast::{
|
||||
str::Quote,
|
||||
str_prefix::{AnyStringPrefix, ByteStringPrefix},
|
||||
|
@ -1133,34 +1081,8 @@ mod tests {
|
|||
false,
|
||||
),
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
|
||||
assert_eq!(r"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a", &normalized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_nested_fstring() {
|
||||
let input =
|
||||
r#"With single quote: ' {my_dict['foo']} With double quote: " {my_dict["bar"]}"#;
|
||||
|
||||
let normalized = normalize_string(
|
||||
input,
|
||||
0,
|
||||
AnyStringFlags::new(
|
||||
AnyStringPrefix::Format(FStringPrefix::Regular),
|
||||
Quote::Double,
|
||||
false,
|
||||
),
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
"With single quote: ' {my_dict['foo']} With double quote: \\\" {my_dict['bar']}",
|
||||
&normalized
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue