Skip to content

Unify and automate code style, formatting and member ordering across the repository #21

Description

@rent-a-developer

Goal

Code style, formatting and the ordering of types/type members are enforced and fixed automatically, so
that none of the three needs a human decision or a review comment again.

Scope is the whole repository: src/, tests/ and benchmarks/. Previously only src/ had an analyzer
gate, and ordering was not enforced anywhere.

Decisions

  • C# keywords, not BCL type names — string, int, bool, nint, not String, Int32,
    Boolean, IntPtr. Reverses the previous rule; 217 of 283 .cs files.
  • this. prefix stays, now without exception. No _camelCase for fields. A primary constructor
    parameter is assigned to a private readonly backing field
    and read through this.field: a captured
    parameter compiles to a field with no readonly, and C# offers no other way to keep the guarantee.
    Nothing enforces this — it is on the author.
  • Expression-bodied members wherever possible (already the rule, stays).
  • Primary constructors wherever possible. .editorconfig previously asked for them via
    csharp_style_prefer_primary_constructors while disabling IDE0290, the same rule. 6 sites.
  • Member order is StyleCop's (SA1201/1202/1203/1204/1214), plus alphabetical sorting inside each
    group
    .
  • Violations break the build, not only CI — all three concerns.
  • Branch names follow Conventional Branch.

Architecture — three concerns, three tools, no overlap

Concern Tool Config
Formatting (whitespace, line breaks, wrapping) CSharpier 1.3.0 .editorconfig, .csharpierignore
Code style (semantic) Roslyn analyzers, Roslynator .editorconfig
Ordering — fixing ReSharper file layout DbConnectionPlus.slnx.DotSettings
Ordering — checking NewStyleCop.Analyzers 1.2.1 stylecop.json

Each tool owns its concern completely and is switched off inside the others'. No .csharpierrc: the print
width comes from max_line_length in .editorconfig, so the line width has one source of truth.

Why the NewStyleCop.Analyzers fork: upstream's last release is 1.2.0-beta.556 from December 2023
and predates the C# this repo writes. Same diagnostic IDs and same stylecop.json, so switching back is a
one-line change — which is what makes the single-maintainer risk acceptable.

Neither package can fix ordering: in both, ElementOrderCodeFixProvider is [NoCodeFix] and never
registered. StyleCop detects, ReSharper fixes. That split is deliberate, not a workaround.

Where each tool runs

When Runs How
Agent edits a .cs file (PostToolUse hook) format scripts/tidy-cs.ps1, default scope, ~1s
Rider save / Code Cleanup format / reorder CSharpier plugin; ReorderMembers profile
tidy-cs.ps1 → -Scope style → -Scope all format → + style → + reorder CSharpier; dotnet format; jb cleanupcode
preflight.ps1 all three tidy-cs.ps1 -Scope all
Build (all projects) all three analyzers + NewStyleCop; CSharpier.MsBuild in check mode
CI lint job verify all three tidy-cs.ps1 -Scope all -Check

CI does not run three separate checks. Neither cleanupcode nor CSharpier is idempotent alone —
cleanupcode re-indents raw string literals and CSharpier puts them back — so asking either in isolation
always answers "changed". -Check -Scope all tidies the checkout for real and compares a hash of the tree
before against after; the checkout is thrown away at the end of the job.

Work items — all done

1. .editorconfig. Grouped and commented; [*] section plus sections for
[*.{csproj,props,targets,DotSettings}] (tabs), [*.{slnx,config,xml,json,yml,yaml}], [*.md]; BOM
removed; dotnet_style_predefined_type_for_* and dotnet_style_qualification_for_* → true:error;
IDE0290 suppression removed; IDE0055 and RCS1037 → none (CSharpier owns whitespace). No
end_of_line anywhere — .gitattributes owns line endings.

StyleCop is default-deny rather than disabled by ranges: all eight categories off, then the wanted rules
back on. SA1101 is not used — this. comes from the four Roslyn dotnet_style_qualification_* rules.

2. Analyzers for tests and benchmarks. EnforceCodeStyleInBuild and TreatWarningsAsErrors moved to
the root Directory.Build.props. The CA rules stay in src/ only: CA1707 alone fires 2100 times on the
Method_ShouldDoSomething naming the test suite is built around.

3. CSharpier. Tool in .config/dotnet-tools.json, .csharpierignore, and CSharpier.MsBuild in
check mode — an unformatted file fails the build, and the build never rewrites files. The tool and the
MsBuild package versions must stay in step or they disagree.

4. Ordering. DotSettings renamed to match the .slnx; StyleCop file layout with alphabetical
sorting; reorder-only cleanup profile; JetBrains.ReSharper.GlobalTools; NewStyleCop.Analyzers with a
matching stylecop.json.

5. Branch naming. Conventional Branch in CONTRIBUTING.md and AGENTS.md.

6. Documentation and scripts. format-cs.ps1 → tidy-cs.ps1, with -Check and a -Scope parameter
(format / style / all); both hook wrappers; ordering wired into preflight.ps1; AGENTS.md,
CONTRIBUTING.md and .agents/references/code-style.md rewritten.

7. The mechanical commits, kept separate, full unit suite after each:

Commit Tool Files
String → string and friends dotnet format style (IDE0049) 207
Convert to primary constructors dotnet format style (IDE0290) 6
CSharpier reformat csharpier format . 230 of 283
Reorder members jb cleanupcode 127
Satisfy the remaining gates dotnet format, jb cleanupcode 41

.git-blame-ignore-revs lists all five. PublicAPI.*.txt: checked, no churn.

8. Gaps nothing was watching. Found while building the above, all now build errors:

  • Unused using directives. 20 of them. IDE0005 is never reported at build time unless the
    compilation produces an XML documentation file (roslyn#41640),
    so GenerateDocumentationFile is on for every project. CS1591/CS1574 are switched off for tests and
    benchmarks, which do not document their members.
  • Target-typed new. 14 sites. IDE0090 only fires where the type is written next to the new,
    which the var rule makes impossible here; RCS1250 reaches return, argument and assignment
    positions.
  • Using order. SA1208/1209/1210/1211/1217. These check rather than fix — CSharpier already
    sorts usings — and report zero violations, which is what says the two tools agree.

What emerged during the work

  • Explicit interface implementations vs SA1202. The layout needs one entry per kind (property,
    indexer, method) matching ImplementsInterface and Access Is="Private". Without the access test it
    also catches implicit implementations and drags Equals(T) away from Equals(object). Events must
    not get an entry: StyleCop counts an explicit event as private, and giving it one breaks the build.
  • CSharpier contradicts SA1216. It sorts using static by namespace, interleaved; SA1216 wants them
    grouped at the end. CSharpier runs last and always has the final say, so SA1216 is unsatisfiable here and
    is deliberately not enabled.
  • Overload order is not defined. Same-named overloads tie on every sort key and ReSharper's sort is
    stable, so it leaves them where it found them. Both orders are correct; a stable sort leaves the
    committed order alone, so CI does not flap.
  • IDE0049 has three blind spots, none of which produce a diagnostic: nint/nuint are not on its
    list, it never looks inside nameof(...), and tests/package-consumption/ is not in the solution.
  • Primary constructor parameters lose readonly — the decision above. Found by comparing compiled
    output, not by any test.
  • .git-blame-ignore-revs is fragile. git never warns about a SHA it cannot resolve; it skips the
    entry and blame silently goes back to pointing at the tool. A rebase-merge or squash-merge rewrites every
    SHA. The CI lint job now fails if any entry stops resolving, and this branch must be merged with a
    merge commit.

Risks

  • Field initializer order. Checked: no behaviour change, by comparing a token multiset of every
    .cs file before and after and by inspecting the emitted fields.
  • jb cleanupcode and .slnx profile lookup (RSRP-502346). Verified working on 2026.2.1.
  • Alphabetical order inside a group is not checked by StyleCop — its elementOrder knows only kind,
    accessibility, const, static and readonly. A member in the wrong alphabetical position is silently fixed
    by the tidy script, by Rider and by the CI lint job, but does not break the build. Accepted.
  • NewStyleCop.Analyzers has one maintainer. Mitigated by identical rule IDs and config.
  • Two tools on save in Rider can fight. On save = formatting only; reordering is deliberate.
  • cleanupcode is slow — it loads the whole solution. It belongs in the full tidy run, in preflight
    and in CI, never in a per-edit hook.
  • CSharpier.MsBuild has no MSBuild inputs/outputs, so it runs on every build. About four seconds on
    a full solution build. -p:CSharpier_Bypass=true skips it.

Verification

The mechanical commits come in prepare/apply pairs, so each can be re-run from its parent and compared.
All five were, and these are the measured results:

Apply commit Tool Difference
C# keywords dotnet format style --diagnostics IDE0049 none
primary constructors dotnet format style --diagnostics IDE0290 + 23 stale doc lines
CSharpier reformat csharpier format . none
reorder members jb cleanupcode + csharpier 4 files, 10 lines
satisfy the gates dotnet format + cleanupcode + csharpier 32 files

The reorder's 10 lines are Equals/CompareTo overload pairs, for the reason above; its check is instead
that the branch tip is a fixed point, tidy-cs.ps1 -Scope all -Check exiting 0. The last commit is mixed:
its 32 files are the hand edits no tool reaches — the keyword conversions under
tests/package-consumption/ and inside nameof(...), the MySqlEntityManipulator conversion, two
global using lines the SDK already generates, and a set of comments and doc-wording fixes.

Release build: 0 warnings, 0 errors. 3475 unit tests green. No public API change.

Activity

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

Metadata

Metadata

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions