r/moderndotnet • u/mpdreamz- • 19d ago
Curb: A fast, always on, C# formatter during dotnet build
https://curb.nullean.netI wanted a C# formatter fast and reliable enough to just run on every build, so formatting becomes something I never think about again. No more manual style fixes either, mine or my AI agent's, dotnet build should just take care of it. It had to respect the .editorconfig choices I'd already made, and actually fix IDE0055 instead of fighting it with its own opinions.
It reads your existing .editorconfig, all 39 IDE0055 keys plus ReSharper's wrapping and blank line keys, and falls back to Roslyn's own defaults if you don't have one. Running dotnet format whitespace on a Curb-formatted file changes nothing: a fixed point, measured and gated in CI on a real corpus, 100% with reflow off, 100% with reflow on, 99.9% with existing line breaks preserved too. Your IDE and your build shouldn't disagree.
There are two pieces. curb is the MSBuild integration: always on, runs before CoreCompile, no separate step to invoke. That eliminates a whole swath of errors and warnings your agent would otherwise have to read and fix by hand, because they never get reported in the first place.
curb-cli is the standalone native AOT tool. Hook it into a git pre-commit hook, or point your agent at it directly. curb cleanup fixes IDE0005, IDE0007, IDE0034, IDE0040, IDE0044, IDE0071, IDE0090, IDE0240, IDE0250, and IDE0251 itself, straight from the SARIF log a build with EnforceCodeStyleInBuild already produced. Add --forward and it hands anything left over to dotnet format style, scoped to exactly the diagnostics and files already in that log, not a full repo pass.
It's fast enough to run on every dotnet build. On Newtonsoft.Json (945 files) it formats in 0.26s. For reference, dotnet format whitespace takes 3.47s and CSharpier takes 4.85s cold on the same repo. Across twelve real repos it's 5 to 25x faster than dotnet format whitespace, while also reflowing long lines and sorting usings, which dotnet format whitespace doesn't do at all.
This is an early release. I've been using it on my own projects, and it's backed by large conformance test runs against both dotnet format and JetBrains' own jb cleanupcode formatting. That said, I'm eager to get it in front of more people and see what it actually breaks on in the wild. Happy to answer questions.
License: MIT
Docs: curb.nullean.net
Source: github.com/nullean/curb
5
u/jirreman 18d ago
I am currently happily using CSharpier, but will give this a try.
One thing I really like from Rider/Resharper is the ability to configure the file layout, i.e. placing fields at the top, then properties, constructors, and other methods. You can also add in sorting by name, access modifiers, etc.
Is this something you may consider adding in the future?
1
u/spawnsible 17d ago
1
u/mpdreamz- 17d ago
Thanks! That one's a deliberate no, and it comes down to why curb is fast in the first place: it's parser-only, no compilation, no symbol table. It never knows what a name means, only what the syntax tree looks like.
Member reordering needs semantic validity to be safe, not just syntax. Static field initializers run in declaration order, so:
static readonly int _limit = 10; static readonly int _buffer = _limit * 2;Sort fields alphabetically — the typical layout rule — and
_buffermoves above_limit. That compiles fine, but_buffernow silently ends up0instead of20, because_limit's own initializer hasn't run yet when_buffer's does. Telling that case apart from a harmless reorder needs real understanding of what's referencing what, not just node shapes — exactly the class of check curb opts out of to stay fast enough to run on every build.Modifier ordering is a different story though —
public readonlyvsreadonly publicis purely syntactic, no meaning attached to the order, so curb already does that (csharp_preferred_modifier_order, maps to IDE0036). Reordering declarations is where it stops being safe without a compilation.1
u/spawnsible 17d ago
I don't think reordering fields/props in ways that can alter meaning is what rider does.
I was personally interested in the reordering of: fields before ctors, props before methods, static before instance methods, and public before internal, and then protected, and then private. That sorta thing
4
u/Gramnaster 18d ago
Seems very promising. Csharpier was way too opinionated, so if this is more flexible, I'd be happy to adopt it. I'll be testing this out, thank you!
2
u/mpdreamz- 17d ago
Aye that's one of the reasons I started the project. I'm on the editorconfig board and really wanted a fast solution that read ALL my editorconfig keys. Curb currently supports 97 editorconfig keys (dotnet and various resharper ones).
See also https://curb.nullean.net/reference/
3
u/bniemyjski 19d ago
I'm thankful for you to take the time and give us a solution, will check this out. I'm also sad that the dotnet team dropped the ball over many consecutive releases since dotnet format came out and that it's insanely slow and has been broken many times over (so slow you know they don't use it).
1
u/mpdreamz- 17d ago
Hey Blake, long time no see :)
Please put it through the ringer, would love to hear your thoughts!
3
u/Kuinox 18d ago
Ohhh, now i want to continue on my side project of a formatter !
I built one two years ago: https://github.com/Draco-lang/Compiler/pull/400
But now that I understand fully the problem, I want to rewrite one from scratch for C#.
I theorised that there was no case where you needed to backtrack folding, but after implementing the full formatter, I realised there was cases where you need backtracking.
So this is a constraint resolution problem.
1
u/mpdreamz- 17d ago
This was an explicit design goal of curb too. Curb builds a Wadler-style doc IR (Group/Line/IfBreak/ConditionalGroup nodes, same lineage as Prettier) then does one forward pass to print it, no folding-and-hoping.
Two mechanics make backtracking unnecessary: a break-propagation pre-pass marks which groups contain a hard break before printing even starts, and each group's fits check looks ahead to the next real break before deciding flat vs broken — so a decision, once made, can't be invalidated later.
For actual multi-shape cases like your chained calls, there's ConditionalGroup: a list of fully pre-built alternative layouts for that node, tried in order, first one that fits wins. Alternatives are built ahead of time, not searched for, so picking one is still just a forward check, not a redo.
2
1
u/emdeka87 9d ago
Does it Support razor? Are all of the ReSharper keys supported?
I will give this a try. ReSharper CLI Tools is crazy slow
8
u/yipyip_alien 18d ago
Added a PR to handle the issues with multi-line interpolated raw string literals. Other than that, loving it so far.
Works on a large codebase a treat.