Skip to content

Fix three rules that could not report, and add the checks that catch it - #8

Merged
nanchen2483 merged 12 commits into
Notalib:mainfrom
henrikottesorensen:main
Aug 3, 2026
Merged

Fix three rules that could not report, and add the checks that catch it#8
nanchen2483 merged 12 commits into
Notalib:mainfrom
henrikottesorensen:main

Conversation

@henrikottesorensen

@henrikottesorensen henrikottesorensen commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Why

dotnet_diagnostic.CS8019.severity = warning has been in the globalconfig asking for unused usings to be reported. It has never reported anything: CS8019 is emitted hidden by the compiler, and a severity set in config does not raise it. One consuming solution had 45 unused directives behind it, across 36 files, and every build was green.

Nothing in this repository could have told us. What we ship is configuration, and configuration fails quietly: a rule that cannot report looks exactly like a rule being obeyed, because the build is silent either way.

That is the argument for the verification added here — and it earned its place immediately, by catching two more failures of the same shape in changes made on this branch, before any of them merged:

Introduced here How it failed Caught by
UsingLayoutAnalyser was built against a newer Roslyn than the SDK running it skipped with CS9057, a warning; the using rules simply did not run pinning the pipeline's SDK and watching verify.sh go red
a version bump added PrivateAssets to the threading analyser stopped it reaching consumers, so VSTHRD100 could never fire for anyone verify-package.sh

Neither reached the package. Both are the same failure as CS8019: configured, believed, incapable of reporting. One had been in the repository for years; the other two took an afternoon to create.

So this PR fixes the dead rule, and adds the checks that catch the shape.

Rules

  • IDE0005 replaces the dead CS8019. GenerateDocumentationFile, which it needs, was already set. CS8019 stays with a comment saying it is inert, so nobody sets it again expecting an effect.
  • SA1412 is off, and NOTA0001 replaces it. The byte-order-mark requirement was added believing it was needed for container builds. It is not — a file without a BOM compiles fine. But it was, by accident, the only thing catching a file saved as Windows-1252, which compiles with no warning at all and reaches the assembly as U+FFFD. NOTA0001 is an MSBuild task in build/Nota.CodeAnalysis.targets, because an analyser cannot see this: by the time one runs, the text has been decoded and the bytes are gone. NotaValidateSourceEncoding=false opts out.
  • UsingLayoutAnalyser owns the using layout — System, then third party, then the consuming solution's own namespaces, one run per vendor. dotnet_separate_import_directive_groups promised this and delivered half: SA1516 uses it for the System boundary only, and the per-vendor grouping is IDE0055's job, which is off. SA1210 has to go off for the new layout; SA1208, SA1209, SA1211, SA1216 and SA1217 are unaffected.
  • usinglayout.first_party_prefixes defaults to Nota. A consumer whose code is called something else overrides it in their own .editorconfig — verified that this takes precedence over the package's global config, rather than assumed.

Verification

Nota.CodeAnalysis.Verification is a consumer built against files that break the rules on purpose, plus three scripts:

  • verify.sh — the six rules report. Confirmed to fail correctly by switching IDE0005 off.
  • verify-encoding.sh — every source file is valid UTF-8, or BOM-marked UTF-16 (svcutil and EF migrations emit that, and those files must keep their BOM).
  • verify-package.sh — packs, installs into a throwaway project, and checks each rule survived. This is the only one that catches a package which installs cleanly and does nothing.

That last one was itself broken when first written: it passed against the exact regression it exists to catch, because NuGet extracts a package once per version and re-packing 2.2.0 served the cached copy. It packs under a throwaway version now, and both states are confirmed — it fails naming VSTHRD100 with the regression present, and passes without it.

Build

The Azure pipeline is replaced by a GitHub Action. GITHUB_TOKEN replaces the variable-group PAT, and the secure NuGet.config turns out never to have been needed — every dependency is public, checked rather than assumed.

Two behaviour changes worth noting:

  • SDK 10, not 8. On SDK 8 the using rules answer CS9057 and silently do not run.
  • Publishing is tag-gated. Merging no longer publishes; v* supplies the version. Previously, forgetting to bump <Version> published nothing while looking successful, because --skip-duplicate declines a version already present.

Pull requests build and run all three verifications without publishing.

For the reviewer

Everything asserted here was measured against a built package rather than reasoned about — including the claims that turned out wrong on the first attempt. The commit messages carry the evidence and the failures, and are worth reading individually.

Consumers of 2.1.4 will see new diagnostics on upgrade: unused usings, the using layout, and any mis-encoded file. dotnet format analyzers --diagnostics UA1000 UA1001 --severity warn handles the layout in one pass.


Co-Authored-By: Claude Opus 5 noreply@anthropic.com

henrikosorensen and others added 12 commits August 2, 2026 19:25
… fire.

Three things, all found by running this package's own globalconfig against probe
files rather than by reading it.

CS8019 never worked. It has asked for unused usings to be reported for as long as
it has been here, and a file with an unused using builds clean: CS8019 is emitted
hidden by the compiler and a severity in config does not raise it. IDE0005 is the
rule that reports; GenerateDocumentationFile, which it needs, was already set in
build/Nota.CodeAnalysis.props. One consuming repository had forty-five unused
directives behind this.

dotnet_separate_import_directive_groups promised more than it delivered. SA1516
uses it for the blank line after the System group and that works; the per-vendor
grouping the name suggests is IDE0055's job, and IDE0055 is off, so nothing
checked it. Rather than enable IDE0055 - which would also start enforcing
indentation and spacing, and reflow every consumer - UsingLayoutAnalyser now owns
the layout: System, then third party, then the consuming solution's own
namespaces, one run per vendor. SA1210 has to go off for it, because it sorts the
whole list alphabetically and wants a solution's own root above a vendor whenever
it sorts earlier. SA1208, SA1209, SA1211, SA1216 and SA1217 do not conflict and
stay on. first_party_prefixes cannot live here - it differs per solution - so
consumers set it in their own .editorconfig, and the scheme degrades sensibly
when they do not.

Nothing verified any of this, and pull requests did not build. That is how a dead
CS8019 line survives: the product is configuration, and configuration fails
silently. Nota.CodeAnalysis.Verification is a consumer that breaks five rules on
purpose, and verify.sh asserts each one reported. Its samples compile only under
-p:VerifyRules=true, because some of these rules are error severity and an
ordinary solution build must not fail. Confirmed to work in both directions:
switching IDE0005 back off makes verify.sh fail naming it.

Pull requests now build and verify; pack and push stay conditioned on main so a
pull request cannot publish.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The BOM requirement was added to get code building in Docker containers. It never
did that. A file without a BOM compiles fine - Roslyn defaults to UTF-8 when no
BOM is present - and the whole of the largest solution builds and passes with the
marks stripped, on macOS and in the container. Whatever the original problem was,
this was not its fix.

SA1412 is what required it, and editing charset does not change that: SA1412 does
not read charset at all, and demands a BOM whether it says utf-8 or utf-8-bom.
Off is the only setting that moves it. charset is set to utf-8 to say what the
convention is, and to be correct if this content is ever used as an .editorconfig.

But the rule was mislabelled rather than useless, and dropping it alone would have
left things worse. It was the only thing standing between the build and a file
saved as Windows-1252 - measured: such a file compiles with no warning, and the
compiler writes U+FFFD into the assembly. Silent, and it reaches the binary. No
analyser can report it, because by the time an analyser runs the text has already
been decoded; and it applies to .resx and .json as much as to .cs. So
verify-encoding.sh, running in the same pipeline stage.

It accepts BOM-marked UTF-16, which is what svcutil and EF migrations emit and
what the compiler reads correctly - and those files must keep their BOM, since it
is the only record of their encoding. It cannot catch a wrong encoding that
happens to produce valid UTF-8, the "“" case, which is indistinguishable from
someone writing those characters deliberately.

Verified against a tree with spaces in its paths, a UTF-16 .resx, and a Latin-1
file - the clean tree passes and the bad file is caught. That combination is
deliberate: an earlier draft passed paths through an unquoted variable, and every
file under "Service References" was reported as corrupt because the name has a
space in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It reads like a test project and is not one: nothing here ships, and it exists
because a rule that cannot report looks exactly like a rule being obeyed. The
CS8019 story is written down, because it is the argument for the whole project
and it will otherwise be forgotten the first time someone wonders why a project
full of deliberately broken files is in the solution.

Also records the two things easiest to get wrong later: watch a check fail before
trusting it, and remember this exercises the globalconfig's content rather than
the package, so a wrong PackagePath would still ship silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removing SA1412 left consumers with nothing. verify-encoding.sh guards this
repository, but a package cannot ask every consumer to wire up a script - so the
net effect of the previous commit, for anyone installing the package, was that
the accidental guard went away and no replacement arrived. That is exactly the
combination that leaves people worse off, and it shipped.

build/Nota.CodeAnalysis.targets now validates every file being compiled, before
CoreCompile, and fails with NOTA0001 naming the file. It has to happen there:
a file saved as Windows-1252 compiles with no warning and reaches the assembly as
U+FFFD, and an analyser cannot see it because by the time an analyser runs the
text has been decoded and the bytes are gone.

UTF-16 with a BOM passes - svcutil and EF migrations emit it, the compiler reads
it correctly, and such a file must keep its BOM. Only @(Compile) is read, so .resx
and .json are not covered; reading the whole tree on every build was not worth
what it costs. NotaValidateSourceEncoding=false switches it off.

Verified as a consumer sees it, not just in this solution: packed, installed from
a local feed into a fresh project, clean source builds and one Windows-1252 file
fails with NOTA0001. That path matters because it is the one the verification
project does not cover - it imports the globalconfig directly, so a broken
PackagePath would still ship silently.

NOTA0001 is added to verify.sh, which needs a deliberately mis-encoded sample, so
Samples/ is now excluded from verify-encoding.sh: those files are wrong on
purpose and that check is for files that are wrong by accident.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The readme was a single sentence, and it is not only the repository's front page:
the csproj packs it to content/README.md and declares it as PackageReadmeFile, so
it is what anyone browsing the package sees. It now says how to install it, what
switches on, the one key a consumer has to set themselves, and which rules
surprise people.

Four things are worth a stranger's attention before their first build, and none
were written down anywhere: IDE0008 and VSTHRD100 are errors rather than warnings,
NOTA0001 is a build task and not a diagnostic so it has its own opt-out, and
usinglayout.first_party_prefixes cannot be shipped by the package because it
differs per solution - unset, the using layout quietly stops distinguishing your
code from a vendor's.

GenerateDocumentationFile gets a line of its own. It looks like an unrelated
documentation setting and is the switch that makes IDE0005 report at all; someone
will eventually decide it is redundant.

Nota.CodeAnalysis/README.md is deleted. It was a near-duplicate of the first line
of this one, nothing referenced it, and it was not the file that reaches the
package - two readmes saying almost the same thing is how one of them goes stale
without anyone noticing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The using layout needs to know which namespaces are the consumer's own, and the
previous commit left that as the one thing every repository had to remember. A
setting every repository must remember is a setting most repositories will not
have - and unset, the layout quietly stops distinguishing Nota code from a
vendor's, which is the whole point of it.

Nota is right for nearly everything here, so it is the default. A consumer whose
code is called something else overrides it in their own .editorconfig, and getting
it wrong is not fatal: their namespaces are sorted as one more vendor rather than
last.

The precedence this relies on was verified rather than assumed - the built package
installed into a project whose .editorconfig disagreed with it, and the
.editorconfig won. That holds for every severity in the globalconfig too, so the
readme now says so as a fact rather than an expectation.

The comment above the setting had said this key could not live here. It is now the
line directly beneath it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pipeline installed SDK 8 and would have packed a release whose using rules did
nothing. An analyser cannot reference a newer Roslyn than the compiler running it:
UsingLayoutAnalyser is built against 4.14, SDK 8.0.423 runs 4.11, and the compiler
answers with CS9057 - a warning, not an error - then carries on without it.
Verified on that exact SDK, where StyleCop reported normally and UA1000 and UA1001
were simply absent.

Which is this package's own failure mode one level out: a rule that is configured,
believed, and incapable of reporting. Nothing about the build looks wrong.

The verification project moves to net10.0 for the same reason. On net8.0 it would
still pass while proving less, because the analyser it is meant to be verifying
could not be loaded at all.

Worth noting the guard worked: pinned to SDK 8, verify.sh fails with "UA1000 did
not report - it is configured but not reaching consumers". Had this been missed,
the pipeline would have gone red rather than shipping.

Consumers building on SDK 8 still get CS9057 and no using rules. That is
acceptable while everything here is on .NET 10; the durable fix is to build
UsingLayoutAnalyser against an older Roslyn, which costs nothing it currently
uses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The repository is on GitHub and the packages go to GitHub Packages, so the build
was the only part living elsewhere - and it paid for that with a variable group
holding a personal access token and a secure NuGet.config downloaded on every run.
GITHUB_TOKEN replaces the first. The second turns out to have been unnecessary
all along: every dependency here is public, and the solution restores from
nuget.org alone, which was checked rather than assumed.

It also settles a question the Azure version left open. Those verification steps
were Bash@3 tasks against self-hosted agents whose operating system I never
established; on ubuntu-latest the question does not arise.

Same shape as before: build, verify the rules report, verify the encoding, pack,
and publish only from main. Pull requests do everything except publish, and the
package is uploaded as an artifact either way so a packaging mistake is visible
without merging.

SDK 10 rather than 8, for the reason recorded in the previous commit: on 8 the
using rules answer CS9057 and then silently do not run.

Actions are pinned to current majors. upload-artifact@v4 was written first and
replaced with v7 - v4 runs on Node 20, which is the deprecation this pipeline
would have started warning about immediately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… again.

0.2.1 is built against Roslyn 4.8 rather than 4.14, so the using rules load on
every SDK from .NET 8 onwards. On 0.2.0 an SDK 8 consumer got CS9057 - a warning -
and no UA1000 or UA1001 at all.

Microsoft.VisualStudio.Threading.Analyzers loses the PrivateAssets that Rider
added when its version was bumped. That is the right default for an ordinary
project and wrong for this one: PrivateAssets stops a reference reaching
consumers, and for an analyser that means the rules never arrive. The globalconfig
went on declaring VSTHRD100 an error while no consumer could receive the analyser
that reports it - configured, believed, and incapable of firing, which is the
failure this package has now produced three times.

Verified through the built package rather than the solution, because the solution
cannot show it: the verification project references the analysers directly, so
VSTHRD100 fired there throughout while consumers got nothing. Packed, installed
from a local feed, and an async void method now fails the build with VSTHRD100.
The nuspec declares all four analysers again.

None of these references may carry PrivateAssets, and the item group now says so,
because the next version bump will offer to add it back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…onfigured.

verify.sh runs inside the solution, where the globalconfig is imported directly
and the analysers are referenced by the project itself. A rule can pass there
while no consumer receives it, and that has now happened twice: UsingLayoutAnalyser
built against a newer Roslyn than the SDK running it, skipped with CS9057; and the
threading analyser acquiring PrivateAssets in a version bump, so VSTHRD100 went on
firing here while consumers got nothing at all. Neither was visible from inside.

verify-package.sh packs, installs into a throwaway project from a local feed, and
compiles a file breaking one rule per analyser - IDE0008 for the globalconfig and
props, NOTA0001 for the targets, SA1208, VSTHRD100, UA1000 and Serilog003 for the
four analysers reaching a consumer at all. CS9057 fails it outright, being a
warning nothing else would notice.

It packs under a throwaway version rather than the real one. That is the whole
check, not a detail: NuGet extracts a package once per version into the global
cache, so re-packing 2.2.0 and installing 2.2.0 gets whatever was extracted first
and the change under test never arrives. Written the obvious way, this script
passed cleanly against the exact regression it exists to catch. It was only found
by putting PrivateAssets back and watching what happened - which is what the
project's own readme tells everyone to do, and which I had not done.

Both states are now confirmed: with the regression it fails naming VSTHRD100,
without it, all rules survive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Merging and releasing were the same act, which made the version number a thing
someone had to remember to change before merging - and forgetting it published
nothing while looking like it had, because --skip-duplicate silently declines a
version already in the registry.

A v* tag now publishes, and the tag is the version: nothing in the repository can
disagree with what shipped. Pushes to main and pull requests still build and run
all three verifications; they simply stop before publishing.

<Version> in the csproj stays as a local default for anyone packing by hand, and
is now labelled as one. On a tag it is overridden for both build and pack, so the
assembly and the package agree.

Both paths were run locally: with no tag the pack is 2.2.0 from the csproj, and
with VERSION_ARG set as the workflow sets it, 2.3.0. The unset case matters - an
undefined variable in a run step expands to nothing rather than erroring, which is
what lets one command serve both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both were described as things that had cost this repository, which reads as
shipped bugs found in the package. They were not. UsingLayoutAnalyser is added by
this branch and its CS9057 problem never existed anywhere else; the threading
analyser's PrivateAssets was added by an editor during a version bump on this same
branch. Neither reached a consumer.

CS8019 is the one that was genuinely there for years, and the distinction matters
to anyone reading this later to judge how much the checks are worth: two of the
three failures were created and caught inside an afternoon, which is a better
argument for them than a history of shipped mistakes would have been, and it
happens to be the true one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@henrikottesorensen
henrikottesorensen requested review from a team, ddfreiling and m-abs August 2, 2026 20:18
@nanchen2483
nanchen2483 merged commit 49423f6 into Notalib:main Aug 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants