Loading

Cleanup

Curb formats from syntax alone — no workspace, no compilation. That is what lets it run inside dotnet build before the compiler runs. Semantic code style is out of that scope: whether a using directive is unused, or a field is only ever assigned in a constructor, is a question only a compilation can answer.

curb cleanup closes part of that gap. The compiler decides whether a rule applies and writes the answer to a SARIF log. curb cleanup reads that log and applies the rewrite. No CSharpCompilation, no SemanticModel, no Workspaces.

dotnet build
curb cleanup
dotnet build
		
  1. fails, or warns, on the rules you escalated
  2. fixes the ones Curb owns
  3. clean

curb sets $(ErrorLog) to $(IntermediateOutputPath)curb.sarif when the project has not set one, so the log exists without anyone asking for it. curb cleanup with no arguments searches the current directory for those logs.

Flag What it does
-s, --sarif-log <path> Name a specific log file, instead of searching for one. Repeatable.
-f, --files <path> Restrict cleanup to these files. Repeatable.
--check Report what would change without writing.
--forward Hand the remainder to dotnet format style after cleanup finishes.
curb rules List every rule and which tool fixes it.

The third build is the verify step you were going to run after any change anyway. A bad fix is a compile error you see immediately.

Mechanism Effect
dotnet_diagnostic.IDEnnnn.severity = none, or not setting it The build never reports it, so cleanup never sees it.
#pragma warning disable IDEnnnn The compiler suppresses it before it reaches the log.
generated_code = true, <auto-generated> Reused from the formatter via FormatOptions.Excluded.
Curb_Diagnostics=false The MSBuild package stops setting $(ErrorLog).
Curb_Bypass=true Everything, formatter included.
A $(ErrorLog) the project already set Curb stands down and never overwrites it.

Three layers keep applying a verdict from a previous build safe:

  1. The node-kind gate. Every fixer declares the syntax it applies to, and the reported position must resolve to it. A diagnostic pointing at a using directive that is no longer there simply does not apply.
  2. The freshness gate. A file whose last-write time is newer than the log is skipped. A span is an offset into the bytes the compiler read; applying it to different bytes is how a tool corrupts source.
  3. The declared-delta verifiers, unchanged from the formatter. A fix says which tokens it removes; ContentVerifier and TokenStreamComparer hold the rest of the file to a strict compare.

Overlapping fixes drop both, never one. Dropping both means a second pass sees the same overlap and drops it again, so the output is a fixed point.

curb rules is the live answer. Ten rules today: IDE0005, IDE0007, IDE0034, IDE0040, IDE0044, IDE0071, IDE0090, IDE0240, IDE0250, IDE0251.

Roslyn emits one IDE0005 per maximal contiguous run of unnecessary directives. The span is a delete instruction. The rule refuses any file containing #if: the compiler decided for one symbol set, and a directive needed only under another would be reported as unnecessary and then lost.

Rule Why
IDE0008 use explicit type The diagnostic does not carry the type name, so the fix is not derivable from the span.
IDE0051 / IDE0052 unused members The fix deletes a declaration. Curb never destroys code.
IDE1006 / IDE0130 naming A rename touches every reference site, can compile while changing which overload binds, and breaks reflection and serialisation strings. Never.
IDE0160 block namespace Curb converts to file-scoped and never back; removing braces can change what a name resolves to.
Rule Fix If the verdict were wrong
IDE0040 accessibility Writes out the accessibility C# already applied Nothing changes; the keyword was already in force.
IDE0044 readonly Inserts readonly into a field's modifier list A compile error — a write through ref or Interlocked the analyser missed.
IDE0090 new() Drops the type name after new A compile error: if the target type were not known, new() is an error.
IDE0007 var Replaces a local's type with var Silent. It compiles and may narrow the declared type. The only rule whose mistake is quiet; built last and leans hardest on the freshness gate.
IDE0250 readonly struct Inserts readonly on a struct Does not compile if some member mutates.
IDE0251 readonly member Inserts readonly on a struct member Does not compile if the member mutates.
IDE0034 simplify default Drops (T) from default(T) A bare default with no inferable target is an error.
IDE0071 simplify interpolation Drops a redundant .ToString() Refused when the call takes arguments, to avoid silently losing the format.
IDE0240 redundant #nullable Removes the directive's line Verified by ContentVerifier rather than TokenStreamComparer, since trivia is not in the token stream.

Curb is not a replacement for dotnet format style. With --forward, it names the remaining diagnostics exactly and hands them to dotnet format:

Cleaned 1 file(s) from 1 log(s) in 105ms — 2 fix(es) in 1 file(s), 0 refused, 0 stale, 0 skipped, 0 failed
  forwarding 1 rule(s) in 1 file(s) to `dotnet format style`: IDE0071
  curb 105ms · dotnet format 2366ms (22.5x) — the difference is the workspace load, which no amount of scoping avoids
		

Both timings are printed because the difference is the point. Nothing else tells you which half of the wait belongs to which tool.