Skip to content

[Repo Assist] Fix soundness bug: %% (double-percent) infix operator moves to new line, breaking code#3287

Merged
nojaf merged 2 commits intomainfrom
repo-assist/fix-issue-2107-double-percent-infix-2b39fc63804c7a74
Mar 16, 2026
Merged

[Repo Assist] Fix soundness bug: %% (double-percent) infix operator moves to new line, breaking code#3287
nojaf merged 2 commits intomainfrom
repo-assist/fix-issue-2107-double-percent-infix-2b39fc63804c7a74

Conversation

@github-actions
Copy link
Contributor

🤖 This is an automated PR from Repo Assist.

Closes #2107

Problem

When a %% infix expression was too long to fit on one line, Fantomas placed the %% operator at the start of the next line:

let a =
    (if aVeryLongVariableNameThatForceLineBreaking = 0 then
         1
     else
         -1)
    %% 4   // ← invalid: F# parser reads this as prefix splice operator %%4
```

This produced **invalid F#**. The F# compiler interprets `%%` at the start of a line as the quotation splice operator `(~%%)`, expecting a `Quotations.Expr` argumentnot an `int`. The error is:
```
error FS0001: This expression was expected to have type 'Quotations.Expr' but here has type 'int'

Root Cause

CodePrinter.fs maintains a noBreakInfixOps set of operators that must stay on the same line as their LHS (because starting a new line with them changes their parse meaning). The set correctly contained %, =, <, > — but was missing %%. The %% operator therefore fell through to the default genMultilineInfixExpr path, which always puts the operator at the start of the next line.

Fix

Add "%%" to noBreakInfixOps. When the expression is too long, the %% operator stays at the end of the LHS line and the RHS is indented:

let a =
    (if aVeryLongVariableNameThatForceLineBreaking = 0 then
         1
     else
         -1) %% 4   // ← valid: %% is on the same line as its left operand

This is identical to the existing behaviour for %, =, <, and >.

Test Status

  • ✅ Build succeeded (dotnet build src/Fantomas.Core/Fantomas.Core.fsproj)
  • ✅ All 2738 tests pass, 7 skipped (dotnet test src/Fantomas.Core.Tests/)
  • ✅ New regression test added: custom double-percent operator stays on same line as lhs when expression is too long, 2107 in OperatorTests.fs
  • ✅ Fixed output verified as valid F# (compiles and runs correctly)

Generated by Repo Assist ·

To install this agentic workflow, run

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

The custom %% operator (and the built-in F# quotation splice operators %%
and %%) can be interpreted by the F# parser as unary prefix operators when
they appear at the start of a new line. This caused Fantomas to produce
invalid F# code by placing the operator on a new line.

The fix adds "%%" to the noBreakInfixOps set, which keeps the operator on
the same line as its left-hand-side expression (with the right-hand-side
indented below if needed), matching the existing behaviour for %, =, <, >.

Fixes #2107

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@nojaf nojaf marked this pull request as ready for review March 16, 2026 07:23
@nojaf nojaf merged commit da508e5 into main Mar 16, 2026
11 checks passed
@nojaf
Copy link
Contributor

nojaf commented Mar 16, 2026

/repo-assist you did not add a changelog entry here. Please open a new PR for this.
See https://github.com/fsprojects/fantomas/blob/main/.claude/skills/fantomas-issue/SKILL.md#7-update-changelogmd

@github-actions
Copy link
Contributor Author

Pull request created: #3291

Generated by Repo Assist

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.

Incorrect line split when using a custom operator

1 participant