Skip to content

Migrate formatting toolchain from dotnet format to curb - #3904

Open
Mpdreamz wants to merge 6 commits into
mainfrom
feature/move-to-curb
Open

Migrate formatting toolchain from dotnet format to curb#3904
Mpdreamz wants to merge 6 commits into
mainfrom
feature/move-to-curb

Conversation

@Mpdreamz

@Mpdreamz Mpdreamz commented Aug 20, 2026

Copy link
Copy Markdown
Member

Why

We've been using dotnet format for C# formatting. It works, but it's slow enough that we had to run it on pre-push instead of pre-commit — nobody wants to wait 46 seconds every time they save a commit. That means formatting feedback came late, and the friction was real.

curb fixes this. It's a new C# formatting tool that reads your existing .editorconfig — both standard .NET keys (dotnet_*, IDE0055) and ReSharper keys (resharper_*) — and formats without loading the MSBuild workspace. No project restore, no Roslyn workspace hydration. Here's what it clocked on our repo (1 201 files):

Cold (no cache) Warm (cache hit)
dotnet curb format . 414ms ~200ms
dotnet curb check . 429ms 306ms
dotnet format whitespace ~3.5s ~3.5s (no cache)
dotnet format (full) ~46s ~46s (no cache)

Fast enough to run on every commit. And in practice the pre-commit warmup cost is near zero — by the time you commit, dotnet build has already run curb in Debug mode and written a cache. The check pass on commit burns through that cache rather than rescanning everything.

What changed

The pre-commit hook now lints on every commit, not just on push. curb's incremental cache (.artifacts/curb.cache) makes the check fast enough that there's no reason to defer it.

CI calls dotnet curb check . directly — no F# build wrapper overhead.

The MSBuild integration means dotnet build in Debug mode auto-reformats any file that's out of style before the compiler sees it. In CI (Release mode) it errors instead of rewrites, which is what you want. You don't have to think about formatting — by the time compilation runs, style is already applied.

.editorconfig tightened up now that running the formatter is cheap:

  • Line length 160 → 140
  • Object/collection initializers chop_if_long
  • Fluent method chains chop_if_long
  • Binary expressions chop_if_long
  • Binary operators lead lines — && x not x &&

curb cleanup — if a build fails on diagnostics curb handles natively (unused usings, redundant modifiers, etc.), run curb cleanup to apply those fixes in ~15ms from the SARIF the last build already wrote, no workspace load. Add --forward to pass anything it can't handle to dotnet format style.

This PR is the toolchain only

The codebase has not been reformatted yet — that's #3905, stacked on this one. CI lint will be red here; it goes green once the formatting sweep lands.

Test plan

  • dotnet build auto-reformats a deliberately mis-formatted file in Debug mode
  • ./build.sh lint catches violations and exits non-zero
  • dotnet curb format . clears all violations
  • Pre-commit hook fires on .cs/.fs changes (moved from pre-push)

Mpdreamz and others added 6 commits August 20, 2026 11:43
Adds curb 0.2.0 as a GlobalPackageReference (MSBuild integration that
auto-rewrites on Debug builds and fails on Release/CI) and curb-cli 0.2.0
as a local dotnet tool. The pre-commit hook now runs ./build.sh lint (moved
from pre-push), which calls curb check with a .artifacts/curb.cache for
incremental pre-commit runs. CI lint job calls curb check directly.
Formatting changes land in a follow-up stacked PR.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- max_line_length 160 -> 140
- csharp_wrap_object_and_collection_initializer_style: wrap_if_long -> chop_if_long
- add csharp_wrap_chained_method_calls = chop_if_long
- add resharper_csharp_empty_block_style = together_same_line (Rider only)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
0.3.0 ships fixes for all three issues we filed:
- #8  resharper_csharp_empty_block_style (now the default)
- #10 csharp_wrap_chained_method_calls + max_chained_method_calls_on_line
- #11 binary operator double-indent inside if () blocks

New editorconfig keys:
- csharp_wrap_chained_binary_expressions = chop_if_long
- csharp_wrap_before_binary_opsign = true (explicit; was already default)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Comment thread build/Targets.fs
else ["--include"] @ includeFiles
exec { run "dotnet" (["format"; "--verbosity"; "quiet"] @ includeArgs) }
let private format (_formatArgs: ParseResults<FormatArgs>) =
exec { run "dotnet" ["curb"; "format"; "."] }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FormatArgs.Include and LintArgs.Include are still parsed by the CLI, but this change now ignores both and always runs against ..

That creates a silent behavior change: ./build.sh format <files> / ./build.sh lint <files> now process the whole repo instead of the requested subset.

Could we either:

  1. map include args to the equivalent curb scoping options, or
  2. remove/deprecate the include args and update build/CommandLine.fs usage text in the same PR so callers don’t get a no-op argument?

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants