Skip to content

Fix lambda formatting in interpolated string expressions#25144

Merged
ntBre merged 7 commits into
mainfrom
brent/lambda-f-string-formatting-bug
May 15, 2026
Merged

Fix lambda formatting in interpolated string expressions#25144
ntBre merged 7 commits into
mainfrom
brent/lambda-f-string-formatting-bug

Conversation

@ntBre
Copy link
Copy Markdown
Contributor

@ntBre ntBre commented May 13, 2026

Summary

This PR fixes #24807 by making RemoveSoftLinesBuffer aware of
BestFitting and always selecting the most-flat variant, as suggested
in Micha's comment, assuming I interpreted it correctly.

As I noted on the issue, I was hoping I could get away with making a
local change to the lambda formatting, but this turned out to affect
the two other uses of RemoveSoftLinesBuffer too.

The formatting of the new regression tests:

def foo():
    subprocess.check_call(f"rm -rf {' '.join(map(lambda object_name: os.path.join(dest_path, object_name), objects_to_remove))}", shell=True)

def bar():
    lambda x=" ".join(
        map(lambda object_name: os.path.join(dest_path, object_name), objects_to_remove)
    ): ...

value = f"prefix {
    ' '.join(map(lambda object_name: os.path.join(dest_path, object_name), objects_to_remove))
} suffix"

now (nearly) matches the output before #21385:

git diff <(uvx [email protected] format - < fmt.py) <(./target/debug/ruff format - < fmt.py)
 def bar():
-    lambda x=" ".join(
-        map(lambda object_name: os.path.join(dest_path, object_name), objects_to_remove)
-    ): ...
+    lambda x=" ".join(map(lambda object_name: os.path.join(dest_path, object_name), objects_to_remove)): (
+        ...
+    )

 value = f"prefix {

with the only difference being that the body of this second lambda
still wraps in the new formatting.

Test Plan

New tests based on the reported issue and possibly the ecosystem check
on this PR

ntBre added 4 commits May 13, 2026 13:18
Summary
--

This PR fixes #24807 by making `RemoveSoftLinesBuffer` aware of
`BestFitting` and always selecting the most-flat variant, as suggested
in [Micha's comment], assuming I interpreted it correctly.

As I noted on the issue, I was hoping I could get away with making a
local change to the lambda formatting, but this turned out to affect
the two other uses of `RemoveSoftLinesBuffer` too.

The formatting of the new regression tests:

```py
def foo():
    subprocess.check_call(f"rm -rf {' '.join(map(lambda object_name: os.path.join(dest_path, object_name), objects_to_remove))}", shell=True)

def bar():
    lambda x=" ".join(
        map(lambda object_name: os.path.join(dest_path, object_name), objects_to_remove)
    ): ...

value = f"prefix {
    ' '.join(map(lambda object_name: os.path.join(dest_path, object_name), objects_to_remove))
} suffix"
```

now (nearly) matches the output before #21385:

```console
❯ git diff <(uvx [email protected] format - < fmt.py) <(./target/debug/ruff format - < fmt.py)
 def bar():
-    lambda x=" ".join(
-        map(lambda object_name: os.path.join(dest_path, object_name), objects_to_remove)
-    ): ...
+    lambda x=" ".join(map(lambda object_name: os.path.join(dest_path, object_name), objects_to_remove)): (
+        ...
+    )

 value = f"prefix {
```

with the only difference being that the body of this second lambda
still wraps in the new formatting.

Test Plan
--

New tests based on the reported issue and possibly the ecosystem check
on this PR

[Micha's comment]: #24807 (comment)
@ntBre ntBre added bug Something isn't working formatter Related to the formatter labels May 13, 2026
@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented May 13, 2026

ruff-ecosystem results

Formatter (stable)

✅ ecosystem check detected no format changes.

Formatter (preview)

✅ ecosystem check detected no format changes.

@ntBre
Copy link
Copy Markdown
Contributor Author

ntBre commented May 13, 2026

It seems that my first pass wasn't quite sufficient. Codex came up with an even more contrived example that demonstrated the missing handling for BestFitting elements inside Interned elements. I tried a couple of fixes that didn't feel very satisfactory before realizing most of this code originated in Biome and thinking to reference their current version of the code:

https://github.com/biomejs/biome/blob/05c26176573534a0abfa92d454d244f9569bc77d/crates/biome_formatter/src/buffer.rs#L597-L605

It looks like they ran into something similar, and I was able to backport enough of their code to get the recursive case working. I wonder if we need to adapt any of the other changes they've made too?

@ntBre ntBre marked this pull request as ready for review May 13, 2026 21:03
@ntBre ntBre requested a review from MichaReiser as a code owner May 13, 2026 21:03
Comment thread crates/ruff_formatter/src/buffer.rs
@ntBre ntBre enabled auto-merge (squash) May 15, 2026 13:40
@ntBre ntBre merged commit 25af028 into main May 15, 2026
44 checks passed
@ntBre ntBre deleted the brent/lambda-f-string-formatting-bug branch May 15, 2026 13:44
thejchap pushed a commit to thejchap/ruff that referenced this pull request May 23, 2026
…5144)

Summary
--

This PR fixes astral-sh#24807 by making `RemoveSoftLinesBuffer` aware of
`BestFitting` and always selecting the most-flat variant, as suggested
in [Micha's comment], assuming I interpreted it correctly.

As I noted on the issue, I was hoping I could get away with making a
local change to the lambda formatting, but this turned out to affect
the two other uses of `RemoveSoftLinesBuffer` too.

The formatting of the new regression tests:

```py
def foo():
    subprocess.check_call(f"rm -rf {' '.join(map(lambda object_name: os.path.join(dest_path, object_name), objects_to_remove))}", shell=True)

def bar():
    lambda x=" ".join(
        map(lambda object_name: os.path.join(dest_path, object_name), objects_to_remove)
    ): ...

value = f"prefix {
    ' '.join(map(lambda object_name: os.path.join(dest_path, object_name), objects_to_remove))
} suffix"
```

now (nearly) matches the output before astral-sh#21385:

```console
❯ git diff <(uvx [email protected] format - < fmt.py) <(./target/debug/ruff format - < fmt.py)
 def bar():
-    lambda x=" ".join(
-        map(lambda object_name: os.path.join(dest_path, object_name), objects_to_remove)
-    ): ...
+    lambda x=" ".join(map(lambda object_name: os.path.join(dest_path, object_name), objects_to_remove)): (
+        ...
+    )

 value = f"prefix {
```

with the only difference being that the body of this second lambda
still wraps in the new formatting.

Test Plan
--

New tests based on the reported issue and possibly the ecosystem check
on this PR

[Micha's comment]:
astral-sh#24807 (comment)
ntBre added a commit that referenced this pull request May 26, 2026
Summary
--

This PR fixes #25397 by dropping `StartBestFittingEntry` and
`EndBestFittingEntry` tags in
`RemoveSoftLinesBuffer`. #25144 changed `RemoveSoftLinesBuffer` to
inline the most-flat variant of
`BestFitting` elements, but it inlined them with their start and end
tags, which caused the problem
observed in #25397 when these tags became nested in another
`BestFitting` element, which interpreted
the first `End` tag as ending the outer `BestFitting` rather than the
nested one. You can see this
clearly in the IR. First, the diff between the 0.15.13 and 0.15.14 IR,
which reveals the unmatched
start tags:

<details>
<summary>0.15.13 vs main</summary>

```diff
@@ -38,29 +38,13 @@
                           "lambda ",
                           group(["arg"]),
                           ": ",
-                          best_fitting([
-                            [
-                              [
-                                <interned 1> [
-                                  "str(",
-                                  group([
-                                    indent([soft_line_break, group(["arg"])]),
-                                    soft_line_break
-                                  ]),
-                                  ")"
-                                ]
-                              ]
-                            ]
-                            [[group(expand: true, [<ref interned *1>])]]
-                            [
-                              [
-                                "(",
-                                indent([hard_line_break, <ref interned *1>]),
-                                hard_line_break,
-                                ")"
-                              ]
+                          [
+                            <interned 1> [
+                              "str(",
+                              group([indent([group(["arg"])])]),
+                              ")"
                             ]
-                          ]),
+                          ],
                           ", self.args"
                         ])
                       ])
@@ -105,27 +89,9 @@
                             "lambda ",
                             group(["arg"]),
                             ": ",
-                            best_fitting([
-                              [
-                                [
-                                  <interned 2> [
-                                    "str(",
-                                    group([
-                                      indent([soft_line_break, group(["arg"])]),
-                                      soft_line_break
-                                    ]),
-                                    ")"
-                                  ]
-                                ]
-                              ]
-                              [[group(expand: true, [<ref interned *2>])]]
-                              [
-                                [
-                                  "(",
-                                  indent([hard_line_break, <ref interned *2>]),
-                                  hard_line_break,
-                                  ")"
-                                ]
+                            [
+                              <interned 2> [
+                                "str(",
+                                group([indent([group(["arg"])])]),
+                                ")"
                               ]
-                            ]),
-                            ", self.args"
+                            ]
                           ])
+                          <START_WITHOUT_END<Group>>
                         ])
-                      ]),
-                      ")"
+                        <START_WITHOUT_END<Indent>>
+                      ])
+                      <START_WITHOUT_END<Group>>
                     ])
+                    <START_WITHOUT_END<Group>>
                   ])
-                ]),
-                ")})\""
-              ]),
-              hard_line_break,
-              ")"
+                  <START_WITHOUT_END<Indent>>
+                ])
+                <START_WITHOUT_END<Group>>
+              ])
+              <START_WITHOUT_END<Indent>>
             ])
-          ]
+            <START_WITHOUT_END<Group>>
+          ])
+          <START_WITHOUT_END<BestFittingEntry>>
         ]
       ])
     ]),
```

</details>

and then the diff between main and this PR, which shows the removed
brackets that represent the
nested tags:

<details>
<summary>main vs PR</summary>

```diff
@@ -38,12 +38,10 @@
                           "lambda ",
                           group(["arg"]),
                           ": ",
-                          [
-                            <interned 1> [
-                              "str(",
-                              group([indent([group(["arg"])])]),
-                              ")"
-                            ]
+                          <interned 1> [
+                            "str(",
+                            group([indent([group(["arg"])])]),
+                            ")"
                           ],
                           ", self.args"
                         ])
@@ -89,24 +87,15 @@
                             "lambda ",
                             group(["arg"]),
                             ": ",
-                            [
-                              <interned 2> [
-                                "str(",
-                                group([indent([group(["arg"])])]),
-                                ")"
-                              ]
-                            ]
+                            <interned 2> [
+                              "str(",
+                              group([indent([group(["arg"])])]),
+                              ")"
+                            ],
+                            ", self.args"
                           ])
-                          <START_WITHOUT_END<Group>>
                         ])
-                        <START_WITHOUT_END<Indent>>
-                      ])
-                      <START_WITHOUT_END<Group>>
+                      ]),
+                      ")"
                     ])
-                    <START_WITHOUT_END<Group>>
                   ])
-                  <START_WITHOUT_END<Indent>>
-                ])
-                <START_WITHOUT_END<Group>>
-              ])
-              <START_WITHOUT_END<Indent>>
+                ]),
+                ")})\""
+              ]),
+              hard_line_break,
+              ")"
             ])
-            <START_WITHOUT_END<Group>>
-          ])
-          <START_WITHOUT_END<BestFittingEntry>>
+          ]
         ]
       ])
     ]),
```

</details>

I think the diff between 0.15.13 and this PR is also nice to see because
it makes it a bit more
clear that the `BestFitting` element has been inlined:

<details>
<summary>0.15.13 vs this PR</summary>

```diff
@@ -38,29 +38,11 @@
                           "lambda ",
                           group(["arg"]),
                           ": ",
-                          best_fitting([
-                            [
-                              [
-                                <interned 1> [
-                                  "str(",
-                                  group([
-                                    indent([soft_line_break, group(["arg"])]),
-                                    soft_line_break
-                                  ]),
-                                  ")"
-                                ]
-                              ]
-                            ]
-                            [[group(expand: true, [<ref interned *1>])]]
-                            [
-                              [
-                                "(",
-                                indent([hard_line_break, <ref interned *1>]),
-                                hard_line_break,
-                                ")"
-                              ]
-                            ]
-                          ]),
+                          <interned 1> [
+                            "str(",
+                            group([indent([group(["arg"])])]),
+                            ")"
+                          ],
                           ", self.args"
                         ])
                       ])
@@ -105,29 +87,11 @@
                             "lambda ",
                             group(["arg"]),
                             ": ",
-                            best_fitting([
-                              [
-                                [
-                                  <interned 2> [
-                                    "str(",
-                                    group([
-                                      indent([soft_line_break, group(["arg"])]),
-                                      soft_line_break
-                                    ]),
-                                    ")"
-                                  ]
-                                ]
-                              ]
-                              [[group(expand: true, [<ref interned *2>])]]
-                              [
-                                [
-                                  "(",
-                                  indent([hard_line_break, <ref interned *2>]),
-                                  hard_line_break,
-                                  ")"
-                                ]
-                              ]
-                            ]),
+                            <interned 2> [
+                              "str(",
+                              group([indent([group(["arg"])])]),
+                              ")"
+                            ],
                             ", self.args"
                           ])
                         ])
@@ -155,7 +119,6 @@

     def __str__(self) -> str:
         return (
-            f"first stringlist of args long={list(map(lambda arg: str(
-                        arg
-                    ), self.args))})"
+            "first string"
+            f"list of args long={list(map(lambda arg: str(arg), self.args))})"
         )
```

</details>

And you can also see at the bottom here that the actual formatting that
#25144 sought to resolve
is still fixed.

Test Plan
--

New regression test based on the code in the issue
anishgirianish pushed a commit to anishgirianish/ruff that referenced this pull request May 28, 2026
…5144)

Summary
--

This PR fixes astral-sh#24807 by making `RemoveSoftLinesBuffer` aware of
`BestFitting` and always selecting the most-flat variant, as suggested
in [Micha's comment], assuming I interpreted it correctly.

As I noted on the issue, I was hoping I could get away with making a
local change to the lambda formatting, but this turned out to affect
the two other uses of `RemoveSoftLinesBuffer` too.

The formatting of the new regression tests:

```py
def foo():
    subprocess.check_call(f"rm -rf {' '.join(map(lambda object_name: os.path.join(dest_path, object_name), objects_to_remove))}", shell=True)

def bar():
    lambda x=" ".join(
        map(lambda object_name: os.path.join(dest_path, object_name), objects_to_remove)
    ): ...

value = f"prefix {
    ' '.join(map(lambda object_name: os.path.join(dest_path, object_name), objects_to_remove))
} suffix"
```

now (nearly) matches the output before astral-sh#21385:

```console
❯ git diff <(uvx [email protected] format - < fmt.py) <(./target/debug/ruff format - < fmt.py)
 def bar():
-    lambda x=" ".join(
-        map(lambda object_name: os.path.join(dest_path, object_name), objects_to_remove)
-    ): ...
+    lambda x=" ".join(map(lambda object_name: os.path.join(dest_path, object_name), objects_to_remove)): (
+        ...
+    )

 value = f"prefix {
```

with the only difference being that the body of this second lambda
still wraps in the new formatting.

Test Plan
--

New tests based on the reported issue and possibly the ecosystem check
on this PR

[Micha's comment]:
astral-sh#24807 (comment)
anishgirianish pushed a commit to anishgirianish/ruff that referenced this pull request May 28, 2026
…sh#25398)

Summary
--

This PR fixes astral-sh#25397 by dropping `StartBestFittingEntry` and
`EndBestFittingEntry` tags in
`RemoveSoftLinesBuffer`. astral-sh#25144 changed `RemoveSoftLinesBuffer` to
inline the most-flat variant of
`BestFitting` elements, but it inlined them with their start and end
tags, which caused the problem
observed in astral-sh#25397 when these tags became nested in another
`BestFitting` element, which interpreted
the first `End` tag as ending the outer `BestFitting` rather than the
nested one. You can see this
clearly in the IR. First, the diff between the 0.15.13 and 0.15.14 IR,
which reveals the unmatched
start tags:

<details>
<summary>0.15.13 vs main</summary>

```diff
@@ -38,29 +38,13 @@
                           "lambda ",
                           group(["arg"]),
                           ": ",
-                          best_fitting([
-                            [
-                              [
-                                <interned 1> [
-                                  "str(",
-                                  group([
-                                    indent([soft_line_break, group(["arg"])]),
-                                    soft_line_break
-                                  ]),
-                                  ")"
-                                ]
-                              ]
-                            ]
-                            [[group(expand: true, [<ref interned *1>])]]
-                            [
-                              [
-                                "(",
-                                indent([hard_line_break, <ref interned *1>]),
-                                hard_line_break,
-                                ")"
-                              ]
+                          [
+                            <interned 1> [
+                              "str(",
+                              group([indent([group(["arg"])])]),
+                              ")"
                             ]
-                          ]),
+                          ],
                           ", self.args"
                         ])
                       ])
@@ -105,27 +89,9 @@
                             "lambda ",
                             group(["arg"]),
                             ": ",
-                            best_fitting([
-                              [
-                                [
-                                  <interned 2> [
-                                    "str(",
-                                    group([
-                                      indent([soft_line_break, group(["arg"])]),
-                                      soft_line_break
-                                    ]),
-                                    ")"
-                                  ]
-                                ]
-                              ]
-                              [[group(expand: true, [<ref interned *2>])]]
-                              [
-                                [
-                                  "(",
-                                  indent([hard_line_break, <ref interned *2>]),
-                                  hard_line_break,
-                                  ")"
-                                ]
+                            [
+                              <interned 2> [
+                                "str(",
+                                group([indent([group(["arg"])])]),
+                                ")"
                               ]
-                            ]),
-                            ", self.args"
+                            ]
                           ])
+                          <START_WITHOUT_END<Group>>
                         ])
-                      ]),
-                      ")"
+                        <START_WITHOUT_END<Indent>>
+                      ])
+                      <START_WITHOUT_END<Group>>
                     ])
+                    <START_WITHOUT_END<Group>>
                   ])
-                ]),
-                ")})\""
-              ]),
-              hard_line_break,
-              ")"
+                  <START_WITHOUT_END<Indent>>
+                ])
+                <START_WITHOUT_END<Group>>
+              ])
+              <START_WITHOUT_END<Indent>>
             ])
-          ]
+            <START_WITHOUT_END<Group>>
+          ])
+          <START_WITHOUT_END<BestFittingEntry>>
         ]
       ])
     ]),
```

</details>

and then the diff between main and this PR, which shows the removed
brackets that represent the
nested tags:

<details>
<summary>main vs PR</summary>

```diff
@@ -38,12 +38,10 @@
                           "lambda ",
                           group(["arg"]),
                           ": ",
-                          [
-                            <interned 1> [
-                              "str(",
-                              group([indent([group(["arg"])])]),
-                              ")"
-                            ]
+                          <interned 1> [
+                            "str(",
+                            group([indent([group(["arg"])])]),
+                            ")"
                           ],
                           ", self.args"
                         ])
@@ -89,24 +87,15 @@
                             "lambda ",
                             group(["arg"]),
                             ": ",
-                            [
-                              <interned 2> [
-                                "str(",
-                                group([indent([group(["arg"])])]),
-                                ")"
-                              ]
-                            ]
+                            <interned 2> [
+                              "str(",
+                              group([indent([group(["arg"])])]),
+                              ")"
+                            ],
+                            ", self.args"
                           ])
-                          <START_WITHOUT_END<Group>>
                         ])
-                        <START_WITHOUT_END<Indent>>
-                      ])
-                      <START_WITHOUT_END<Group>>
+                      ]),
+                      ")"
                     ])
-                    <START_WITHOUT_END<Group>>
                   ])
-                  <START_WITHOUT_END<Indent>>
-                ])
-                <START_WITHOUT_END<Group>>
-              ])
-              <START_WITHOUT_END<Indent>>
+                ]),
+                ")})\""
+              ]),
+              hard_line_break,
+              ")"
             ])
-            <START_WITHOUT_END<Group>>
-          ])
-          <START_WITHOUT_END<BestFittingEntry>>
+          ]
         ]
       ])
     ]),
```

</details>

I think the diff between 0.15.13 and this PR is also nice to see because
it makes it a bit more
clear that the `BestFitting` element has been inlined:

<details>
<summary>0.15.13 vs this PR</summary>

```diff
@@ -38,29 +38,11 @@
                           "lambda ",
                           group(["arg"]),
                           ": ",
-                          best_fitting([
-                            [
-                              [
-                                <interned 1> [
-                                  "str(",
-                                  group([
-                                    indent([soft_line_break, group(["arg"])]),
-                                    soft_line_break
-                                  ]),
-                                  ")"
-                                ]
-                              ]
-                            ]
-                            [[group(expand: true, [<ref interned *1>])]]
-                            [
-                              [
-                                "(",
-                                indent([hard_line_break, <ref interned *1>]),
-                                hard_line_break,
-                                ")"
-                              ]
-                            ]
-                          ]),
+                          <interned 1> [
+                            "str(",
+                            group([indent([group(["arg"])])]),
+                            ")"
+                          ],
                           ", self.args"
                         ])
                       ])
@@ -105,29 +87,11 @@
                             "lambda ",
                             group(["arg"]),
                             ": ",
-                            best_fitting([
-                              [
-                                [
-                                  <interned 2> [
-                                    "str(",
-                                    group([
-                                      indent([soft_line_break, group(["arg"])]),
-                                      soft_line_break
-                                    ]),
-                                    ")"
-                                  ]
-                                ]
-                              ]
-                              [[group(expand: true, [<ref interned *2>])]]
-                              [
-                                [
-                                  "(",
-                                  indent([hard_line_break, <ref interned *2>]),
-                                  hard_line_break,
-                                  ")"
-                                ]
-                              ]
-                            ]),
+                            <interned 2> [
+                              "str(",
+                              group([indent([group(["arg"])])]),
+                              ")"
+                            ],
                             ", self.args"
                           ])
                         ])
@@ -155,7 +119,6 @@

     def __str__(self) -> str:
         return (
-            f"first stringlist of args long={list(map(lambda arg: str(
-                        arg
-                    ), self.args))})"
+            "first string"
+            f"list of args long={list(map(lambda arg: str(arg), self.args))})"
         )
```

</details>

And you can also see at the bottom here that the actual formatting that
astral-sh#25144 sought to resolve
is still fixed.

Test Plan
--

New regression test based on the code in the issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working formatter Related to the formatter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Formatting a long f-string requires two passes to reach a stable result

2 participants