Getting started¶
From install to the first dotnet format run. Pick the path that fits:
- Preview first: what either path would change, without changing anything
- A. A new project, or no StyleCop
- B. Coming from StyleCop
Both use two packages: StyleBro.Analyzers (the rules, referenced by your projects) and StyleBro.Migrate (the
stylebro-migrate command line tool, used once to write settings):
Which version¶
The latest release on nuget.org is 0.7.0-alpha.1, a prerelease: install the tool with --prerelease and reference
that exact version. This page describes main, which matched that release when it shipped; what changed since then
ships in the next one and is listed in changes/.
To try the code on main before the next release, build both
packages from a clone into a local folder (any version string works; 0.8.0-dev.1 here):
git clone https://github.com/bisforboman/stylebro
dotnet pack stylebro/src/StyleBro.Package -o stylebro-feed -p:Version=0.8.0-dev.1
dotnet pack stylebro/src/StyleBro.Migrate -o stylebro-feed -p:Version=0.8.0-dev.1
dotnet tool install --global StyleBro.Migrate --add-source stylebro-feed --version 0.8.0-dev.1
Then add the folder as a package source in your repository's nuget.config and use 0.8.0-dev.1 wherever this page
says 0.7.0-alpha.1. When the nuget.config has a <packageSourceMapping>, the new source needs a mapping too: NuGet
only looks for a package in the sources mapped to it, so the source alone gives NU1102 (package not found):
<configuration>
<packageSources>
<add key="stylebro" value="path/to/stylebro-feed" />
</packageSources>
<packageSourceMapping>
<!-- the mappings already there stay -->
<packageSource key="stylebro">
<package pattern="StyleBro.*" />
</packageSource>
</packageSourceMapping>
</configuration>
Preview first¶
Before changing anything, --diff shows what a path would do to your code. It works on all three commands:
stylebro-migrate init --diff # path A: init's settings, then format
stylebro-migrate path/to/repo --diff # path B: the migration's settings (--write), then format
stylebro-migrate format --diff # only the format run
The preview copies the repository to a temporary folder (git's tracked and untracked files, not the ignored ones; the
working trees of submodules too), runs the command there, then stylebro-migrate format until a run changes nothing,
and deletes the copy. Your repository isn't touched. If it doesn't reference StyleBro.Analyzers yet, the copy gets the
reference from step 1 (the tool's own version) and the output says so. It prints a summary and
writes the full diff to .git/stylebro-preview.patch in the repository's git folder, where it isn't an untracked file
(outside a git repository: the temp folder; --diff=other.patch names another file).
--keep keeps the copy and prints where it is; --all passes on to format; with several solutions or projects in the
folder, --project MySolution.sln names the one to format. --no-restore skips the restore in the copy: it takes the
repository's own restore output, so run dotnet restore first (it's ignored when the preview adds the package, which
needs a restore). Where the preview adds the reference under central package management: next to StyleCop's (a
GlobalPackageReference gets a GlobalPackageReference, in the same Directory.Packages.props), else a version-less
PackageReference with the version in the Directory.Packages.props that applies (also one that a folder's
Directory.Packages.props imports). FFMpegCore, stylebro-migrate init --diff:
Previewing 'stylebro-migrate init --write' and 'stylebro-migrate format' on a copy; nothing in C:\src\FFMpegCore changes.
Copying the repository to C:\Users\me\AppData\Local\Temp\stylebro-preview-fmsw0dps.3qa\FFMpegCore... 0s
Settings: 31 lines in .editorconfig; 3 lines in Directory.Build.props (StyleBro.Analyzers)
StyleBro.Analyzers wasn't referenced: added StyleBro.Analyzers 0.7.0-alpha.1 to Directory.Build.props for the preview, as docs/getting-started.md says (the patch includes it).
Finding what format fixes ('dotnet format --verify-no-changes')... 16s
Format run 1... 29s
Format run 2... 14s
Format: 124 files, 579 reported changes, clean after 1 run
BRO1603 Documentation text should end with a ... 65 files
BRO1001 Members should be ordered 33 files
BRO1401 Use a trailing comma in multi-line in... 33 files
...
... and 24 more rules
Sample (BRO1603, FFMpegCore.Extensions.Downloader/Exceptions/FFMpegDownloaderException.cs):
-/// Custom exception for FFMpegDownloader
+/// Custom exception for FFMpegDownloader.
Sample (BRO1401, FFMpegCore.Examples/Program.cs):
- FrameRate = 30 //set source frame rate
+ FrameRate = 30, // set source frame rate
Full diff: .git\stylebro-preview.patch (5,966 lines)
Done in 1m 2s.
Files are counted under every rule dotnet format --verify-no-changes reported in them before the fixes; a file with
only whitespace changes counts as IDE0055, one with nothing reported (another fix's side effect) as other. The patch
is what the real commands write (byte for byte on FFMpegCore); git apply .git/stylebro-preview.patch applies it.
A. A new project, or no StyleCop¶
1. Add the package¶
Add it to every project, for example in Directory.Build.props at the repository root:
<Project>
<ItemGroup>
<PackageReference Include="StyleBro.Analyzers" Version="0.7.0-alpha.1" PrivateAssets="all" />
</ItemGroup>
</Project>
With central package management, the version goes in Directory.Packages.props and the reference has none:
<!-- Directory.Packages.props -->
<ItemGroup>
<PackageVersion Include="StyleBro.Analyzers" Version="0.7.0-alpha.1" />
</ItemGroup>
<!-- Directory.Build.props -->
<ItemGroup>
<PackageReference Include="StyleBro.Analyzers" PrivateAssets="all" />
</ItemGroup>
A project that turns central package management off itself (<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>,
often an analyzer or source generator project) fails its restore with the version-less reference (NU1015). Give such
projects a reference with a version, next to the version-less one:
<!-- Directory.Build.props -->
<ItemGroup>
<PackageReference Include="StyleBro.Analyzers" PrivateAssets="all" Condition="'$(ManagePackageVersionsCentrally)' == 'true'" />
<PackageReference Include="StyleBro.Analyzers" Version="0.7.0-alpha.1" PrivateAssets="all" Condition="'$(ManagePackageVersionsCentrally)' != 'true'" />
</ItemGroup>
MSBuild imports only the nearest Directory.Build.props above a project: if a folder (say src/) has its own and
it doesn't import the root one, put the reference there (or in each such file). If it does import it (GetPathOfFileAbove,
or a property like Orleans' $(_ParentDirectoryBuildPropsPath)), the root's reference is enough: a second one fails the
restore (NU1504, duplicate reference). A reference inside a conditional
ItemGroup (Condition="'$(TargetFramework)' == 'net8.0'") reaches only that target framework, and
<RunAnalyzers>false</RunAnalyzers> turns all analyzers off where it applies. --diff adds the reference to every
Directory.Build.props the projects use and names the projects whose restore output has no StyleBro.Analyzers (a
code base that already follows every rule reports nothing, and that's fine).
With lock files (<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>), the next restore adds StyleBro.Analyzers
to every packages.lock.json: commit them with the reference. Where <RestoreLockedMode>true</RestoreLockedMode> is on
(always, or in CI), that restore fails instead (NU1004, "the package references have changed"): update the lock files
once with locked mode off, dotnet restore -p:RestoreLockedMode=false, and commit them. --diff does this in its copy
by itself, and its patch shows the updated lock files.
Two things that get in the way of a package from a local feed (trying a build before it's on nuget.org):
- A
<RestoreSources>property (often inDirectory.Build.props) replaces the sources of everynuget.config: a feed added there is never asked, and the restore fails with NU1101/NU1102. Add the feed toRestoreSourcestoo (<RestoreSources>$(RestoreSources);C:\feed</RestoreSources>), or useRestoreAdditionalProjectSources. - Projects a test run generates inside the repository (say under
artifacts/or a test's temp folder) inherit the rootDirectory.Build.props, and with it the StyleBro reference, which their own restore may not find. Leave them out with a condition on the reference, e.g.Condition="!$(MSBuildProjectDirectory.Contains('artifacts'))", or give that folder aDirectory.Build.propsof its own that doesn't import the root one.
The package brings StyleBro's rules and its preset: rule severities and formatting options, close to StyleCop's
defaults. Your own .editorconfig wins over the preset. The preset has a version, and stylebro-migrate init --write
(step 2) pins it in Directory.Build.props (<StyleBroPreset>1.0</StyleBroPreset>), so a later package upgrade adds no
new rules until you raise it (versioning). To configure everything yourself instead:
<StyleBroPreset>none</StyleBroPreset>.
2. Turn on the built-in rules¶
StyleBro relies on some built-in .NET rules (IDE0055 formatting, IDE0036 modifier order, IDE0049 type aliases, ...).
Their severities have to be in .editorconfig, because dotnet format ignores rule severities in a package's preset.
In the repository root:
stylebro-migrate init # shows the block
stylebro-migrate init --write # adds it to .editorconfig (created if missing)
stylebro-migrate init --diff shows what the block and the format run in step 3 would change, without writing
anything (Preview first).
The block sits between # BEGIN stylebro-migrate and # END stylebro-migrate; running the command again replaces it.
Put your own settings outside it. To get these rules reported by the build too, add
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> to Directory.Build.props (init says so when no project
file sets it yet).
An .editorconfig that silences analyzers in bulk (dotnet_analyzer_diagnostic.severity = silent, or a category's
severity below warning) would hide StyleBro's rules from dotnet format, while the build still reports them at the
preset's severities. init then writes each rule the preset turns on by its id, so both see the same rules; a rule
your .editorconfig sets by id stays as you set it. stylebro-migrate format never says clean while such a bulk setting
hides rules from it.
A folder whose own .editorconfig turns every analyzer off (dotnet_analyzer_diagnostic.severity = none or silent,
with or without root = true; test samples, for example) is left alone instead: init writes StyleBro's rules, IDE0055
and the other built-in rules it manages as none by id there (a rule's own entry wins over the bulk one, in the
preset as in the root block), so the build reports nothing there and format doesn't touch those files, not even their
whitespace. A rule that folder's .editorconfig sets by id stays as you set it. These lines go into a block between
# BEGIN stylebro-folder and # END stylebro-folder (in a root = true folder: its stylebro-migrate block, with
nothing else in it). Your own IDE0055 severity (say suggestion) is kept everywhere: the block doesn't set it then, and
format still formats whitespace, except where your severity is none (by id, for the Style category or in bulk):
format leaves the whitespace of those files alone, as it does where a block turns IDE0055 off.
init also looks at your code¶
init looks at your code first and keeps the conventions it clearly follows. For each setting below it counts both
forms in your C# (generated code, EF Core migrations, vendored folders and submodules don't count; code inside #if
counts too, as format sees it in the target framework that defines the symbol):
Vendored folders are recognized by their name (vendor, third_party, ...). Code copied in under another name (a
src/Sdk from another product) counts as yours and can decide a convention; mark its folder generated_code = true in
your root .editorconfig ([src/Sdk/**], see excluding vendored code) and run init again:
folders marked that way don't count.
- fewer than 3 places (or none): too few to tell, StyleBro's default stays and nothing is written;
- 3 to 9 places: followed when they all agree;
- 10 places or more: followed when one form has at least 75 %;
- mixed (no form gets there): the StyleBro rule that enforces the setting is turned off, so the code stays as it is
(
off:dotnet_diagnostic.BRO1520.severity = none;both: for braces on one-line bodiescsharp_prefer_braces = when_multiline, which allows both). Where no StyleBro rule enforces the setting ({placement,<inheritdoc/>spacing,default(T)), the verdict ismixed: the default stays and nothing is turned off.
A followed form that is StyleBro's default needs no line; any other is written into the block. A key your root
.editorconfig sets wins, so does a Sonar setup's, and a dotnet_naming_rule for private fields decides the field style
(naming rules for interfaces or constants don't). One exception: when your .editorconfig sets an SDK key that only a
StyleBro rule enforces (operator placement, braces = true, using placement, arithmetic parentheses =
always_for_clarity) and your code clearly does the other thing, nothing ever enforced that key. Then init leaves the
key alone and turns that rule off (your .editorconfig says end_of_line, ... so BRO1520 is off), instead of letting it
rewrite the code. The same goes when the code mixes both forms under such a key (the starter kit says
beginning_of_line, 486 operators start the line and 285 end it): nothing enforced the key, so the rule goes off as for
any mixed convention (.editorconfig says beginning_of_line, unenforced, code mixed: BRO1520 is off). Running init
again keeps such a line, even when format changed the counts.
A key your .editorconfig sets at severity silent or none (csharp_prefer_braces = true:silent, or
dotnet_diagnostic.IDE0011.severity = silent next to it) isn't enforced by anything: it's what the IDE wrote, not what the
team checks. init treats it like a key that isn't set and follows the code; where the code's form differs from the key,
it writes the code's form into the block, which comes later in the file and wins (also when that form is StyleBro's
default). This applies to every convention whose key takes a severity: braces (IDE0011), using placement (IDE0065),
arithmetic parentheses (IDE0048), default values (IDE0034) and operator placement.
Folders with their own root = true .editorconfig (a sample/ with its own solution): the root's settings never
reach their files, since root = true stops the lookup there. init gives each such folder with C# code a block of its
own in its .editorconfig, with the built-in rule severities, judged from that folder's code and settings alone (the
report has a section per folder). A folder with too little code to tell keeps StyleBro's defaults, and the report says so.
stylebro-migrate --write writes the whole block into such a folder too.
A few settings work a little differently:
- One-line statements (
if (a) return;,case 1: a(); break;): the preset setscsharp_preserve_single_line_statements = false, which moves each to a line of its own. From 3 such statements on,initwritestrue, majority or not, unless your.editorconfigsets the key. <inheritdoc/>on overrides and implementations (BRO1601): in projects that generate documentation,initcounts the overrides and interface implementations BRO1601 checks, with and without a doc comment. Mostly documented: the rule stays on; mostly undocumented or mixed: it's turned off.- Line endings count files. Without
end_of_line,formatwrites your OS's line ending into the lines it rewrites (on Windows CRLF into LF files), soinitwrites the files' ending when it's clear and nothing when it's mixed. Files git converts on checkout (core.autocrlf, or a.gitattributestextattribute withouteol: LF in the repository, CRLF in a Windows checkout) don't count, and the report names which of the two converts them: there the OS's ending is what the checkout has, and a fixed one would makeformatrewrite every file on the other OS. When git converts at least as many files as it leaves alone, those are the convention and nothing is written: a few files committed with CRLF don't decide for the rest (one trial repository had 3 such files next to 145 converted ones, andend_of_line = crlffrom those 3 meant 12,593 line-ending errors in an LF checkout).stylebro-migrate --writewritesend_of_linethe same way. When your.editorconfigsetsend_of_linebut git converts the files on checkout (end_of_line = lfwith*.cs text=automeans CRLF files in a Windows checkout),initkeeps endings out of it:end_of_line = unsetfor C# files and IDE0055 as a suggestion, so neitherformatnor the build touches them, and the report says how to settle it (*.cs text eol=lfin.gitattributes, or removingend_of_line). When yourend_of_linesays one ending but the files git leaves alone clearly have the other (crlf, with nearly every file LF), nothing ever enforced it:initwrites the files' ending for C# files, soformatdoesn't flip every file it touches, and the report says how to switch to the key's ending instead. s_/t_prefixes count private static fields (constants aside) with and without the .NET runtime'ss_(ort_) prefix. Mostly without: BRO1307 stays on; mostly with or mixed: it's turned off, soformatdoesn't rename them (there is no switch for static fields alone, som_fields keep their prefix too). Adotnet_naming_ruleof yours that requiress_decides instead.- Type names count files: files that name built-in types only with keywords (
string,int), and files with a framework name IDE0049 would rewrite (String,System.Int32). Mostly keywords: IDE0049 stays on; mostly framework names or mixed: it's turned off. Your owndotnet_style_predefined_type_*keys decide instead. - Member order (BRO1001) counts types whose members BRO1001 would sort apart (two or more
members with different sort keys), with your own order settings: already in its order or not. A type that is in order
once overloads stay together counts as sorted (whether they do is the overloads convention's call). Mostly sorted: the
rule stays on; mostly unsorted or mixed: it's turned off, so
formatdoesn't reorder code that was never sorted (StabilityMatrix: 370 sorted, 414 not). Only type members count: BRO1001 also sorts the types of a namespace, but most files hold one type, so they would say little. - Unsigned literal suffixes (BRO1135) count
u/ulsuffixes in upper or lower case (0Uor0u);Lalone doesn't count, since nobody writes1l. Mostly upper case: the rule stays on; mostly lower case or mixed: it's turned off (Orleans: 423 upper, 185 lower). With it off, BRO1122 writes6u/1ulinstead of6U/1UL. - Items of split lists (BRO1108) count argument and parameter lists whose items start on more
than one line: each item on its own line, or grouped (
F(tag, 1,and"a", "b")on the next line). Mostly one per line: the rule stays on; mostly grouped or mixed: it's turned off (metadata-extractor: 104 one per line, 197 grouped). - Implicit
private(BRO1404) counts the members of classes, structs and records that are private when nothing says otherwise (fields, methods, properties, events, constructors, nested types; not what BRO1404 skips: partial members, explicit implementations, static constructors): withprivateor without an access modifier. Mostly with: the default stays; mostly without:dotnet_style_require_accessibility_modifiers = omit_if_default, so neither BRO1404 nor BRO1007 adds modifiers (IDE0040, which would remove them, is off ininit's block); mixed: BRO1404 is turned off. Interface members are counted apart: when the.editorconfigsaysalways(which asks forpublicon interface members too), the build doesn't enforce it (noEnforceCodeStyleInBuild), and the code's interface members are mostly without an access modifier or mixed,initwritesfor_non_interface_members, so BRO1404 adds nopublicthere (BaGetter: none of its 102 interface members had one). - Implicit
internal(BRO1404) counts the types in a namespace or a file (not partial ones): withinternalor without an access modifier. No setting asks for modifiers on types only, so mostly without, or mixed, turns BRO1404 off (AngleSharp: 273 types without, 26 withinternal). caseblocks count blocks below acaselabel (case X:, then{on the next line): indented one level below the label or at its level. Mostly at its level:csharp_indent_case_contents_when_block = false, so the SDK formatter (IDE0055) keeps them (AngleSharp: all 131 at the label's level).- Combined declarations (BRO1142 for locals, BRO1114 for fields) count
declarations of two or more (
int n = h.Length, i = 0;) and one-variable declarations right after one of the same type and modifiers they could have been combined with (notvar, not fields with attributes). Mostly one by one: the rule stays on; mostly combined or mixed: it's turned off, soformatdoesn't split them. Locals and fields are counted apart: a repository may combine locals in hot loops and never fields. - Combined attributes (BRO1102) count declarations with two or more attributes (not parameters
or type parameters): in one list (
[A, B]) or one per list. Mostly one per list: the rule stays on; mostly combined or mixed: it's turned off, soformatdoesn't split them. - Summary wording (BRO1604 for properties, BRO1606 for constructors) counts
summaries that start the StyleCop way (
Gets ...,Gets or sets ...;Initializes a new instance of ...) against ones in their own words (The samples, ...,Whether ...;Opens a file ...). Mostly the StyleCop way: the rule stays on; mostly their own words or mixed: it's turned off, soformatdoesn't reword public documentation. A summary that starts with an element (<see cref="..."/>) isn't counted. - CSharpier: when the repository uses CSharpier (a
.csharpierrc,.csharpierrc.json,.csharpierrc.yamlor.csharpierrc.yml, or acsharpiertool in.config/dotnet-tools.jsonordotnet-tools.json, at the root or in a solution's or project's folder or a folder above it; or aCSharpier.MsBuildpackage reference), the two formatters would undo each other's layout (StabilityMatrix's pre-commit check rejected 161 files StyleBro had formatted).initthen turns off IDE0055 (sostylebro-migrate formatskips the whitespace pass; when CSharpier formats only some folders, IDE0055 is off in those folders' sections, one section per folder CSharpier formats, and the whitespace pass leaves just them out) and every StyleBro rule for something CSharpier decides, and the layout conventions in the table are reported asset(CSharpier formats it). Off: - line breaks in lists: BRO1107-BRO1110, BRO1116-BRO1119; constraints BRO1111, constructor initializers BRO1105, enum values BRO1121, query clauses BRO1127-BRO1130;
- wrapping: BRO1520-BRO1524 (operators,
=>,=, call chains,?:); - single-line blocks and members, accessors: BRO1508-BRO1510, BRO1527;
- the blank lines CSharpier normalizes itself: before
{andelse/catch/finally(BRO1501, BRO1502), after{(BRO1503), before}(BRO1518), before a do-while'swhile(BRO1512), runs of them (BRO1517), and the ones CSharpier removes where StyleBro would add them: before a comment (BRO1504: CSharpier removes it inside initializers and aboveelse) and between elements (BRO1505: CSharpier removes it between accessors).
Everything else stays: naming, member order, documentation, braces on one-line bodies, readability rules, and the
blank lines CSharpier keeps as you wrote them (after }, after attributes, around doc comments, between switch
sections). init also writes stylebro_layout_by = csharpier and CSharpier's line width
(stylebro_layout_print_width: printWidth from the .csharpierrc, else max_line_length, else 100): an if merged
into the enclosing one (BRO1149) gets its condition the way CSharpier writes it, and the brace
rules count a statement CSharpier will split over lines as multi-line, so its braces come in the same run. When
CSharpier formats only one folder (its tool or config there, with C# code outside it), these settings go into that
folder's section ([Source/**.cs]) and the rest of the code is judged as usual. CSharpier
sorts using directives itself (System first), so init writes no sort key (dotnet format sorts usings whenever
dotnet_sort_system_directives_first or dotnet_separate_import_directive_groups is set, and its order differs from
CSharpier's in places: App3._0 against App3_0). Where your .editorconfig sets one (false, or true for the
groups), init writes the closer value in its block: a key can't be unset. Run
CSharpier after format: other fixes that rewrite code (attributes split, parentheses added) leave lines in StyleBro's
layout, which CSharpier then lays out its way, and StyleBro leaves that alone: after CSharpier, stylebro-migrate
format changes nothing more. stylebro-migrate --write (from StyleCop) does the same. To have StyleBro
format instead, remove CSharpier and run init again.
- Running init again keeps the convention lines the first run wrote (the ones below # Conventions in this
repository's code, with their # init: comments), and the report says kept from the last init. They describe your
code as it was before StyleBro formatted it: counted again after a format run, StyleBro's own output would decide
(a rule that added blank lines in some places would make your code look mixed, and the setting would flip). To have a
convention counted again, delete its line from the block and run init again; to count everything again, delete the
block. Conventions the first run left at StyleBro's default (no line) are counted again each time.
- Private constants and static readonly fields (BRO1306) count three forms: PascalCase,
camelCase and _camelCase. A clear one is written as stylebro_private_static_field_naming (PascalCase is the
default and needs no line); mixed turns BRO1306 off, so format doesn't rename them (Ical.Net: 26 PascalCase, 8
camelCase, 50 _camelCase; BRO1306 renamed them in 28 files). With it off, public and internal fields aren't checked
either. A dotnet_naming_rule for constants or static readonly fields decides instead.
- Usings inside a namespace (BRO1126) count fully qualified ones (using Company.App.Common;)
and ones relative to an enclosing namespace (using Common; inside namespace Company.App.Orders). Mostly qualified:
the rule stays on; mostly relative or mixed: it's turned off (Fitnet: 91 qualified, 92 relative). A name is relative
when an enclosing namespace has a namespace your code declares with its first part; a namespace from a package doesn't
count.
- File headers with empty // lines: when your file_header_template starts or ends with a line break (IDE0073
writes an empty // line there), or most files' headers start or end with an empty // line, init turns
BRO1120 off: it would remove those lines, and every new file from the template would bring them
back (Ical.Net: 222 of 223 files).
- Charset and final line breaks: when your .editorconfig sets charset (utf-8 or utf-8-bom) or
insert_final_newline but the C# files don't clearly follow it (BOM or not, last line break or not, the same 75 %),
nothing ever enforced it, and format would add or strip the BOM or the last line break in every file. Like a line
ending the files contradict, init writes the files' value for C# files (unset when they're mixed: each file keeps
its own), and the report says how to switch (remove the line and run stylebro-migrate format). Ical.Net:
charset = utf-8 with 108 of 223 files starting with a BOM, all 108 stripped; the starter kit:
insert_final_newline = false with 1,092 of 1,597 files ending in a line break, all 1,092 removed. stylebro-migrate
--write does the same. trim_trailing_whitespace = false can't be kept this way: dotnet format doesn't read the key
and trims trailing whitespace on every line it formats, so the report only says so (with IDE0055 off nothing is trimmed).
These keys (and end_of_line) are judged per file against the .editorconfig that decides it, as the compiler reads
them: the nearest one that sets the key wins, within it the last matching section. A nearer file overrides anything
the root block says, so when a folder's own .editorconfig (one without root = true, like Bogus's Source/) sets
the key, init writes its line there, in a block between # BEGIN stylebro-folder and # END stylebro-folder
(Bogus: the root block's insert_final_newline = unset never reached Source/, and format stripped 48 files' last
line break).
- Spacing that IDE0055 enforces counts places on one line: a space after if/for/foreach/while/switch/
using/lock/catch or not (if (x) or if(x)), spaces inside parentheses (control-flow statements, parenthesized
expressions, casts), after a cast ((int) x), and inside call and parameter lists (F( x )). A clear form other than
the SDK's default is written (csharp_space_after_keywords_in_control_flow_statements,
csharp_space_between_parentheses, csharp_space_after_cast,
csharp_space_between_method_call_parameter_list_parentheses,
csharp_space_between_method_declaration_parameter_list_parentheses). When any of them is mixed, IDE0055 is turned
off with one # init: spacing: note, since it can't be limited to one option, and format skips the whitespace pass
(Bogus: foreach( var x in y ) in 341 places, 65 %, which IDE0055 had rewritten). With CSharpier, column alignment or
an IDE0055 severity of your own, nothing is written.
- Blank lines between members (BRO1505) count the neighbouring members of a type that need one
(one spans several lines, or is documented), with a blank line between them or without; blank lines above doc
comments (BRO1513) count doc comments right below code or after a blank line. Mostly with: the rule
stays on; mostly without or mixed: it's turned off, so format doesn't add blank lines to compact code (Kavita: 392
and 312 files).
- Blank lines below // comments (BRO1506) count the comments it checks (the last line of a
comment group; not the file header, trailing comments or comments below code) right above the code or with a blank line
below. Mostly right above: the rule stays on; mostly with a blank line or mixed: it's turned off (RecyclableMemoryStream
keeps one below commented-out tests).
- this.: when your .editorconfig requires it (a dotnet_style_qualification_for_* key = true:warning, or
= true with IDE0009 at suggestion or above, by its id, the Style category's severity or the bulk one), the block
leaves IDE0009 alone instead of turning it off.
- Interface members: dotnet_style_require_accessibility_modifiers = always asks for public on interface members
too. When your code doesn't have them and the build doesn't enforce it (EnforceCodeStyleInBuild with IDE0040 at
warning or error by id, category or bulk; a :warning after the value doesn't count in the build), init writes
for_non_interface_members, so BRO1404 doesn't add them.
- else/catch/finally placement counts only blocks that span lines: in a one-line method
({ if (x) { return 1; } else return 2; }) the layout isn't a choice.
- Blank lines below doc comments (BRO1511) count doc comments right above their element or with
a blank line between. Mostly right above: the rule stays on; mostly with one or mixed: it's turned off (MoreLINQ).
- Primary-constructor parameters (BRO1302) count those named like parameters (logger) or like
the fields they replace (_logger). Mostly _logger or mixed turns BRO1302 off: it has no switch for these
parameters alone, so it's then off for every parameter (the # init: line says so).
- Tuple element names (BRO1311) count (int Count, ...) and (int count, ...). Mostly camelCase
writes stylebro_tuple_element_name_casing = camelCase; mixed turns the rule off.
- Preset version: init --write sets <StyleBroPreset> in each Directory.Build.props your projects read, but not
in one whose projects no solution lists and that doesn't mention StyleBro.Analyzers (example or test-fixture folders).
A Directory.Build.props it has to create below another one imports that one, so nothing the projects read gets lost.
- Run from a subfolder: when the folder you run init (or stylebro-migrate --write) in is part of a git repository
whose build reaches past it (a Directory.Build.props, Directory.Packages.props, .editorconfig or .globalconfig
above it, or a solution in it that lists projects outside it, like build/App.sln), it works on the repository's root
and says so. A self-contained folder stays on its own.
- .globalconfig: the root's .globalconfig counts like its .editorconfig for what your team set and enforces (a
this. it requires, its IDE0055 severity, the conventions' keys). With AnalysisMode All and
EnforceCodeStyleInBuild, every code-style rule is a warning in the build, so a dotnet_style_qualification_for_* = true
without a severity counts as enforced.
- A folder that turns the Style category off (dotnet_analyzer_diagnostic.category-Style.severity = none in its
.editorconfig): a rule's own entry wins over a category, so init turns the built-in rules its block sets by id off
there too (in the folder's own block), and says so.
- Column alignment: when your .editorconfig aligns code in columns for ReSharper or Rider
(resharper_int_align_variables = true, resharper_csharp_int_align_fields = true and the other int_align keys;
the comments' alignment aside), IDE0055's whitespace formatting would flatten it (RestSharp:
var doc = CreateAttributesXml(); became var doc = CreateAttributesXml(); in 171 files). init turns IDE0055
off, with a # init: comment, so stylebro-migrate format skips the whitespace pass. No StyleBro rule changes the
spaces inside a line, so they stay on. An IDE0055 severity of your own wins.
- One-line jumps (if (x) return;, also break, continue, throw, goto, yield break on the if line): from
3 of them on, init writes stylebro_allow_single_line_jump_statements = true, majority or not, so
BRO1514 doesn't brace them (the starter kit: 878 of 1,130 bodies with braces, and BRO1514 braced
194 one-line jumps).
- #regions inside members (BRO1113, the starter kit's #region Arrange / Act / Assert in
its tests): from 3 of them on, the rule is turned off, so format doesn't remove them. BRO1112
(regions between members) is off in the preset already.
- Using order counts files with 2 or more usings: sorted with System first, sorted without, or not sorted. dotnet
format sorts usings whenever dotnet_sort_system_directives_first is set (to any value), so the preset doesn't set
it: init writes it when your usings are sorted (or there are too few files to tell), and only a comment when they
aren't, or when some files put System first and others don't. When your .editorconfig sets the key itself and 3 or
more files aren't in its order, the report says how many format will reorder and that removing the key keeps them
(the starter kit: false, 262 of 1,120 files); init can't do it for you, since unset sorts them too.
- No root .editorconfig without code for it: when all your C# sits in folders whose .editorconfig says
root = true (the starter kit's src/) and the root has no .editorconfig, init writes none there. Likewise the
preview adds the package reference only where projects with C# files read it (a template pack project without code
doesn't get a root Directory.Build.props).
| Setting | What's counted | Rule | Key |
|---|---|---|---|
| Private field names | field or _field |
BRO1303 | stylebro_private_field_naming |
{ placement |
{ of a multi-line block on its own line or at the end of the line |
SDK formatter (IDE0055) | csharp_new_line_before_open_brace |
else/catch/finally |
after } on a new line or the same line |
SDK formatter (IDE0055) | csharp_new_line_before_else, _catch, _finally |
| Braces | one-line if/else/for/foreach/while/using/lock bodies with or without braces |
BRO1514-BRO1516 | csharp_prefer_braces |
| Operator placement | binary and ?: operators at the start or end of a wrapped line |
BRO1520 | dotnet_style_operator_placement_when_wrapping |
=> placement |
=> of expression bodies and switch arms where a line wraps |
BRO1521 | stylebro_arrow_placement_when_wrapping |
= placement |
= where a line wraps |
BRO1522 | stylebro_equals_placement_when_wrapping |
| Trailing commas | multi-line initializers, enums and switch expressions | BRO1401 | stylebro_trailing_comma |
| Empty strings | string.Empty or "" (outside constant contexts) |
BRO1106 | stylebro_empty_string_style |
| Null checks | is null or == null |
BRO1133 | stylebro_null_check_style |
| Summaries | one-line <summary> texts with the tags on their own lines or on one line |
BRO1616 | stylebro_summary_layout |
<inheritdoc/> |
<inheritdoc/> or <inheritdoc /> |
BRO1601 | stylebro_inheritdoc_style |
| Default values | default(T) or default |
IDE0034 | csharp_prefer_simple_default_expression |
) of split lists |
after the last item or on its own line | BRO1110 | stylebro_closing_parenthesis_placement |
| First item of split lists | on the next line or after ( |
BRO1107 | stylebro_split_list_first_item |
| Items of split lists | one per line or grouped on lines | BRO1108 | dotnet_diagnostic.BRO1108.severity |
| Leading multi-line lambdas | calls whose first argument is a multi-line lambda, anonymous method or anonymous object: the other arguments on its last line (}, Kind);) or one argument per line (mixed: both allowed) |
BRO1107, BRO1108 | stylebro_allow_leading_multiline_lambda |
| Constructor initializers | : base(...)/: this(...) on its own line or the constructor's |
BRO1105 | stylebro_constructor_initializer_placement |
| Constraints | where on its own line or the declaration's |
BRO1111 | stylebro_constraint_placement |
new T { ... } |
with or without () before an initializer |
BRO1141 | stylebro_object_creation_parentheses |
| Switch sections | after a blank line or without one | BRO1526 | stylebro_blank_line_between_switch_sections |
| Using directives | files with a namespace and their usings on one level: inside it (file-scoped too) or outside; only inside_namespace is written, the preset already says outside_namespace |
BRO1008 | csharp_using_directive_placement |
| Arithmetic parentheses | a + (b * c) or a + b * c |
BRO1406 | dotnet_style_parentheses_in_arithmetic_binary_operators |
| One-line statements | a braceless if/else/loop body or a case label's statements on their owner's line or below it |
SDK formatter (IDE0055) | csharp_preserve_single_line_statements |
| Documented overrides | overrides and interface implementations with or without a doc comment | BRO1601 | dotnet_diagnostic.BRO1601.severity |
| Member order | types whose members BRO1001 sorts apart: already in its order or not | BRO1001 | dotnet_diagnostic.BRO1001.severity |
| Overloads | overload groups whose members BRO1001 sorts to different places (by access, static, ...): next to each other or apart |
BRO1001 | stylebro_keep_overloads_together |
| Unsigned literal suffixes | u/ul suffixes in upper or lower case (0U or 0u) |
BRO1135 | dotnet_diagnostic.BRO1135.severity |
s_/t_ prefixes |
private static fields with or without the s_/t_ prefix |
BRO1307 | dotnet_diagnostic.BRO1307.severity |
Implicit private |
members that are private by default, with or without private |
BRO1404 | dotnet_style_require_accessibility_modifiers |
Implicit internal |
types that are internal by default, with or without internal |
BRO1404 | dotnet_diagnostic.BRO1404.severity |
| Partial parts | partial class X parts with or without an access modifier |
BRO1007 | dotnet_diagnostic.BRO1007.severity |
case blocks |
a block below a case label indented or at the label's level |
SDK formatter (IDE0055) | csharp_indent_case_contents_when_block |
| Combined attributes | [A, B] or one attribute per list |
BRO1102 | dotnet_diagnostic.BRO1102.severity |
| Combined locals | int a, b; or one local per declaration |
BRO1142 | dotnet_diagnostic.BRO1142.severity |
| Combined fields | int _a, _b; or one field per declaration |
BRO1114 | dotnet_diagnostic.BRO1114.severity |
| Type names | files with only keywords (int) or framework names (Int32) |
IDE0049 | dotnet_diagnostic.IDE0049.severity |
| Private constants and static readonly fields | PascalCase, camelCase or _camelCase |
BRO1306 | stylebro_private_static_field_naming |
| Usings inside a namespace | fully qualified or relative to an enclosing namespace | BRO1126 | dotnet_diagnostic.BRO1126.severity |
| File headers | headers with or without an empty // line at the top or bottom (or a file_header_template with one) |
BRO1120 | dotnet_diagnostic.BRO1120.severity |
| One-line jumps | if (x) return; on the if line, or a lone jump in braces |
BRO1514 | stylebro_allow_single_line_jump_statements |
| Empty blocks | { } on one line or on two (mixed: both allowed) |
BRO1508, BRO1509 | stylebro_allow_empty_single_line_blocks |
| One-line properties | properties with a block-bodied accessor on one line (X { get { return x; } }) or on several |
BRO1509 | dotnet_diagnostic.BRO1509.severity |
| Accessor bodies | multi-line properties whose accessors all have block bodies: all one-line or all multi-line, or mixed | BRO1510 | dotnet_diagnostic.BRO1510.severity |
| Auto-property accessors | { get; set; } on the declaration's line or on several lines |
BRO1527 | dotnet_diagnostic.BRO1527.severity |
#regions inside members |
the #regions inside members (3 turn the rule off) |
BRO1113 | dotnet_diagnostic.BRO1113.severity |
| Column alignment | your resharper_*int_align_* keys set to true |
SDK formatter (IDE0055) | dotnet_diagnostic.IDE0055.severity |
| Charset | files with or without a BOM, when the charset that applies to them doesn't match them |
SDK formatter | charset |
| Final line breaks | files with or without a last line break, when the insert_final_newline that applies to them doesn't match them |
SDK formatter | insert_final_newline |
| Line endings | files with LF or CRLF line endings (not files git converts on checkout) | SDK formatter | end_of_line |
| Using order | files with 2 or more usings: sorted with System first, sorted without, or not sorted | dotnet format |
dotnet_sort_system_directives_first |
| Spacing | if (x) or if(x); spaces inside parentheses, after casts, inside call and parameter lists (mixed: IDE0055 off) |
SDK formatter (IDE0055) | csharp_space_after_keywords_in_control_flow_statements, csharp_space_between_parentheses, csharp_space_after_cast, csharp_space_between_method_call_parameter_list_parentheses, csharp_space_between_method_declaration_parameter_list_parentheses |
| Member separation | neighbouring members that need a blank line, with or without one | BRO1505 | dotnet_diagnostic.BRO1505.severity |
| Blank line above doc comments | doc comments right below code or after a blank line | BRO1513 | dotnet_diagnostic.BRO1513.severity |
| Blank line below comments | // comments right above code or with a blank line below |
BRO1506 | dotnet_diagnostic.BRO1506.severity |
| Blank line below doc comments | doc comments right above their element or with a blank line between | BRO1511 | dotnet_diagnostic.BRO1511.severity |
| Primary-constructor parameters | named logger or _logger (classes and structs) |
BRO1302 | dotnet_diagnostic.BRO1302.severity |
| Tuple element names | (int Count, ...) or (int count, ...) |
BRO1311 | stylebro_tuple_element_name_casing |
The keys are described in Settings. init prints each verdict (kept, default, off, both,
mixed, too few, or set when your configuration decides) with the value that applies, then a summary that names the keys
(RealWorld):
Conventions in the code (judged from 3 places: all must agree below 10, 75% from 10 on).
off: mixed, or not what your setting says, so the rule that enforces it is turned off; both: mixed, both allowed;
mixed: the default stays, nothing is turned off (no StyleBro rule enforces it, or your .editorconfig sets the rule):
too few private fields: 2 places -> default stays (camelCase)
set '{' of multi-line blocks: 196 of 196 on its own line -> set in .editorconfig (all)
too few operators where a line wraps: 1 place -> default stays (beginning_of_line)
kept multi-line initializers: 18 of 18 without a trailing comma -> omit
default empty strings: 3 of 3 string.Empty -> string_empty
off null checks: 7 'is null', 10 '== null' -> BRO1133 is off
off split lists: 13 first item on the next line, 17 first item after '(' -> BRO1107 is off
...
Summary:
Kept your style for 5 settings: wrote stylebro_trailing_comma; 4 were already StyleBro's default.
Turned 2 rules off because your code mixes both forms: BRO1133 (null checks), BRO1107 (split lists).
To choose later: set the key in .editorconfig, remove its init line, run 'stylebro-migrate format'.
4 settings had fewer than 3 places to tell, StyleBro's defaults stay: stylebro_private_field_naming,
dotnet_style_operator_placement_when_wrapping, stylebro_equals_placement_when_wrapping, stylebro_summary_layout.
Each written line gets a comment in the block saying why:
# init: multi-line initializers: without a trailing comma in 18 of 18 places in your code
stylebro_trailing_comma = omit
# init: null checks: your code mixes both forms (7 'is null', 10 '== null'), so BRO1133 is off. To choose one: set stylebro_null_check_style and remove the next line.
dotnet_diagnostic.BRO1133.severity = none
Changing a detected setting later¶
Set the key in your own part of .editorconfig, outside the block (for example stylebro_null_check_style =
equality_operator), remove the rule's severity = none line from the block, and run stylebro-migrate format to bring
the code in line. The block is rewritten each time init runs, but a key you set yourself wins over what it counts: it
then writes neither the setting nor the severity = none line.
EF Core migrations (files with [Migration(...)], a Migration base class or a ModelSnapshot) are written by
dotnet ef, so init marks their folders generated_code = true: formatting and StyleBro leave them alone. Vendored
folders (vendor, vendored, third_party, thirdparty, external) get the same section.
When it finds a StyleCop setup (stylecop.json, StyleCop rule ids in .editorconfig, rulesets or
global configs, a StyleCop.Analyzers reference), it stops without writing and tells you to run
stylebro-migrate --write instead: that's path B, which turns StyleBro's rules on only where
StyleCop enforced them.
With SonarQube or SonarCloud (a SonarAnalyzer.CSharp reference, Sonar rule severities in rulesets or configs, or a
quality profile you export and pass with --sonar-profile profile.xml), init also turns on the StyleBro and .NET rules
that fix what your Sonar rules report (S927 -> BRO1313, S4136 -> overloads kept together, ...) and prints which:
Coming from SonarQube. The same works for stylebro-migrate --write in path B.
3. Run stylebro-migrate format¶
stylebro-migrate format
This fixes whitespace, the built-in rules and every StyleBro rule. One fix can make work for another rule, so the
command runs dotnet format again until a run changes no file (at most three runs) and prints what each run changed
(Run 1: 293 files changed: and the first 10 of them, Run 2: 0 files changed, clean.); --once runs it once, with the same report and the findings kept in that run. It ends
with the findings StyleBro's fixes leave on purpose, each with its reason (Findings kept on purpose),
and exits with 0 when clean, 2 when a run still changed files. When dotnet format skips a project because its
references didn't load (a broken restore or package cache), the command names it and says the run is incomplete (for
one target framework only: the code only that framework compiles); --diff stops then instead of showing a partial
preview, and --verify-no-changes fails when a project wasn't loaded in any run (a project that references an F#
project, which dotnet format can't load, stays a warning). When no project has StyleBro.Analyzers in its restore output (obj/project.assets.json), the command stops
before dotnet format runs: without the analyzers it would fix only whitespace and the built-in rules and report the
code clean. Projects without it next to ones with it are named (Without StyleBro: ...). The command restores once
before the runs; that restore skips NuGet's audit (-p:NuGetAudit=false): vulnerability warnings (NU1901-NU1904) are
the build's business, and with TreatWarningsAsErrors they failed every run. A solution filter (.slnf) works like
its solution, with only its projects. A solution with several filters of it next to it (Steeltoe's
Steeltoe.All.slnx and its seven .slnf parts) counts as that solution; the command says how to name a filter instead.
A folder with several solutions or projects needs one named (stylebro-migrate format MySolution.slnx): the command lists them instead of picking one.
With none at the root, it looks for a Traversal project there (dirs.proj with Sdk="Microsoft.Build.Traversal",
which dotnet format can't load: format runs on a temporary solution with the projects it lists), then for solutions
one folder down (src/MySolution.slnx): the only one is used, and the command says so (Using src/MySolution.slnx);
several are listed, as at the root. baseline and --diff find them the same way.
If dotnet format crashes, the command names the projects of that run, and when the crash came after some files were
written, lists every file it changed (Run 1 failed after changing 12 files:).
A solution in a folder of its own (solutions/App.slnx listing ../src/...) works too: the command runs from the
folder that holds all its projects. Where the repository enforces IDE0005 (unnecessary usings, by id, its Style category
or the bulk severity), the command fixes it too: other fixes (UInt32 to uint) can leave a using unused. After the
runs it names the generated files git tracks in the projects it changed (*.g.cs, *.Designer.cs, a T4 template's
output, files with an <auto-generated> header): X is generated and checked in: rebuild and commit it. Where
CSharpier formats the repository, it ends with a reminder to run CSharpier.
Review the diff and commit it. In an established codebase, land that commit when few pull requests are open (they
will conflict with it), and add its hash to .git-blame-ignore-revs so git blame skips it (GitHub reads that file
too; locally: git config blame.ignoreRevsFile .git-blame-ignore-revs). In a large codebase you may not want one big
change: see Large codebases.
Why not plain dotnet format? It also applies every other analyzer's fixes and the compiler's own, and some of
those change behavior or break the build: on one trial repository it added required to properties (CS8618) and a
Sonar fix removed init; accessors, nine build errors. stylebro-migrate format passes dotnet format only StyleBro's
rule ids and the built-in rules init or the migration turn on (--diagnostics ...); whitespace formatting runs as
usual. It also runs once per target framework where needed (Multi-targeted repositories)
and never touches git submodules. The plain equivalent is dotnet format --diagnostics with the ids to fix:
dotnet format --diagnostics BRO1001 BRO1505 BRO1601 IDE0055 IDE0036 BRO1008
stylebro-migrate format --all applies everything, like plain dotnet format.
What to expect on the first run: solution names, run time, warnings as errors.
4. Check it in CI¶
Run stylebro-migrate format --verify-no-changes --severity warn after the build: it changes nothing and fails (exit
code 2) when formatting would change a file. Findings kept on purpose don't fail it; plain
dotnet format --verify-no-changes fails on those too, so a script or an AI agent that loops "until clean" never gets
there with it. Set-up for GitHub Actions and Azure Pipelines: ci.md. init and the migration
name the workflow lines (.github/workflows, azure-pipelines.yml) that still run plain dotnet format --verify-no-changes.
To check the staged files before each commit: Pre-commit hook.
With --agents-md, init --write also puts a short StyleBro section into AGENTS.md for AI coding
agents (opt-in). For scripts and agents there are format --files (just the files an edit touched) and --json:
Scripts and AI agents.
B. Coming from StyleCop¶
Still deciding? StyleBro vs StyleCop compares the two with measured numbers.
stylebro-migrate reads your StyleCop setup (rulesets, global configs, .editorconfig files, stylecop.json) and
writes StyleBro and SDK settings that enforce the same things, so code StyleCop was happy with stays as it is. A
walk-through on a small project, with real output: samples/StyleCopMigration.
1. Dry run¶
stylebro-migrate path/to/repo
It prints what it read, the settings it would write, and the StyleCop rules nothing enforces after the switch (dropped
by design, partly covered, or not covered yet). Nothing is changed. To also see what the settings and the format run
would do to the code: stylebro-migrate path/to/repo --diff (Preview first).
2. Write the settings¶
stylebro-migrate path/to/repo --write
This writes the settings into your .editorconfig files (between the stylebro-migrate markers), turns StyleBro's
preset off in the root Directory.Build.props (the settings replace it), and adds the replacing rule ids to existing
StyleCop suppressions (#pragma warning disable, [SuppressMessage], <NoWarn>). You don't need init after this:
the block includes the built-in rules.
3. Swap the package¶
Remove the StyleCop reference and add StyleBro.Analyzers instead, as in path A, step 1. Look
for both package ids, StyleCop.Analyzers and the prerelease StyleCop.Analyzers.Unstable, and for a
GlobalPackageReference in Directory.Packages.props (central package management): replace it with
<GlobalPackageReference Include="StyleBro.Analyzers" Version="..." />. stylecop.json and the StyleCop suppressions
can stay; StyleBro ignores them.
4. Run stylebro-migrate format¶
stylebro-migrate format
Why not plain dotnet format: step 3 of path A. In a StyleCop-clean repository this should change
little or nothing. Look at what it did change: usually code StyleCop
missed, or a rule where StyleBro deliberately differs (differences-from-stylecop.md).
Then check the report from step 1 for rules your team relied on that nothing enforces any more. Details:
migrating.md. CI: ci.md.
Newer C# and APIs (optional)¶
To also rewrite code into newer C# and newer APIs (target-typed new(), collection expressions, file-scoped
namespaces, ArgumentNullException.ThrowIfNull, ...) with the SDK's own rules, after either path:
stylebro-migrate init --modernize --write
dotnet format
In multi-targeted projects, StyleBro's multi-target guard hides the newer-API rules where a target framework lacks the
API, and the newer-C# rules are suggestions unless the project sets LangVersion. Tiers, examples and the rules left out:
modernizing.md.
What to expect on the first run¶
- Name the solution when the root has several. With more than one project or solution file in the folder (say
Build.csprojnext toMySolution.slnx),dotnet formatstops with MSB1011 ("more than one project or solution file"). Pass the one to format:dotnet format MySolution.slnx(the same forstylebro-migrate format). - Multi-targeted repositories take longer.
stylebro-migrate formatrunsdotnet formatonce per target framework, and each run loads the solution again. That's minutes, not seconds: Dapper, which builds in 13 seconds, took 345 seconds to format. TreatWarningsAsErrorsfails the first build until everything is fixed or recorded: StyleBro's findings are warnings, so they become errors. Rundotnet format(orstylebro-migrate format) before building, or record what's left in a baseline.- Some renames are left to you. The naming rules don't rename a member whose name also appears in a string
(reflection, serialized names,
DebuggerDisplay): the rename would compile but break at run time. Those warnings stay afterdotnet format: see Findings kept on purpose.
Findings kept on purpose¶
Every StyleBro finding has a fix, but a fix can see more than the analyzer: the whole solution, other projects
included. When a change would break something only the fix can see, it leaves the finding, the warning stays, and the
fix offers no code action for it. That's the one exception to "dotnet format fixes everything StyleBro reports",
and it's on purpose. The reasons, per finding:
- the name is in a string elsewhere in the solution (serialized names and test JSON for properties and public fields;
for other fields only where code reaches a member by name:
GetField("_count"),GetProperty(...),Type.GetType(...),[MemberData("...")], an attribute'sName = "..."): the rename would compile but break at run time; - the name is in a
nameof(...)elsewhere, whose text would change with it (an HTTP header name, a log key); - another member would hide the new name, or the new name already means something else where the name is used;
- a use is in generated code (a source generator, a Razor page, a designer file), which a tool writes again;
- a related override or implementation is public API, or also implements a member that isn't renamed;
- the rename would add compile errors (tuple element names), or the namespace is also defined outside the solution.
stylebro-migrate format lists them after its last run:
Run 2: 0 files changed, clean.
Clean: 1 finding kept on purpose (listed below). StyleBro's fixes leave these: renaming by hand breaks what the check protects. Check those uses first, or suppress or baseline them (...).
src/Counter.cs(5,17): BRO1303 Rename '_count' to 'count'. Kept: the name is in a string in the solution (reflection, serialization): renaming would compile but break at run time.
What to do with one: don't rename by hand to silence it (that's what breaks the reflection or the generated code).
Check the uses the reason names; if they can change too, rename both by hand. Otherwise suppress it with a reason
(#pragma warning disable BRO1303 // read by reflection in Tests), or record it in the baseline.
With TreatWarningsAsErrors the kept findings are build errors after the first run, so make dealing with them part
of it: run stylebro-migrate format, then either suppress each one as above or run stylebro-migrate baseline (it
records exactly what is left, the kept findings included), and commit that together with the formatting. The list is
printed after every run, also when format ends "Not clean".
Exit codes of stylebro-migrate format, for CI and agents:
| Exit code | Plain run | --verify-no-changes |
|---|---|---|
| 0 | clean: the last run changed nothing (kept findings don't count) | formatting would change nothing; only kept findings are left |
| 2 | still changing after three runs | formatting would change a file |
| 1 | a project no run loaded (Incomplete:): its files weren't checked |
|
| other | dotnet format failed (load or build errors) |
the same |
--verify-no-changes changes nothing: when dotnet format --verify-no-changes reports findings, it formats the files
with findings (for their rules) in a temporary copy of the repository to see whether any file would change and which
findings the fixes keep, then deletes the copy. After a clean stylebro-migrate format on the same code, settings and
options, it already knows the findings that run kept (recorded in the temporary folder) and formats no copy.
Plain dotnet format --verify-no-changes can't tell the two apart and fails on kept findings.
Excluding vendored code¶
Code you don't own (a copied library, generated sources checked in, a ThirdParty folder) shouldn't be reformatted.
Mark it as generated code in the root .editorconfig:
[ThirdParty/**]
generated_code = true
# Entity Framework Core migrations: generated by 'dotnet ef', regenerated rather than edited
[**/Migrations/**]
generated_code = true
EF Core's migration files (Migrations/*.cs and the model snapshot) are generated but carry no <auto-generated>
header, so without this the first run reformats every one of them (291 files in one trial repository).
StyleBro's rules, the built-in rules (IDE0055, IDE0073, ...) and dotnet format's whitespace pass all skip generated
code, in the build and in dotnet format. One side effect: the compiler treats generated files as nullable-oblivious
unless they say #nullable enable themselves.
dotnet_analyzer_diagnostic.severity = none in that folder isn't enough: a rule turned on by its id
(dotnet_diagnostic.BRO1001.severity = warning in the root) wins over it. stylebro-migrate --write therefore writes
every replacing rule as none into such a folder's .editorconfig, and it never edits files inside git submodules
(folders with their own .git).
Files linked from a submodule. A project can compile files from another folder (<Compile Include="..\..\ext\Lib\Code.cs" />,
or a shared .projitems). stylebro-migrate format never formats anything inside a git submodule (it passes --exclude
with every submodule folder), so StyleBro doesn't report findings there either: the package's build targets read the
submodule paths from the .gitmodules above the project and hide StyleBro's findings in those folders. A folder that
is not a submodule (a copied library) still needs the generated_code = true section above.
Multi-targeted repositories¶
If any project sets several <TargetFrameworks>, plain dotnet format doesn't work at all; stylebro-migrate format
does. It takes the same arguments and options:
stylebro-migrate format
stylebro-migrate format MySolution.sln --verify-no-changes --severity warn
Plain dotnet format crashes there on the SDK's formatting fix (IDE0055) without writing anything (reported upstream as dotnet/roslyn#86000 and dotnet/roslyn#86001). The command runs
dotnet format once per target framework instead; without multi-targeted projects it's one run. A diagnostic
every framework sees is printed once, and --report writes one format-report.json covering all runs. Spellings of
one framework share a run (net6 and net6.0, NuGet's short names). A run loads only the projects that target its
framework, so the renaming fixes also check the names in every other C# file of the repository: a field the tests
read with GetField("count") keeps its name in the library's run too, and a name a project outside the run uses
(UsedInProjectNotLoaded) is left to a run that loads both, or kept. Files a project compiles from outside its own
folder (<Compile Include="..\Shared\*.cs" />) are formatted in the runs of the projects that compile them, and
--files finds such a file's project too. Projects that compile the same files with other settings (a library and its AOT variant
compiling ..\Lib\*.cs with other DefineConstants, language version or nullable context for that framework) run in
separate parts of a framework's run (== net8.0, part 1 of 2), each with what it references: loaded together, dotnet
format crashes on their different edits of one file (dotnet/roslyn#86001).
Projects that compile a shared file with the same settings (a polyfill every library links) share a run: they parse it
the same, so its copies get the same edits (<LangVersion>latest</LangVersion> counts as the version it means). This
applies without multi-targeted projects too. Why: ci.md. stylebro-migrate init tells you when it finds such projects.
A framework whose code compiles exactly like another's in a project gets no run of its own there: when no #if tests a
symbol that tells them apart (say the code tests NET8_0_OR_GREATER but nothing newer), net8.0, net9.0 and net10.0
see the same files, settings and branches, and one run formats them all. Only frameworks of one kind share a run (.NET,
.NET Standard or .NET Framework, and the same platform such as -windows), since their APIs differ more. The
Multi-targeted: line names the frameworks that ran and those that didn't need to.
After a run that changed files, the next one checks every framework at once first: a run whose check is clean isn't made again, and one whose check found something formats only the files with findings (all of its files when an earlier run of the same round changed a file it formats too).
A multi-line string can hold lines starting with # (#pragma warning disable in a source generator's test input)
inside an #if branch some framework leaves out: there those lines read as directives, and the SDK's formatter moves
them to column 0, breaking the string where the branch is compiled (CS8999). format names such files (Protected:
...) and, while dotnet format runs, swaps those lines' # for a character the file doesn't have, then puts it back,
so the rest of the file is formatted as usual. If format is killed before it puts the # back (a closed terminal, a
crash), the next stylebro-migrate command does: it keeps a record of the swap in %TEMP%/stylebro-mask ($TMPDIR on
Linux and macOS) and says Restored: <file>. A file changed in a way that moved the swapped characters is left alone
and named (Not restored: ...); format and --verify-no-changes then stop with Stopped: ... until the # is back.
A project dotnet format can't load at all (F#, .fsproj) makes it skip the C# projects that reference it, for the
frameworks where the reference matters. format names them once, with that cause ("Partly incomplete: ... Likely
cause: they reference TestFSharp.fsproj"); a restore doesn't change it, and the rest of the repository is formatted.
Old-style (non-SDK) projects¶
Projects in the old format (<Project ToolsVersion="15.0" ...> without Sdk=, usually .NET Framework with
packages.config) work with stylebro-migrate format, with a few extra steps:
- Restore with Visual Studio's MSBuild first, from a Developer Command Prompt:
msbuild -t:restore -p:RestorePackagesConfig=true
dotnet restore can't restore packages.config, and for the other old-style projects it writes restore output that
Visual Studio's MSBuild then rejects ("Your project file doesn't list 'win' as a RuntimeIdentifier"). So format
restores only the SDK-style projects when it finds old-style ones, and says so (the -p:RestorePackagesConfig=true
part only matters with packages.config). If a build complains like that anyway (something ran
dotnet restore), run the MSBuild restore again.
- Reference assemblies. dotnet format loads the projects with the .NET SDK, which needs the .NET Framework
reference assemblies: without them every project is skipped ("Required references did not load"). Install the
targeting pack (the .NET Framework Developer Pack, or Visual Studio's ".NET Framework 4.x targeting pack"), or use the
Microsoft.NETFramework.ReferenceAssemblies.net462 NuGet package (the version your projects target) and point
MSBuild at it with an environment variable, which reaches both msbuild and dotnet format:
set TargetFrameworkRootPath=%NUGET_PACKAGES%\microsoft.netframework.referenceassemblies.net462\1.0.3\build.
- Install StyleBro.Analyzers the packages.config way, like StyleCop was: Visual Studio's package manager
(or Install-Package StyleBro.Analyzers in its console) writes all three parts, or add them by hand. In
packages.config:
<package id="StyleBro.Analyzers" version="0.7.0-alpha.1" targetFramework="net462" developmentDependency="true" />
In the project (paths relative to it; the packages folder is next to the solution):
<ItemGroup>
<Analyzer Include="..\packages\StyleBro.Analyzers.0.7.0-alpha.1\analyzers\dotnet\cs\StyleBro.Analyzers.dll" />
<Analyzer Include="..\packages\StyleBro.Analyzers.0.7.0-alpha.1\analyzers\dotnet\cs\StyleBro.CodeFixes.dll" />
</ItemGroup>
<!-- at the end, after the Microsoft.CSharp.targets import: the preset and the baseline -->
<Import Project="..\packages\StyleBro.Analyzers.0.7.0-alpha.1\build\StyleBro.Analyzers.targets" Condition="Exists('..\packages\StyleBro.Analyzers.0.7.0-alpha.1\build\StyleBro.Analyzers.targets')" />
plus an <Error Condition="!Exists('...StyleBro.Analyzers.targets')" ... /> line in the project's
EnsureNuGetPackageBuildImports target, as NuGet adds for every package with build targets. Don't use a
PackageReference there: it makes NuGet restore the project the PackageReference way, its packages.config packages
are no longer restored, and a fresh clone doesn't build. A PackageReference in a Directory.Build.props that also
serves SDK-style projects needs a condition that keeps it away from them:
<PackageReference Include="StyleBro.Analyzers" Version="0.7.0-alpha.1" PrivateAssets="all" Condition="'$(UsingMicrosoftNETSdk)' == 'true' or !Exists('$(MSBuildProjectDirectory)\packages.config')" />
stylebro-migrate names the packages.config projects and says this in its report.
- --diff leaves packages.config projects out: the copy it formats has no git-ignored packages folder, and
dotnet restore can't restore packages.config. It previews the other projects and names the ones it left out; only
when every project uses packages.config does it stop. Other old-style projects preview like format runs:
unrestored, so the preview has no StyleBro changes for them, and it says which. To see the changes in the
packages.config projects, on a clean working tree: run the command without --diff, restore with MSBuild as above,
run stylebro-migrate format, and review git diff (git checkout . undoes it).
- If StyleCop came in through packages.config or an <Analyzer Include="..\packages\StyleCop.Analyzers.1.0.2\...">
hint path, stylebro-migrate reads its version there: rules that version doesn't have stay off.
Large codebases¶
If dotnet format would change too much at once, record today's violations in a baseline instead:
stylebro-migrate baseline # in the folder with your solution
stylebro-migrate baseline --project MySolution.sln # or name it
This writes stylebro.baseline; commit it. The build, the IDE and dotnet format then ignore those violations, and
only new code has to follow the rules. A baseline can't hide whitespace formatting. Details: baseline.md.
How long a full run takes. format loads the whole solution once per run, and in a multi-targeted repository once
per target framework. On Orleans (about 200 projects, several target frameworks) the first stylebro-migrate format
took 57 minutes and about 9 GB of memory, and format --verify-no-changes 11.5 minutes. A repository that size with
TreatWarningsAsErrors is best started with a baseline: the build passes right away, and the backlog can be fixed a
project at a time (stylebro-migrate format path/to/Project.csproj) or a few files at a time (--files).