Skip to content

[Repo Assist] Stay multiline when list element is a tuple with lambda as last field (#3278)#3280

Closed
github-actions[bot] wants to merge 1 commit intomainfrom
repo-assist/fix-issue-3278-lambda-tuple-list-single-line-d09b59d5b80a41a1
Closed

[Repo Assist] Stay multiline when list element is a tuple with lambda as last field (#3278)#3280
github-actions[bot] wants to merge 1 commit intomainfrom
repo-assist/fix-issue-3278-lambda-tuple-list-single-line-d09b59d5b80a41a1

Conversation

@github-actions
Copy link
Contributor

Closes #3278

🤖 This PR was created by Repo Assist, an automated AI assistant. Please review carefully.

Problem

When a list is collapsed to a single-line form (elements separated by ;), if any list element is a tuple whose last element is a lambda or if/then/else, the lambda body captures the ; separator and subsequent list elements as a sequence expression — silently changing the semantics of the code.

Example:

// Input (2-element list):
let x =
    [
        1, fun () -> 1
        1, fun () -> 1
    ]

// Before (WRONG — 1-element list!):
let x = [ 1, fun () -> 1; 1, fun () -> 1 ]
// F# parses as: [ (1, fun () -> (1; 1, fun () -> 1)) ] — ONE element

Verified:

let x_fmt = [ 1, fun () -> 1; 1, fun () -> 1 ]
List.length x_fmt  // = 1, not 2 — semantics changed!

Root Cause

In genArrayOrList, the alwaysMultiline guard forces multiline layout when any element is if/then/else, or all elements are lambdas/ite. But 1, fun () -> 1 is an Expr.Tuple, not a bare lambda, so neither guard fires. The list passes the "fits on one line?" check and collapses — producing semantically incorrect code.

Fix

Extend the alwaysMultiline check with a wouldSwallowNextListItem helper: if any non-last element is a tuple whose last expression is a lambda or if/then/else, force multiline. In the multiline form, elements are on separate lines where whitespace-sensitivity scopes the lambda body correctly.

This follows the "stay multiline" design principle (Option B in RFC #3279), which is the approach the community overwhelmingly voted for (16:1 as of the time of this PR).

The fix is minimal and surgical — it only affects lists where a non-last element would cause a semantic change when collapsed.

Test Status

  • ✅ Build: dotnet build fantomas.sln — succeeded
  • ✅ Tests: dotnet test src/Fantomas.Core.Tests/2738 passed, 0 failed, 7 skipped
  • ✅ New regression test added: lambda in non-last tuple position in list stays multiline to preserve semantics, 3278

Generated by Repo Assist ·

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/repo-assist.md@346204513ecfa08b81566450d7d599556807389f

…3278)

When a list is collapsed to a single-line form (e.g. [a; b; c]), the
items are separated by ";" which in this context means list separator.
However, if a list element is a tuple whose last expression is a lambda
or if/then/else, the lambda body will capture the ";" and subsequent
list items as a sequence expression, changing the semantics of the code.

Example:
  // Input (2-element list):
  let x = [ 1, fun () -> 1; 1, fun () -> 1 ]
  // The lambda body captures "; 1, fun () -> 1" -> list has 1 element!

Fix: extend the alwaysMultiline check in genArrayOrList to detect when
any non-last element is a tuple whose last sub-expression is a lambda or
if/then/else. In that case, keep the list multiline so each element is
on its own line (where whitespace-sensitivity prevents the capture).

This follows the "stay multiline" design principle (Option B in #3279).

Fixes #3278

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@nojaf
Copy link
Contributor

nojaf commented Mar 12, 2026

Gonna solve this in a larger refactor to have a more overall consistent story.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Lambda in tuple in list on single line changes meaning of code

1 participant