Skip to content

Multi-targeted repositories

If any project sets several <TargetFrameworks>, plain dotnet format doesn't work at all; bro format does. It takes the same arguments and options:

bro format
bro 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 project the solution doesn't list but one of its projects references (so the build compiles it) is formatted in the run of its framework; one with several frameworks is listed under "Leaving alone" (add it to the solution). So is a project that needs a workload this machine doesn't have (an Android, iOS or Browser head: dotnet restore fails on it with NETSDK1147); the rest is formatted, and after dotnet workload restore the next run formats it too. 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. bro 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. A file several projects compile (<Compile Include="..\Tests\Helpers.cs" />) doesn't split them into separate parts when they only define other symbols it doesn't test.

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, plus those an earlier run of the same round changed. A run that changed only line endings or byte order marks (end_of_line, charset) is the last one: nothing is left for another to fix.

dotnet format may warn that it couldn't load part of the workspace in one framework's run ('dotnet format' had warnings loading the workspace in its net6.0 run). To see the warnings, add --verbosity diagnostic to the same bro format command: a plain dotnet format loads the projects differently and may not show them.

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 bro 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 dotnet format that outlives a killed format still writes those files: they're put back once it has ended, and until then format stops with Stopped: another 'bro format' .... A # line in an #elif branch or under a condition like NET8_0_OR_GREATER && !NET10_0_OR_GREATER, and a string's own #if/#endif lines, are protected too.

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. Any other project skipped for some frameworks leaves the code only those frameworks compile unchecked: when another framework compiles the same code (one format merged into the skipped one's run, e.g. netstandard2.0 into netstandard1.3), that one runs instead (== netstandard2.0, instead of netstandard1.3); otherwise --verify-no-changes says Not verified rather than clean.