Skip to content

feat: FirstClassErrors Roslyn analyzers (FCE001–FCE016)#14

Merged
Reefact merged 37 commits into
mainfrom
analyzers
Jul 4, 2026
Merged

feat: FirstClassErrors Roslyn analyzers (FCE001–FCE016)#14
Reefact merged 37 commits into
mainfrom
analyzers

Conversation

@Reefact

@Reefact Reefact commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Adds a set of Roslyn analyzers that catch, at build time, the mistakes the FirstClassErrors runtime and documentation pipeline would otherwise surface late or silently. They are bundled in the FirstClassErrors NuGet package (analyzers/dotnet/cs), so any consumer of the package gets them automatically — no extra install.

Rules (16)

Error codes

  • FCE001 DuplicateErrorCode — 🔴 Error
  • FCE002 EmptyErrorCode — 🔴 Error
  • FCE003 NonLiteralErrorCode — 🔵 Info (opt-in)
  • FCE004 InvalidErrorCodeFormat — 🔵 Info (opt-in)
  • FCE005 TooGenericErrorCode — 🔵 Info (opt-in)

Documentation wiring

  • FCE006 DocumentedByTargetNotFound — 🔴 Error
  • FCE007 DocumentedByInvalidSignature — 🔴 Error
  • FCE008 DocumentedByWithoutProvidesErrorsFor — 🔴 Error
  • FCE009 ErrorFactoryNotDocumented — 🟠 Warning
  • FCE010 MultipleFactoriesShareDocumentation — 🟠 Warning

Documentation content

  • FCE011 DuplicateDocumentedCode — 🔴 Error
  • FCE012 EmptyExamples — 🟠 Warning
  • FCE013 ExampleDoesNotCallDocumentedFactory — 🟠 Warning
  • FCE014 ShortMessageSameAsDetailedMessage — 🔵 Info
  • FCE015 DocumentationTitleTooGeneric — 🔵 Info (opt-in)

Usage

  • FCE016 UnusedToExceptionResult — 🟠 Warning

Several rules catch failures that are otherwise silent: a mistyped [DocumentedBy], a documented factory in a type missing [ProvidesErrorsFor], or two documented factories that collapse to one entry in the generated catalog. FCE001 and FCE011 are whole-compilation checks (tagged CompilationEnd).

What's included

  • FirstClassErrors.Analyzers (netstandard2.0) + FirstClassErrors.Analyzers.UnitTests (xUnit v3 — 48 tests), wired into the solution.
  • Packaging: the analyzer is packed into the FirstClassErrors NuGet package via TargetsForTfmSpecificContentInPackage.
  • Dogfooding: FirstClassErrors.Usage references the analyzer as OutputItemType=Analyzer and builds clean (0 warnings / 0 errors).
  • CI: .github/workflows/analyzers.yml runs restore + build + tests + the dogfood build.
  • Docs: doc/analyzers/ — one reference page per rule plus an index, linked from both READMEs.

Notes for the reviewer

  • The rules' help links point at main (doc/analyzers/FCExxx.md) — they resolve once this is merged.
  • The branch carries a few Merge remote-tracking branch commits from local pulls; they do not change the code. Squash and merge yields a clean single commit on main.
  • The 16 rule pages are currently English-only; a French translation can follow.

🤖 Generated with Claude Code

claude and others added 30 commits July 4, 2026 14:38
…and CI

Add the FirstClassErrors.Analyzers project (netstandard2.0) and its xUnit v3
test project, wired into the solution under the existing src/tests folders.

Includes the diagnostic id/category catalog for the agreed 16 rules, a
dependency-free in-process analyzer test harness (compiles a snippet against
the running runtime + the FirstClassErrors core, runs one analyzer, returns
its diagnostics), empty analyzer release-tracking files, and a GitHub Actions
workflow that restores/builds/tests the analyzers.

The repository has no CI; this workflow is the validation path since the
development environment cannot build .NET locally. No diagnostic rules yet —
those land one commit per FCExxx.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Report ErrorCode.Create("") / whitespace / null literal arguments, which throw
an ArgumentException at runtime, as a build-time error. Only literal arguments
are inspected; non-literal codes are out of scope (reserved for FCE003).

Covered by four tests (empty, whitespace, valid, non-literal) exercised through
the in-process analyzer harness.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Report a [DocumentedBy("X")] whose referenced method does not exist on the
containing type. The reference is resolved by name at extraction time, so a
typo is silently skipped and the error goes undocumented.

Introduces two shared helpers used here and by the next wiring rules:
KnownSymbols (resolves FirstClassErrors types by metadata name) and
SymbolFacts (attribute lookup + type-inheritance checks).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Report a [DocumentedBy("X")] whose target method exists but cannot serve as a
documentation factory: it must be static, parameterless and return
ErrorDocumentation. A missing target stays FCE006's concern, so this rule is
silent when no method of that name exists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Report a type that declares [DocumentedBy] factories but is missing
[ProvidesErrorsFor]. Extraction only scans types carrying [ProvidesErrorsFor],
so such documentation is silently ignored. Reported once per type.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
…and CI

Add the FirstClassErrors.Analyzers project (netstandard2.0) and its xUnit v3
test project, wired into the solution under the existing src/tests folders.

Includes the diagnostic id/category catalog for the agreed 16 rules, a
dependency-free in-process analyzer test harness (compiles a snippet against
the running runtime + the FirstClassErrors core, runs one analyzer, returns
its diagnostics), empty analyzer release-tracking files, and a GitHub Actions
workflow that restores/builds/tests the analyzers.

The repository has no CI; this workflow is the validation path since the
development environment cannot build .NET locally. No diagnostic rules yet —
those land one commit per FCExxx.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Report ErrorCode.Create("") / whitespace / null literal arguments, which throw
an ArgumentException at runtime, as a build-time error. Only literal arguments
are inspected; non-literal codes are out of scope (reserved for FCE003).

Covered by four tests (empty, whitespace, valid, non-literal) exercised through
the in-process analyzer harness.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Report a [DocumentedBy("X")] whose referenced method does not exist on the
containing type. The reference is resolved by name at extraction time, so a
typo is silently skipped and the error goes undocumented.

Introduces two shared helpers used here and by the next wiring rules:
KnownSymbols (resolves FirstClassErrors types by metadata name) and
SymbolFacts (attribute lookup + type-inheritance checks).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Report a [DocumentedBy("X")] whose target method exists but cannot serve as a
documentation factory: it must be static, parameterless and return
ErrorDocumentation. A missing target stays FCE006's concern, so this rule is
silent when no method of that name exists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Report a type that declares [DocumentedBy] factories but is missing
[ProvidesErrorsFor]. Extraction only scans types carrying [ProvidesErrorsFor],
so such documentation is silently ignored. Reported once per type.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Report the same literal error code created by more than one ErrorCode.Create("X")
in the compilation, lighting up every participating site. ErrorCode.Create
registers each code in a process-wide set and throws when a code is created
twice; this shifts the failure to build time.

Detection aggregates occurrences across the whole compilation (CompilationStart
-> operation collect -> CompilationEnd report) with ordinal comparison, matching
the runtime registry. Cross-assembly duplicates and non-literal codes remain out
of scope (the latter is FCE003); empty codes are left to FCE002.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Report a call to Error.ToException() whose result is discarded as a standalone
statement. ToException() only builds the exception; without a throw (or
capturing the result) the error is silently lost.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Report a non-private static factory in a [ProvidesErrorsFor] type that returns
an Error but has no [DocumentedBy]; such an error is left out of the generated
catalog. Private methods are treated as helpers and skipped to limit false
positives. Adds Error to the shared KnownSymbols resolver.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Report factories in the same type whose [DocumentedBy] reference the same
documentation method. One documentation method describes one error, so sharing
it (title, description, examples) means at least one error is mis-documented.
Every sharing factory is flagged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Report the terminal WithExamples() call of the documentation DSL when given no
example factory. The call is mandatory (it produces ErrorDocumentation) but may
be called empty, yielding documentation that shows no realistic message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Pack FirstClassErrors.Analyzers.dll into analyzers/dotnet/cs of the core NuGet
package via TargetsForTfmSpecificContentInPackage, so any project referencing the
FirstClassErrors package gets the FCExxx rules automatically. The ProjectReference
is ReferenceOutputAssembly=false + PrivateAssets=all: build-order only, no runtime
or dependency-graph impact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
… cover it in CI

Reference the analyzer from the sample project as OutputItemType=Analyzer so the
FCExxx rules run at build/IDE time on real usage code, and add a CI step that
builds the sample with the analyzers active. This fails on any Error-severity
finding and surfaces warnings in the log, keeping the sample exemplary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
DuplicateErrorCode is reported from a RegisterCompilationEndAction, so its
descriptor must carry WellKnownDiagnosticTags.CompilationEnd. Beyond silencing
RS1037, the tag tells the IDE this is a whole-compilation (cross-file) diagnostic
surfaced at build / full-solution analysis rather than live per file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Report when more than one documented factory produces the same error code by
referencing the same ErrorCode field. Documentation extraction groups by code
and keeps a single entry, so the others collapse silently. Complements FCE001,
which only sees duplicate ErrorCode.Create literals (a shared field has just one).

Aggregates per code field across the compilation (operation-block collect ->
compilation-end report) and carries the CompilationEnd tag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Report an example passed to WithExamples(...) that invokes no factory of the type
declaring the documentation. Examples exist to expose the documented error's real
messages, so each should build that error. Lambda and method-group examples are
inspected; unrecognized shapes are left alone to avoid false positives.

Extracts the operation-tree walk into a shared OperationFacts helper, reused by
FCE011.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Report ErrorCode.Create(x) where the argument is not a compile-time constant;
such a code is invisible to FCE001 duplicate detection. Opt-in (disabled by
default) for teams that want codes to stay literal.

Also teaches the test harness to force opt-in rules on for a test run, the way
an .editorconfig severity entry would.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Report a literal error code that does not follow the UPPER_SNAKE_CASE
convention. Convention check, opt-in (disabled by default). Empty codes stay
FCE002's concern and non-literal codes FCE003's.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Report a literal error code that is one of a small denylist of catch-all words
(ERROR, INVALID, FAILED, ...) which carry no diagnostic value. Opt-in (disabled
by default).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Report WithPublicMessage(short, detailed) where both literal messages are equal.
The short message is a public summary and the detailed one an optional public
detail, so identical values usually signal a copy-paste. Info, enabled by default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Report a WithTitle("...") whose literal title is one of a small denylist of
empty phrases (Error, Invalid value, Failure, ...). A good title names the
condition. Opt-in (disabled by default).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
claude added 7 commits July 4, 2026 18:07
The FCE015 edit replaced the class-closing brace without re-adding it, leaving
Descriptors unclosed (CS1513). Restores the brace.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
…2001)

FCE003/004/005/015 ship with isEnabledByDefault: false. The analyzer release
file must record their Severity as "Disabled" (the effective severity of a
disabled-by-default rule), not their nominal Info; otherwise RS2001 flags a
category/severity mismatch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
…nfig

Raise the RS* analyzer-development rule categories to warning for the analyzer
project so the command-line build (CI) flags the same issues an IDE does, instead
of leaving them for a human to notice. Localization rules are left at defaults.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Raising the analyzer-development (RS*) categories surfaced nothing in the
command-line build (the CI toolchain's Microsoft.CodeAnalysis.Analyzers does not
carry those rules at build severity), so it did not achieve the goal of mirroring
the IDE's checks — and, since editorconfig also applies in the IDE, it would only
risk adding noise there. The analyzer already follows the standard RS practices
(CompilationEnd tags, release tracking, concurrent execution, help links).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Add doc/analyzers/FCE001..FCE016.md (the helpLinkUri targets): each page gives
the rule's category, severity, default state, a noncompliant/compliant example,
details/limitations, and how to enable the opt-in rules. Add doc/analyzers/README.md
as the grouped index and link it from the main README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
… link

Mirror in doc/README.fr.md the analyzer section and reference link added to the
English README, keeping the bilingual READMEs in step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
Follow the repository's bilingual convention for the analyzer reference docs:
rename each rule page to FCExxx.en.md and add a French FCExxx.fr.md, with an
English (README.md) and French (README.fr.md) index. Point the rules' help links
at the .en.md pages and fix the French README links. Keeps EN and FR docs in step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVAic46o24c1XAfiKEoE6
@Reefact Reefact merged commit 858070a into main Jul 4, 2026
2 checks passed
@Reefact Reefact deleted the analyzers branch July 4, 2026 21:46
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.

2 participants