Skip to content

Commit

Permalink
Add optional parentheses IR
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaReiser committed Sep 18, 2023
1 parent 0346e78 commit 118ebee
Show file tree
Hide file tree
Showing 12 changed files with 405 additions and 102 deletions.
144 changes: 144 additions & 0 deletions crates/ruff_formatter/src/builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1453,6 +1453,150 @@ impl<Context> std::fmt::Debug for Group<'_, Context> {
}
}

/// Content that may get parenthesized if it exceeds the configured line width but only if the parenthesized
/// layout doesn't exceed the line width too, in which case it falls back to the flat layout.
///
/// This IR is identical to the following [`best_fitting`] layout but is implemented as custom IR for
/// best performance.
///
/// ```rust
/// # use ruff_formatter::prelude::*;
/// # use ruff_formatter::format_args;
///
/// let format_expression = format_with(|f: &mut Formatter<SimpleFormatContext>| token("A long string").fmt(f));
/// let _ = best_fitting![
/// // ---------------------------------------------------------------------
/// // Variant 1:
/// // Try to fit the expression without any parentheses
/// group(&format_expression),
/// // ---------------------------------------------------------------------
/// // Variant 2:
/// // Try to fit the expression by adding parentheses and indenting the expression.
/// group(&format_args![
/// token("("),
/// soft_block_indent(&format_expression),
/// token(")")
/// ])
/// .should_expand(true),
/// // ---------------------------------------------------------------------
/// // Variant 3: Fallback, no parentheses
/// // Expression doesn't fit regardless of adding the parentheses. Remove the parentheses again.
/// group(&format_expression).should_expand(true)
/// ]
/// // Measure all lines, to avoid that the printer decides that this fits right after hitting
/// // the `(`.
/// .with_mode(BestFittingMode::AllLines) ;
/// ```
///
/// The element breaks from left-to-right because it uses the unintended version as *expanded* layout, the same as the above showed best fitting example.
///
/// ## Examples
///
/// ### Content that fits into the configured line width.
///
/// ```rust
/// # use ruff_formatter::prelude::*;
/// # use ruff_formatter::{format, PrintResult, write};
///
/// # fn main() -> FormatResult<()> {
/// let formatted = format!(SimpleFormatContext::default(), [format_with(|f| {
/// write!(f, [
/// token("aLongerVariableName = "),
/// best_fit_parenthesize(&token("'a string that fits into the configured line width'"))
/// ])
/// })])?;
///
/// assert_eq!(formatted.print()?.as_code(), "aLongerVariableName = 'a string that fits into the configured line width'");
/// # Ok(())
/// # }
/// ```
///
/// ### Content that fits parenthesized
///
/// ```rust
/// # use ruff_formatter::prelude::*;
/// # use ruff_formatter::{format, PrintResult, write};
///
/// # fn main() -> FormatResult<()> {
/// let formatted = format!(SimpleFormatContext::default(), [format_with(|f| {
/// write!(f, [
/// token("aLongerVariableName = "),
/// best_fit_parenthesize(&token("'a string that exceeds configured line width but fits parenthesized'"))
/// ])
/// })])?;
///
/// assert_eq!(formatted.print()?.as_code(), "aLongerVariableName = (\n\t'a string that exceeds configured line width but fits parenthesized'\n)");
/// # Ok(())
/// # }
/// ```
///
/// ### Content that exceeds the line width, parenthesized or not
///
/// ```rust
/// # use ruff_formatter::prelude::*;
/// # use ruff_formatter::{format, PrintResult, write};
///
/// # fn main() -> FormatResult<()> {
/// let formatted = format!(SimpleFormatContext::default(), [format_with(|f| {
/// write!(f, [
/// token("aLongerVariableName = "),
/// best_fit_parenthesize(&token("'a string that exceeds the configured line width and even parenthesizing doesn't make it fit'"))
/// ])
/// })])?;
///
/// assert_eq!(formatted.print()?.as_code(), "aLongerVariableName = 'a string that exceeds the configured line width and even parenthesizing doesn't make it fit'");
/// # Ok(())
/// # }
/// ```
#[inline]
pub fn best_fit_parenthesize<Context>(
content: &impl Format<Context>,
) -> BestFitParenthesize<Context> {
BestFitParenthesize {
content: Argument::new(content),
group_id: None,
}
}

#[derive(Copy, Clone)]
pub struct BestFitParenthesize<'a, Context> {
content: Argument<'a, Context>,
group_id: Option<GroupId>,
}

impl<Context> BestFitParenthesize<'_, Context> {
/// Optional ID that can be used in conditional content that supports [`Condition`] to gate content
/// depending on whether the parentheses are rendered (flat: no parentheses, expanded: parentheses).
#[must_use]
pub fn with_group_id(mut self, group_id: Option<GroupId>) -> Self {
self.group_id = group_id;
self
}
}

impl<Context> Format<Context> for BestFitParenthesize<'_, Context> {
fn fmt(&self, f: &mut Formatter<Context>) -> FormatResult<()> {
f.write_element(FormatElement::Tag(StartBestFitParenthesize {
id: self.group_id,
}));

Arguments::from(&self.content).fmt(f)?;

f.write_element(FormatElement::Tag(EndBesetFitParenthesize));

Ok(())
}
}

impl<Context> std::fmt::Debug for BestFitParenthesize<'_, Context> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BestFitParenthesize")
.field("group_id", &self.group_id)
.field("content", &"{{content}}")
.finish()
}
}

/// Sets the `condition` for the group. The element will behave as a regular group if `condition` is met,
/// and as *ungrouped* content if the condition is not met.
///
Expand Down
29 changes: 29 additions & 0 deletions crates/ruff_formatter/src/format_element/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ impl Document {
ConditionalGroup(&'a tag::ConditionalGroup),
FitsExpanded(&'a tag::FitsExpanded),
BestFitting,
BestFitParenthesize { expanded: bool },
}

fn expand_parent(enclosing: &[Enclosing]) {
Expand Down Expand Up @@ -64,6 +65,18 @@ impl Document {
Some(Enclosing::Group(group)) => !group.mode().is_flat(),
_ => false,
},
FormatElement::Tag(Tag::StartBestFitParenthesize { .. }) => {
enclosing.push(Enclosing::BestFitParenthesize { expanded: expands });
expands = false;
continue;
}

FormatElement::Tag(Tag::EndBesetFitParenthesize) => {
if let Some(Enclosing::BestFitParenthesize { expanded }) = enclosing.pop() {
expands = expanded;
}
continue;
}
FormatElement::Tag(Tag::StartConditionalGroup(group)) => {
enclosing.push(Enclosing::ConditionalGroup(group));
false
Expand Down Expand Up @@ -499,6 +512,21 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
}
}

StartBestFitParenthesize { id } => {
write!(f, [token("best_fit_parenthesize(")])?;

if let Some(group_id) = id {
write!(
f,
[
text(&std::format!("\"{group_id:?}\""), None),
token(","),
space(),
]
)?;
}
}

StartConditionalGroup(group) => {
write!(
f,
Expand Down Expand Up @@ -607,6 +635,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
| EndIndent
| EndGroup
| EndConditionalGroup
| EndBesetFitParenthesize
| EndLineSuffix
| EndDedent
| EndFitsExpanded
Expand Down
12 changes: 12 additions & 0 deletions crates/ruff_formatter/src/format_element/tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ pub enum Tag {

StartFitsExpanded(FitsExpanded),
EndFitsExpanded,

/// Parenthesizes the content but only if adding the parentheses and indenting the content
/// makes the content fit in the configured line width.
StartBestFitParenthesize {
id: Option<GroupId>,
},
EndBesetFitParenthesize,
}

impl Tag {
Expand All @@ -103,6 +110,7 @@ impl Tag {
| Tag::StartVerbatim(_)
| Tag::StartLabelled(_)
| Tag::StartFitsExpanded(_)
| Tag::StartBestFitParenthesize { .. }
)
}

Expand All @@ -129,6 +137,9 @@ impl Tag {
StartVerbatim(_) | EndVerbatim => TagKind::Verbatim,
StartLabelled(_) | EndLabelled => TagKind::Labelled,
StartFitsExpanded { .. } | EndFitsExpanded => TagKind::FitsExpanded,
StartBestFitParenthesize { .. } | EndBesetFitParenthesize => {
TagKind::BestFitParenthesize
}
}
}
}
Expand All @@ -152,6 +163,7 @@ pub enum TagKind {
Verbatim,
Labelled,
FitsExpanded,
BestFitParenthesize,
}

#[derive(Debug, Copy, Default, Clone, Eq, PartialEq)]
Expand Down
96 changes: 96 additions & 0 deletions crates/ruff_formatter/src/printer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,70 @@ impl<'a> Printer<'a> {
stack.push(TagKind::Group, args.with_print_mode(print_mode));
}

FormatElement::Tag(StartBestFitParenthesize { id }) => {
const OPEN_PAREN: FormatElement = FormatElement::Token { text: "(" };
const INDENT: FormatElement = FormatElement::Tag(Tag::StartIndent);
const HARD_LINE_BREAK: FormatElement = FormatElement::Line(LineMode::Hard);

let print_mode = if self.flat_group_print_mode(
TagKind::BestFitParenthesize,
*id,
args,
queue,
stack,
)? == PrintMode::Expanded
{
if let Some(id) = id {
self.state
.group_modes
.insert_print_mode(*id, PrintMode::Expanded);
}

stack.push(
TagKind::BestFitParenthesize,
args.with_measure_mode(MeasureMode::AllLines),
);
queue.extend_back(&[OPEN_PAREN, INDENT, HARD_LINE_BREAK]);
let fits_expanded = self.fits(queue, stack)?;
queue.pop_slice();
stack.pop(TagKind::BestFitParenthesize)?;

if fits_expanded {
PrintMode::Expanded
} else {
PrintMode::Flat
}
} else {
PrintMode::Flat
};

if let Some(id) = id {
self.state.group_modes.insert_print_mode(*id, print_mode);
}

if print_mode.is_expanded() {
queue.extend_back(&[OPEN_PAREN, INDENT, HARD_LINE_BREAK]);
}

stack.push(
TagKind::BestFitParenthesize,
args.with_print_mode(print_mode),
);
}

FormatElement::Tag(Tag::EndBesetFitParenthesize) => {
if args.mode().is_expanded() {
const HARD_LINE_BREAK: FormatElement = FormatElement::Line(LineMode::Hard);
const CLOSE_PAREN: FormatElement = FormatElement::Token { text: ")" };

// Pop the indent
stack.pop(TagKind::Indent)?;
queue.extend_back(&[HARD_LINE_BREAK, CLOSE_PAREN]);
}

stack.pop(TagKind::BestFitParenthesize)?;
}

FormatElement::Tag(StartConditionalGroup(group)) => {
let condition = group.condition();
let expected_mode = match condition.group_id {
Expand Down Expand Up @@ -1182,6 +1246,38 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
return Ok(self.fits_group(TagKind::Group, group.mode(), group.id(), args));
}

FormatElement::Tag(StartBestFitParenthesize { id }) => {
if let Some(id) = id {
self.printer
.state
.group_modes
.insert_print_mode(*id, args.mode());
}

// Don't use the parenthesized with indent layout even when measuring expanded mode similar to `BestFitting`.
// This is to expand the left and not right after the `(` parentheses (it is okay to expand after the content that it wraps).
self.stack.push(TagKind::BestFitParenthesize, args);
}

FormatElement::Tag(EndBesetFitParenthesize) => {
// If this is the end tag of the outer most parentheses for which we measure if it fits,
// pop the indent.
if args.mode().is_expanded() && self.stack.top_kind() == Some(TagKind::Indent) {
self.stack.pop(TagKind::Indent).unwrap();
let unindented = self.stack.pop(TagKind::BestFitParenthesize)?;

// There's a hard line break after the indent but don't return `Fits::Yes` here
// to ensure any trailing comments (that, unfortunately, are attached to the statement and not the expression)
// fit too.
self.state.line_width = 0;
self.state.pending_indent = unindented.indention();

return Ok(self.fits_text(Text::Token(")"), unindented));
}

self.stack.pop(TagKind::BestFitParenthesize)?;
}

FormatElement::Tag(StartConditionalGroup(group)) => {
let condition = group.condition();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,32 @@ def test():
if True:
VLM_m2m = VLM.m2m_also_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.through
allows_group_by_select_index = self.connection.features.allows_group_by_select_index


# This is a deviation from Black:
# Black keeps the comment inside of the parentheses, making it more likely to exceed the line width.
# Ruff renders the comment after the parentheses, giving it more space to fit.
if True:
if True:
if True:
# Black layout
model.config.use_cache = (
False # FSTM still requires this hack -> FSTM should probably be refactored s
)
# Ruff layout
model.config.use_cache = False # FSTM still requires this hack -> FSTM should probably be refactored s


# Regression test for https://github.com/astral-sh/ruff/issues/7463
mp3fmt="<span style=\"color: grey\"><a href=\"{}\" id=\"audiolink\">listen</a></span></br>\n"

# Regression test for https://github.com/astral-sh/ruff/issues/7067
def f():
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = (
True
)

# Regression test for https://github.com/astral-sh/ruff/issues/7462
if grid is not None:
rgrid = (rgrid.rio.reproject_match(grid, nodata=fillvalue) # rio.reproject nodata is use to initlialize the destination array
.where(~grid.isnull()))
Loading

0 comments on commit 118ebee

Please sign in to comment.