Skip to content

Commit

Permalink
WIP: Initial attempt to split string formatting
Browse files Browse the repository at this point in the history
  • Loading branch information
dhruvmanila committed Dec 8, 2023
1 parent e043bd4 commit e54f469
Show file tree
Hide file tree
Showing 16 changed files with 319 additions and 154 deletions.
24 changes: 24 additions & 0 deletions crates/ruff_python_ast/src/nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,14 @@ impl FStringValue {
matches!(self.inner, FStringValueInner::Concatenated(_))
}

/// Returns a slice of all the [`FStringPart`]s contained in this value.
pub fn as_slice(&self) -> &[FStringPart] {
match &self.inner {
FStringValueInner::Single(part) => std::slice::from_ref(part),
FStringValueInner::Concatenated(parts) => parts,
}
}

/// Returns an iterator over all the [`FStringPart`]s contained in this value.
pub fn parts(&self) -> impl Iterator<Item = &FStringPart> {
match &self.inner {
Expand Down Expand Up @@ -1241,6 +1249,14 @@ impl StringLiteralValue {
self.parts().next().map_or(false, |part| part.unicode)
}

/// Returns a slice of all the [`StringLiteral`] parts contained in this value.
pub fn as_slice(&self) -> &[StringLiteral] {
match &self.inner {
StringLiteralValueInner::Single(value) => std::slice::from_ref(value),
StringLiteralValueInner::Concatenated(value) => value.strings.as_slice(),
}
}

/// Returns an iterator over all the [`StringLiteral`] parts contained in this value.
pub fn parts(&self) -> impl Iterator<Item = &StringLiteral> {
match &self.inner {
Expand Down Expand Up @@ -1457,6 +1473,14 @@ impl BytesLiteralValue {
matches!(self.inner, BytesLiteralValueInner::Concatenated(_))
}

/// Returns a slice of all the [`BytesLiteral`] parts contained in this value.
pub fn as_slice(&self) -> &[BytesLiteral] {
match &self.inner {
BytesLiteralValueInner::Single(value) => std::slice::from_ref(value),
BytesLiteralValueInner::Concatenated(value) => value.as_slice(),
}
}

/// Returns an iterator over all the [`BytesLiteral`] parts contained in this value.
pub fn parts(&self) -> impl Iterator<Item = &BytesLiteral> {
match &self.inner {
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_python_formatter/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::comments::Comments;
use crate::expression::string::QuoteChar;
use crate::string::QuoteChar;
use crate::PyFormatOptions;
use ruff_formatter::{Buffer, FormatContext, GroupId, SourceCode};
use ruff_source_file::Locator;
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_python_formatter/src/expression/binary_like.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ use crate::expression::parentheses::{
is_expression_parenthesized, write_in_parentheses_only_group_end_tag,
write_in_parentheses_only_group_start_tag, Parentheses,
};
use crate::expression::string::{AnyString, FormatString, StringLayout};
use crate::expression::OperatorPrecedence;
use crate::prelude::*;
use crate::preview::is_fix_power_op_line_length_enabled;
use crate::string::{AnyString, FormatString, StringLayout};

#[derive(Copy, Clone, Debug)]
pub(super) enum BinaryLike<'a> {
Expand Down
37 changes: 33 additions & 4 deletions crates/ruff_python_formatter/src/expression/expr_bytes_literal.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,47 @@
use ruff_formatter::FormatRuleWithOptions;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::ExprBytesLiteral;

use crate::comments::SourceComment;
use crate::expression::expr_string_literal::is_multiline_string;
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::expression::string::{AnyString, FormatString};
use crate::expression::parentheses::{
in_parentheses_only_group, NeedsParentheses, OptionalParentheses,
};
use crate::prelude::*;
use crate::string::{AnyString, FormatStringContinuation, StringLayout};

#[derive(Default)]
pub struct FormatExprBytesLiteral;
pub struct FormatExprBytesLiteral {
layout: StringLayout,
}

impl FormatRuleWithOptions<ExprBytesLiteral, PyFormatContext<'_>> for FormatExprBytesLiteral {
type Options = StringLayout;

fn with_options(mut self, options: Self::Options) -> Self {
self.layout = options;
self
}
}

impl FormatNodeRule<ExprBytesLiteral> for FormatExprBytesLiteral {
fn fmt_fields(&self, item: &ExprBytesLiteral, f: &mut PyFormatter) -> FormatResult<()> {
FormatString::new(&AnyString::Bytes(item)).fmt(f)
let ExprBytesLiteral { value, .. } = item;

match self.layout {
StringLayout::DocString => unreachable!("`ExprBytesLiteral` cannot be a docstring"),
StringLayout::Default => match value.as_slice() {
[] => unreachable!("Empty `ExprBytesLiteral`"),
[bytes_literal] => bytes_literal.format().fmt(f),
_ => in_parentheses_only_group(&FormatStringContinuation::new(
&AnyString::BytesLiteral(item),
))
.fmt(f),
},
StringLayout::ImplicitConcatenatedStringInBinaryLike => {
FormatStringContinuation::new(&AnyString::BytesLiteral(item)).fmt(f)
}
}
}

fn fmt_dangling_comments(
Expand Down
40 changes: 34 additions & 6 deletions crates/ruff_python_formatter/src/expression/expr_f_string.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,49 @@
use memchr::memchr2;

use crate::comments::SourceComment;
use ruff_formatter::FormatResult;
use ruff_formatter::FormatRuleWithOptions;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::ExprFString;

use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::comments::SourceComment;
use crate::expression::parentheses::{
in_parentheses_only_group, NeedsParentheses, OptionalParentheses,
};
use crate::prelude::*;

use super::string::{AnyString, FormatString};
use crate::string::{AnyString, FormatStringContinuation, StringLayout};

#[derive(Default)]
pub struct FormatExprFString;
pub struct FormatExprFString {
layout: StringLayout,
}

impl FormatRuleWithOptions<ExprFString, PyFormatContext<'_>> for FormatExprFString {
type Options = StringLayout;

fn with_options(mut self, options: Self::Options) -> Self {
self.layout = options;
self
}
}

impl FormatNodeRule<ExprFString> for FormatExprFString {
fn fmt_fields(&self, item: &ExprFString, f: &mut PyFormatter) -> FormatResult<()> {
FormatString::new(&AnyString::FString(item)).fmt(f)
let ExprFString { value, .. } = item;

match self.layout {
StringLayout::DocString => unreachable!("`ExprFString` cannot be a docstring"),
StringLayout::Default => match value.as_slice() {
[] => unreachable!("Empty `ExprFString`"),
[f_string_part] => f_string_part.format().fmt(f),
_ => in_parentheses_only_group(&FormatStringContinuation::new(
&AnyString::FString(item),
))
.fmt(f),
},
StringLayout::ImplicitConcatenatedStringInBinaryLike => {
FormatStringContinuation::new(&AnyString::FString(item)).fmt(f)
}
}
}

fn fmt_dangling_comments(
Expand Down
30 changes: 24 additions & 6 deletions crates/ruff_python_formatter/src/expression/expr_string_literal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ use ruff_python_ast::ExprStringLiteral;
use ruff_text_size::{Ranged, TextLen, TextRange};

use crate::comments::SourceComment;
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::expression::string::{
AnyString, FormatString, StringLayout, StringPrefix, StringQuotes,
use crate::expression::parentheses::{
in_parentheses_only_group, NeedsParentheses, OptionalParentheses,
};
use crate::prelude::*;
use crate::string::{
AnyString, FormatStringContinuation, StringLayout, StringPrefix, StringQuotes,
};


#[derive(Default)]
pub struct FormatExprStringLiteral {
Expand All @@ -26,9 +29,24 @@ impl FormatRuleWithOptions<ExprStringLiteral, PyFormatContext<'_>> for FormatExp

impl FormatNodeRule<ExprStringLiteral> for FormatExprStringLiteral {
fn fmt_fields(&self, item: &ExprStringLiteral, f: &mut PyFormatter) -> FormatResult<()> {
FormatString::new(&AnyString::String(item))
.with_layout(self.layout)
.fmt(f)
let ExprStringLiteral { value, .. } = item;

let _parent_docstring_quote_style = f.context().docstring();
let _locator = f.context().locator();

match self.layout {
StringLayout::Default | StringLayout::DocString => match value.as_slice() {
[] => unreachable!("Empty `ExprStringLiteral`"),
[string_literal] => string_literal.format().with_options(self.layout).fmt(f),
_ => in_parentheses_only_group(&FormatStringContinuation::new(
&AnyString::StringLiteral(item),
))
.fmt(f),
},
StringLayout::ImplicitConcatenatedStringInBinaryLike => {
FormatStringContinuation::new(&AnyString::StringLiteral(item)).fmt(f)
}
}
}

fn fmt_dangling_comments(
Expand Down
1 change: 0 additions & 1 deletion crates/ruff_python_formatter/src/expression/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ pub(crate) mod expr_yield;
pub(crate) mod expr_yield_from;
mod operator;
pub(crate) mod parentheses;
pub(crate) mod string;

#[derive(Copy, Clone, PartialEq, Eq, Default)]
pub struct FormatExpr {
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_python_formatter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ mod prelude;
mod preview;
mod shared_traits;
pub(crate) mod statement;
pub(crate) mod string;
pub(crate) mod type_param;
mod verbatim;

Expand Down
16 changes: 14 additions & 2 deletions crates/ruff_python_formatter/src/other/bytes_literal.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
use ruff_python_ast::BytesLiteral;
use ruff_text_size::Ranged;

use crate::prelude::*;
use crate::string::{Quoting, StringPart};

#[derive(Default)]
pub struct FormatBytesLiteral;

impl FormatNodeRule<BytesLiteral> for FormatBytesLiteral {
fn fmt_fields(&self, _item: &BytesLiteral, _f: &mut PyFormatter) -> FormatResult<()> {
unreachable!("Handled inside of `FormatExprBytesLiteral`");
fn fmt_fields(&self, item: &BytesLiteral, f: &mut PyFormatter) -> FormatResult<()> {
let locator = f.context().locator();
let parent_docstring_quote_style = f.context().docstring();

StringPart::from_source(item.range(), &locator)
.normalize(
Quoting::CanChange,
&locator,
f.options().quote_style(),
parent_docstring_quote_style,
)
.fmt(f)
}
}
50 changes: 47 additions & 3 deletions crates/ruff_python_formatter/src/other/f_string.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,56 @@
use ruff_python_ast::FString;
use ruff_python_ast::{FString, FStringElement, FStringExpressionElement};
use ruff_text_size::Ranged;

use crate::prelude::*;
use crate::string::{Quoting, StringPart};

#[derive(Default)]
pub struct FormatFString;

impl FormatNodeRule<FString> for FormatFString {
fn fmt_fields(&self, _item: &FString, _f: &mut PyFormatter) -> FormatResult<()> {
unreachable!("Handled inside of `FormatExprFString`");
fn fmt_fields(&self, item: &FString, f: &mut PyFormatter) -> FormatResult<()> {
let locator = f.context().locator();
let parent_docstring_quote_style = f.context().docstring();

let unprefixed = locator
.slice(item.range())
.trim_start_matches(|c| c != '"' && c != '\'');
let triple_quoted = unprefixed.starts_with(r#"""""#) || unprefixed.starts_with(r"'''");
let quoting = if item.elements.iter().any(|element| match element {
FStringElement::Expression(FStringExpressionElement { range, .. }) => {
let string_content = locator.slice(*range);
if triple_quoted {
string_content.contains(r#"""""#) || string_content.contains("'''")
} else {
string_content.contains(['"', '\''])
}
}
FStringElement::Literal(_) => false,
}) {
Quoting::Preserve
} else {
Quoting::CanChange
};

let result = StringPart::from_source(item.range(), &locator)
.normalize(
quoting,
&locator,
f.options().quote_style(),
parent_docstring_quote_style,
)
.fmt(f);

// TODO(dhruvmanila): With PEP 701, comments can be inside f-strings.
// This is to mark all of those comments as formatted but we need to
// figure out how to handle them. Note that this needs to be done only
// after the f-string is formatted, so only for all the non-formatted
// comments.
let comments = f.context().comments();
item.elements.iter().for_each(|value| {
comments.mark_verbatim_node_comments_formatted(value.into());
});

result
}
}
32 changes: 32 additions & 0 deletions crates/ruff_python_formatter/src/other/f_string_part.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use ruff_formatter::{FormatOwnedWithRule, FormatRefWithRule};
use ruff_python_ast::FStringPart;

use crate::prelude::*;

#[derive(Default)]
pub struct FormatFStringPart;

impl FormatRule<FStringPart, PyFormatContext<'_>> for FormatFStringPart {
fn fmt(&self, item: &FStringPart, f: &mut PyFormatter) -> FormatResult<()> {
match item {
FStringPart::Literal(string_literal) => string_literal.format().fmt(f),
FStringPart::FString(f_string) => f_string.format().fmt(f),
}
}
}

impl<'ast> AsFormat<PyFormatContext<'ast>> for FStringPart {
type Format<'a> = FormatRefWithRule<'a, FStringPart, FormatFStringPart, PyFormatContext<'ast>>;

fn format(&self) -> Self::Format<'_> {
FormatRefWithRule::new(self, FormatFStringPart)
}
}

impl<'ast> IntoFormat<PyFormatContext<'ast>> for FStringPart {
type Format = FormatOwnedWithRule<FStringPart, FormatFStringPart, PyFormatContext<'ast>>;

fn into_format(self) -> Self::Format {
FormatOwnedWithRule::new(self, FormatFStringPart)
}
}
1 change: 1 addition & 0 deletions crates/ruff_python_formatter/src/other/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub(crate) mod decorator;
pub(crate) mod elif_else_clause;
pub(crate) mod except_handler_except_handler;
pub(crate) mod f_string;
pub(crate) mod f_string_part;
pub(crate) mod identifier;
pub(crate) mod keyword;
pub(crate) mod match_case;
Expand Down
Loading

0 comments on commit e54f469

Please sign in to comment.