diff --git a/crates/ruff_formatter/src/builders.rs b/crates/ruff_formatter/src/builders.rs index 3e34b4e25f..f5d81eb726 100644 --- a/crates/ruff_formatter/src/builders.rs +++ b/crates/ruff_formatter/src/builders.rs @@ -2155,15 +2155,18 @@ impl std::fmt::Debug for IndentIfGroupBreaks<'_, Context> { } } -/// Changes the definition of *fits* for `content`. Instead of measuring it in *flat*, measure it with -/// all line breaks expanded and test if no line exceeds the line width. The [`FitsExpanded`] acts -/// as a expands boundary similar to best fitting, meaning that a [`hard_line_break`] will not cause the parent group to expand. +/// Changes the definition of *fits* for `content`. It measures the width of all lines and allows +/// the content inside of the [`fits_expanded`] to exceed the configured line width. The content +/// coming before and after [`fits_expanded`] must fit into the configured line width. +/// +/// The [`fits_expanded`] acts as a expands boundary similar to best fitting, +/// meaning that a [`hard_line_break`] will not cause the parent group to expand. /// /// Useful in conjunction with a group with a condition. /// /// ## Examples -/// The outer group with the binary expression remains *flat* regardless of the array expression -/// that spans multiple lines. +/// The outer group with the binary expression remains *flat* regardless of the array expression that +/// spans multiple lines with items exceeding the configured line width. /// /// ``` /// # use ruff_formatter::{format, format_args, LineWidth, SimpleFormatOptions, write}; @@ -2183,7 +2186,7 @@ impl std::fmt::Debug for IndentIfGroupBreaks<'_, Context> { /// token("["), /// soft_block_indent(&format_args![ /// token("a,"), space(), token("# comment"), expand_parent(), soft_line_break_or_space(), -/// token("b") +/// token("'A very long string that exceeds the configured line width of 80 characters but the enclosing binary expression still fits.'") /// ]), /// token("]") /// ])) @@ -2194,7 +2197,7 @@ impl std::fmt::Debug for IndentIfGroupBreaks<'_, Context> { /// let formatted = format!(SimpleFormatContext::default(), [content])?; /// /// assert_eq!( -/// "a + [\n\ta, # comment\n\tb\n]", +/// "a + [\n\ta, # comment\n\t'A very long string that exceeds the configured line width of 80 characters but the enclosing binary expression still fits.'\n]", /// formatted.print()?.as_code() /// ); /// # Ok(()) diff --git a/crates/ruff_formatter/src/format_element/tag.rs b/crates/ruff_formatter/src/format_element/tag.rs index 6e120a6381..e922425d20 100644 --- a/crates/ruff_formatter/src/format_element/tag.rs +++ b/crates/ruff_formatter/src/format_element/tag.rs @@ -84,11 +84,14 @@ pub enum Tag { StartFitsExpanded(FitsExpanded), EndFitsExpanded, + /// Marks the start and end of a best-fitting variant. StartBestFittingEntry, EndBestFittingEntry, /// Parenthesizes the content but only if adding the parentheses and indenting the content /// makes the content fit in the configured line width. + /// + /// See [`crate::builders::best_fit_parenthesize`] for an in-depth explanation. StartBestFitParenthesize { id: Option, }, diff --git a/crates/ruff_formatter/src/format_extensions.rs b/crates/ruff_formatter/src/format_extensions.rs index da2d115001..e363757743 100644 --- a/crates/ruff_formatter/src/format_extensions.rs +++ b/crates/ruff_formatter/src/format_extensions.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - use crate::prelude::*; use std::cell::OnceCell; use std::marker::PhantomData; diff --git a/crates/ruff_formatter/src/printer/mod.rs b/crates/ruff_formatter/src/printer/mod.rs index 24f5f31c30..4535234fa3 100644 --- a/crates/ruff_formatter/src/printer/mod.rs +++ b/crates/ruff_formatter/src/printer/mod.rs @@ -1183,7 +1183,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { // line break should be printed as regular line break return Ok(Fits::Yes); } - MeasureMode::AllLines => { + MeasureMode::AllLines | MeasureMode::AllLinesAllowTextOverflow => { // Continue measuring on the next line self.state.line_width = 0; self.state.pending_indent = args.indention(); @@ -1354,9 +1354,11 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { } FormatElement::Tag(StartLineSuffix { reserved_width }) => { - self.state.line_width += reserved_width; - if self.state.line_width > self.options().line_width.into() { - return Ok(Fits::No); + if *reserved_width > 0 { + self.state.line_width += reserved_width; + if self.state.line_width > self.options().line_width.into() { + return Ok(Fits::No); + } } self.queue.skip_content(TagKind::LineSuffix); self.state.has_line_suffix = true; @@ -1370,32 +1372,42 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { condition, propagate_expand, })) => { - let condition_met = match condition { - Some(condition) => { - let group_mode = match condition.group_id { - Some(group_id) => self.group_modes().get_print_mode(group_id)?, - None => args.mode(), + match args.mode() { + PrintMode::Expanded => { + // As usual, nothing to measure + self.stack.push(TagKind::FitsExpanded, args); + } + PrintMode::Flat => { + let condition_met = match condition { + Some(condition) => { + let group_mode = match condition.group_id { + Some(group_id) => { + self.group_modes().get_print_mode(group_id)? + } + None => args.mode(), + }; + + condition.mode == group_mode + } + None => true, }; - condition.mode == group_mode - } - None => true, - }; + if condition_met { + // Measure in fully expanded mode and allow overflows + self.stack.push( + TagKind::FitsExpanded, + args.with_measure_mode(MeasureMode::AllLinesAllowTextOverflow) + .with_print_mode(PrintMode::Expanded), + ); + } else { + if propagate_expand.get() { + return Ok(Fits::No); + } - if condition_met { - // Measure in fully expanded mode. - self.stack.push( - TagKind::FitsExpanded, - args.with_print_mode(PrintMode::Expanded) - .with_measure_mode(MeasureMode::AllLines), - ); - } else { - if propagate_expand.get() && args.mode().is_flat() { - return Ok(Fits::No); + // As usual + self.stack.push(TagKind::FitsExpanded, args); + } } - - // As usual - self.stack.push(TagKind::FitsExpanded, args); } } @@ -1482,7 +1494,8 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { } match args.measure_mode() { MeasureMode::FirstLine => return Fits::Yes, - MeasureMode::AllLines => { + MeasureMode::AllLines + | MeasureMode::AllLinesAllowTextOverflow => { self.state.line_width = 0; continue; } @@ -1498,7 +1511,9 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { } } - if self.state.line_width > self.options().line_width.into() { + if self.state.line_width > self.options().line_width.into() + && !args.measure_mode().allows_text_overflow() + { return Fits::No; } @@ -1601,6 +1616,17 @@ enum MeasureMode { /// The content only fits if none of the lines exceed the print width. Lines are terminated by either /// a hard line break or a soft line break in [`PrintMode::Expanded`]. AllLines, + + /// Measures all lines and allows lines to exceed the configured line width. Useful when it only matters + /// whether the content *before* and *after* fits. + AllLinesAllowTextOverflow, +} + +impl MeasureMode { + /// Returns `true` if this mode allows text exceeding the configured line width. + const fn allows_text_overflow(self) -> bool { + matches!(self, MeasureMode::AllLinesAllowTextOverflow) + } } impl From for MeasureMode { diff --git a/crates/ruff_formatter/src/printer/printer_options/mod.rs b/crates/ruff_formatter/src/printer/printer_options/mod.rs index c73ca1c62d..efbe850cbf 100644 --- a/crates/ruff_formatter/src/printer/printer_options/mod.rs +++ b/crates/ruff_formatter/src/printer/printer_options/mod.rs @@ -120,7 +120,6 @@ impl SourceMapGeneration { } } -#[allow(dead_code)] #[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum LineEnding { diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py index b77a52689d..3d08feaaf7 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py @@ -394,3 +394,15 @@ z = ( # c: and this comment + a ) + +# Test for https://github.com/astral-sh/ruff/issues/7431 +if True: + if True: + if True: + if True: + msg += " " + _( + "Since the role is not mentionable, it will be momentarily made mentionable " + "when announcing a streamalert. Please make sure I have the correct " + "permissions to manage this role, or else members of this role won't receive " + "a notification." + ) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unsplittable.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unsplittable.py index 4d46684bf5..b5f2c161c8 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unsplittable.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unsplittable.py @@ -94,8 +94,3 @@ 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())) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assert.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assert.py index b5ef028af1..ac127dd44a 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assert.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assert.py @@ -153,3 +153,19 @@ def test(): key9: value9, } ), "Not what we expected and the message is too long to fit in one lineeeeeeeeeeeeeee" + +# Test for https://github.com/astral-sh/ruff/issues/7246 +assert items == [ + "a very very very very very very very very very very very very very very very long string", +] + +assert package.files == [ + { + "file": "pytest-3.5.0-py2.py3-none-any.whl", + "hash": "sha256:6266f87ab64692112e5477eba395cfedda53b1933ccd29478e671e73b420c19c", # noqa: E501 + }, + { + "file": "pytest-3.5.0.tar.gz", + "hash": "sha256:fae491d1874f199537fd5872b5e1f0e74a009b979df9d53d1553fd03da1703e1", # noqa: E501 + }, +] diff --git a/crates/ruff_python_formatter/src/expression/parentheses.rs b/crates/ruff_python_formatter/src/expression/parentheses.rs index 134d4dcbb5..005b3efa05 100644 --- a/crates/ruff_python_formatter/src/expression/parentheses.rs +++ b/crates/ruff_python_formatter/src/expression/parentheses.rs @@ -155,32 +155,34 @@ impl<'content, 'ast> FormatParenthesized<'content, 'ast> { impl<'ast> Format> for FormatParenthesized<'_, 'ast> { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { - let inner = format_with(|f| { + let current_level = f.context().node_level(); + + let content = format_with(|f| { group(&format_args![ - token(self.left), dangling_open_parenthesis_comments(self.comments), - soft_block_indent(&Arguments::from(&self.content)), - token(self.right) + soft_block_indent(&Arguments::from(&self.content)) ]) .fmt(f) }); - let current_level = f.context().node_level(); + let inner = format_with(|f| { + if let NodeLevel::Expression(Some(group_id)) = current_level { + // Use fits expanded if there's an enclosing group that adds the optional parentheses. + // This ensures that expanding this parenthesized expression does not expand the optional parentheses group. + write!( + f, + [fits_expanded(&content) + .with_condition(Some(Condition::if_group_fits_on_line(group_id)))] + ) + } else { + // It's not necessary to wrap the content if it is not inside of an optional_parentheses group. + content.fmt(f) + } + }); let mut f = WithNodeLevel::new(NodeLevel::ParenthesizedExpression, f); - if let NodeLevel::Expression(Some(group_id)) = current_level { - // Use fits expanded if there's an enclosing group that adds the optional parentheses. - // This ensures that expanding this parenthesized expression does not expand the optional parentheses group. - write!( - f, - [fits_expanded(&inner) - .with_condition(Some(Condition::if_group_fits_on_line(group_id)))] - ) - } else { - // It's not necessary to wrap the content if it is not inside of an optional_parentheses group. - write!(f, [inner]) - } + write!(f, [token(self.left), inner, token(self.right)]) } } diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens3.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens3.py.snap deleted file mode 100644 index d13dff5ee4..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens3.py.snap +++ /dev/null @@ -1,77 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/trailing_comma_optional_parens3.py ---- -## Input - -```py -if True: - if True: - if True: - return _( - "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas " - + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.", - "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", - ) % {"reported_username": reported_username, "report_reason": report_reason} -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,8 +1,14 @@ - if True: - if True: - if True: -- return _( -- "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas " -- + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.", -- "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", -- ) % {"reported_username": reported_username, "report_reason": report_reason} -+ return ( -+ _( -+ "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas " -+ + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.", -+ "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", -+ ) -+ % { -+ "reported_username": reported_username, -+ "report_reason": report_reason, -+ } -+ ) -``` - -## Ruff Output - -```py -if True: - if True: - if True: - return ( - _( - "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas " - + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.", - "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", - ) - % { - "reported_username": reported_username, - "report_reason": report_reason, - } - ) -``` - -## Black Output - -```py -if True: - if True: - if True: - return _( - "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas " - + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.", - "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", - ) % {"reported_username": reported_username, "report_reason": report_reason} -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap index 849e9974cb..2c2ccd9b09 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap @@ -400,6 +400,18 @@ z = ( # c: and this comment + a ) + +# Test for https://github.com/astral-sh/ruff/issues/7431 +if True: + if True: + if True: + if True: + msg += " " + _( + "Since the role is not mentionable, it will be momentarily made mentionable " + "when announcing a streamalert. Please make sure I have the correct " + "permissions to manage this role, or else members of this role won't receive " + "a notification." + ) ``` ## Output @@ -849,6 +861,18 @@ z = ( # c: and this comment + a ) + +# Test for https://github.com/astral-sh/ruff/issues/7431 +if True: + if True: + if True: + if True: + msg += " " + _( + "Since the role is not mentionable, it will be momentarily made mentionable " + "when announcing a streamalert. Please make sure I have the correct " + "permissions to manage this role, or else members of this role won't receive " + "a notification." + ) ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__unsplittable.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__unsplittable.py.snap index cd42bf3a36..1189d13a37 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__unsplittable.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__unsplittable.py.snap @@ -100,11 +100,6 @@ 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())) ``` ## Output @@ -227,15 +222,6 @@ 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 - ).where( # rio.reproject nodata is use to initlialize the destination array - ~grid.isnull() - ) ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__assert.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__assert.py.snap index 534b604e8a..8f502dfe0a 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__assert.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__assert.py.snap @@ -159,6 +159,22 @@ def test(): key9: value9, } ), "Not what we expected and the message is too long to fit in one lineeeeeeeeeeeeeee" + +# Test for https://github.com/astral-sh/ruff/issues/7246 +assert items == [ + "a very very very very very very very very very very very very very very very long string", +] + +assert package.files == [ + { + "file": "pytest-3.5.0-py2.py3-none-any.whl", + "hash": "sha256:6266f87ab64692112e5477eba395cfedda53b1933ccd29478e671e73b420c19c", # noqa: E501 + }, + { + "file": "pytest-3.5.0.tar.gz", + "hash": "sha256:fae491d1874f199537fd5872b5e1f0e74a009b979df9d53d1553fd03da1703e1", # noqa: E501 + }, +] ``` ## Output @@ -326,6 +342,23 @@ def test(): key9: value9, } ), "Not what we expected and the message is too long to fit in one lineeeeeeeeeeeeeee" + + +# Test for https://github.com/astral-sh/ruff/issues/7246 +assert items == [ + "a very very very very very very very very very very very very very very very long string", +] + +assert package.files == [ + { + "file": "pytest-3.5.0-py2.py3-none-any.whl", + "hash": "sha256:6266f87ab64692112e5477eba395cfedda53b1933ccd29478e671e73b420c19c", # noqa: E501 + }, + { + "file": "pytest-3.5.0.tar.gz", + "hash": "sha256:fae491d1874f199537fd5872b5e1f0e74a009b979df9d53d1553fd03da1703e1", # noqa: E501 + }, +] ```