agentleFS
glennawatsonglennawatson/CSharpAgentSkills/skills/dotnet-analyzers/SKILL.md

dotnet-analyzers

Skill2 starsChanged 42 days agoMITView on GitHub

Use when adding, configuring, or reviewing Roslyn analyzers in a .NET repo — StyleSharp/PerformanceSharp/SecuritySharp (or an equivalent first-party analyzer set), the built-in CAxxxx code-quality rules, IDExxxx code-style rules, and SYSLIBxxxx obsoletion/source-generator diagnostics. Covers wiring analyzers in, severity precedence in .editorconfig, fixing vs. suppressing, and picking the highest-value rules to turn on.

The file

We split this skill into the rules it gives an agent

3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Don't put root = true in the nested file — it must inherit, not replace, the repo-root config.

    line 140
  2. Distinctive:

    Every relaxation gets a comment saying *why* it's fine in tests, same discipline as any other suppression.

    line 141
  3. Very specific:

    If you're writing a first-party analyzer (see roslyn-analyzer-performance for making its hot paths cheap), its diagnostic message and docs …

    line 252

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

.NET analyzers: wiring, configuring, and fixing

An analyzer that isn't wired to fail the build is a linter nobody reads. Getting this right is three separate jobs: add the analyzer packages so they run, configure severities so the right things fail the build, and fix what they find instead of making them quiet. See dotnet-project-hygiene for the AnalysisLevel/TreatWarningsAsErrors basics this goes deeper on, and csharp-verification for why an analyzer warning is a failure, not noise.

Adding analyzers

Reference analyzer packages the same way across every project with central package management: version in Directory.Packages.props, the actual PackageReference once in Directory.Build.props so every project that imports it gets the analyzer, PrivateAssets="all" so the analyzer doesn't flow to your own package's consumers (it's a build-time tool, not a runtime dependency):

<!-- Directory.Packages.props -->
<PropertyGroup>
  <!-- StyleSharp.Analyzers, PerformanceSharp.Analyzers and SecuritySharp.Analyzers ship from the
       same release pipeline and always share a version. -->
  <RoslynCommonAnalyzersVersion>5.1.2</RoslynCommonAnalyzersVersion>
</PropertyGroup>
<ItemGroup>
  <PackageVersion Include="StyleSharp.Analyzers" Version="$(RoslynCommonAnalyzersVersion)" />
  <PackageVersion Include="PerformanceSharp.Analyzers" Version="$(RoslynCommonAnalyzersVersion)" />
  <PackageVersion Include="SecuritySharp.Analyzers" Version="$(RoslynCommonAnalyzersVersion)" />
</ItemGroup>

<!-- Directory.Build.props -->
<ItemGroup>
  <PackageReference Include="StyleSharp.Analyzers" PrivateAssets="all"/>
  <PackageReference Include="PerformanceSharp.Analyzers" PrivateAssets="all"/>
  <PackageReference Include="SecuritySharp.Analyzers" PrivateAssets="all"/>
  <PackageReference Include="Roslynator.Analyzers" PrivateAssets="All"/>
  <PackageReference Include="SonarAnalyzer.CSharp" PrivateAssets="all"/>
</ItemGroup>

A plain PackageReference (version omitted) inside Directory.Build.props is enough under CPM — you don't need GlobalPackageReference here; that item exists for packages every project needs even when a project doesn't otherwise reference anything (see dotnet-central-package-management). Both forms need PrivateAssets="all" explicitly; CPM does not imply it.

Turn on the rest of the pipeline in the same Directory.Build.props:

<PropertyGroup>
  <AnalysisLevel>latest</AnalysisLevel>
  <AnalysisMode>All</AnalysisMode>            <!-- or AllEnabledByDefault, Recommended -->
  <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
  <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

With AnalysisMode=All and EnforceCodeStyleInBuild=true, both a CA rule (CA1305) and an IDE rule (IDE0008) fire on a plain dotnet build with no .editorconfig entry for either — AnalysisMode is the fallback bucket for anything the file doesn't mention.

Adopting the RoslynCommonAnalyzers presets

StyleSharp/PerformanceSharp/SecuritySharp ship ready-to-use presets at their repo root — recommended.editorconfig, recommended-performancesharp.editorconfig, recommended-securitysharp.editorconfig — each listing every rule at its default severity, with opt-in rules commented out. Per the packages' own docs/CONFIGURATION.md: copy one in, or merge its [*.cs] block into your existing .editorconfig, then tune. They configure entirely through .editorconfig — no separate JSON file — using the same AnalyzerConfigOptionsProvider mechanism the .NET SDK's own CA analyzers use, with a per-package option prefix (stylesharp., performancesharp., securitysharp.) mirroring CA's dotnet_code_quality. prefix. Rule-specific keys (stylesharp.SST1315.union_member_naming) win over the general one (stylesharp.union_member_naming).

Configuring: severity and precedence

Severity lives in .editorconfig, per rule:

[*.cs]
dotnet_diagnostic.CA1062.severity = warning
dotnet_diagnostic.IDE0060.severity = suggestion

Category-level fallback (dotnet_analyzer_diagnostic.category-<Category>.severity) sets a default for every rule in that category that isn't individually overridden — useful for "everything in Naming is a suggestion by default" without writing 40 lines. A per-rule dotnet_diagnostic.<ID>.severity always wins over it — but see the precedence caveat below before relying on it: an explicit AnalysisMode changes whether it does anything at all.

Precedence:

  1. dotnet_diagnostic.<ID>.severity = none/NoWarn — the rule never fires, full stop. <NoWarn>$(NoWarn);CA1822</NoWarn> plus TreatWarningsAsErrors=true builds clean on a file that triggers CA1822 — NoWarn wins over warnings-as-errors, it isn't a race.
  2. dotnet_diagnostic.<ID>.severity = <level> in .editorconfig sets that rule's severity regardless of AnalysisMode. Setting CA1822 to suggestion in .editorconfig removes it from the build-warning list even under AnalysisMode=Recommended.
  3. AnalysisMode/AnalysisLevel, when AnalysisMode is set explicitly (Recommended, All, …) — this computes a per-rule severity that outranks category-level fallback. With AnalysisMode=All set, dotnet_analyzer_diagnostic.category-Performance.severity = error has no effect on CA1822 (stays warning) — an explicit AnalysisMode is not just a fallback bucket, it wins over category-level severity for rules it computes a severity for.
  4. dotnet_analyzer_diagnostic.category-<Category>.severity — only actually takes effect as documented (a default for every rule in that category with no explicit dotnet_diagnostic.<ID> entry) when AnalysisMode is left unset at its SDK default. Removing AnalysisMode entirely from the same project makes the identical dotnet_analyzer_diagnostic.category-Performance.severity = error promote CA1822 to error. If your Directory.Build.props sets AnalysisMode explicitly — which this skill recommends — category-level severity is not a reliable lever for CA rules; use per-rule dotnet_diagnostic.<ID>.severity instead.
  5. TreatWarningsAsErrors / WarningsAsErrors — promotes whatever severity survived steps 1–4 from warning to error. It cannot resurrect a rule steps 1–2 silenced.

So: NoWarn and .editorconfig per-rule severity are suppressions/overrides that happen before warnings-as-errors is even consulted, and both outrank an explicit AnalysisMode. Dropping a rule's severity to none and adding it to NoWarn do the same thing through different doors — both need the same justification as a #pragma, not a reflex edit. Don't reach for category-level severity as your primary lever in a repo that sets AnalysisMode explicitly; it silently no-ops for rules AnalysisMode already assigns a severity to.

Test-directory relaxations

A nested .editorconfig under src/tests/ narrows scope for rules that don't fit test code — CA1812 (uninstantiated internal class — that's every test fixture), CA2213/CA1063 (dispose patterns that don't matter when the process exits after the run), or a project-specific analyzer's hot-path rule:

# src/tests/.editorconfig — inherits the repo-root .editorconfig; only lists test-specific overrides
[*.cs]
dotnet_diagnostic.CA1812.severity = none # Avoid uninstantiated internal classes
dotnet_diagnostic.CA2213.severity = none # Disposable fields should be disposed

performancesharp.avoid_linq_on_hot_path = false
dotnet_diagnostic.PSH1100.severity = none # Tests can use LINQ for clarity

Don't put root = true in the nested file — it must inherit, not replace, the repo-root config. Every relaxation gets a comment saying why it's fine in tests, same discipline as any other suppression.

Grouping convention in a large .editorconfig

Real .editorconfig files sort CA rules by numeric ID within a category comment block, and every none/non-default severity carries an inline reason — either a real false-positive-for-us reason, or "this rule is redundant with another one this repo treats as canonical":

###################
# CA10xx - Design
###################
dotnet_diagnostic.CA1000.severity = none # Do not declare static members on generic types — common factory pattern
dotnet_diagnostic.CA1001.severity = error # Types that own disposable fields should be disposable
dotnet_diagnostic.CA1051.severity = none # Duplicate of SST1401 (canonical) — do not declare visible instance fields
dotnet_diagnostic.CA1822.severity = none # Mark members as static — covered by PSH1414
dotnet_diagnostic.CA1860.severity = none # Avoid using 'Enumerable.Any()' extension method — covered by PSH1103

That "covered by PSH#### (canonical)" pattern is deliberate: when a first-party analyzer and a CA/Roslynator/SonarAnalyzer rule overlap, pick one as canonical (usually the one with the better fixer or the more specific message) and silence the other with a comment naming its replacement — never leave both live reporting the same defect twice.

SYSLIB rules get their own grouped block, same pattern:

# Microsoft .NET Runtime Obsoletions (SYSLIB0xxx)
dotnet_diagnostic.SYSLIB0011.severity = error # BinaryFormatter serialization is obsolete
...
# GeneratedRegex source generator
dotnet_diagnostic.SYSLIB1045.severity = error # Convert to 'GeneratedRegexAttribute' for compile-time regex generation
# LibraryImport (P/Invoke) source generator
dotnet_diagnostic.SYSLIB1054.severity = error # Use 'LibraryImportAttribute' instead of 'DllImportAttribute' ...

Fixing, not suppressing

Fix the cause. A suppression is for a genuine false positive, scoped as narrowly as possible, with a comment saying why:

// The connection string's empty password is the fixture the secret-detection rule is measured
// against, not a real credential.
#pragma warning disable SES1203
private const string TestConnectionString = "Server=.;Password=;";
#pragma warning restore SES1203

[SuppressMessage] for a suppression that belongs to a whole member rather than a few lines, with the same non-negotiable Justification. Never drop a rule's severity in .editorconfig just to quiet one call site — that silences it everywhere, not just where you looked.

Apply a fixer to exactly the files you touched, one rule at a time, instead of reformatting the tree:

dotnet format analyzers --include src/Orders/OrderService.cs --diagnostics SYSLIB1045
dotnet format style --include src/Orders/OrderService.cs --diagnostics IDE0008

See csharp-verification for the full gate (dotnet build clean, dotnet format --verify-no-changes clean, suppressions never used to force green).

High-value rules worth turning on

These come from .editorconfig blocks actually enabled at error/warning across multiple first-party repos surveyed for this skill (reactiveui, splat, Akavache, refit, Fusillade, Primitives):

  • CA1848 (use LoggerMessage delegates) — error everywhere surveyed. See csharp-logging-diagnostics for the [LoggerMessage] pattern it wants instead of ILogger.LogInformation(...).
  • CA1873 (avoid expensive Debug.Assert/logging argument evaluation) — guard the argument with IsEnabled rather than computing it unconditionally; see csharp-logging-diagnostics.
  • CA2007 (ConfigureAwait(false)) — consistently disabled in these repos with the same reasoning each time: "Rx and library callers drive synchronization context themselves." Don't cargo-cult this rule on; decide it for your own library's actual callers.
  • CA1305/CA1307/CA1309/CA1310 (culture-sensitive string operations, culture-sensitive comparison) — real bugs in anything building cache keys, URLs, or persisted data from ToString()/ToLower() without an explicit CultureInfo/StringComparison. Write instead: value.ToString(CultureInfo.InvariantCulture), string.Equals(a, b, StringComparison.Ordinal).
  • CA1822 (mark members static) — frequently disabled in favor of an equivalent first-party rule (PSH1414 in the surveyed repos) rather than left off; check whether your analyzer set has a canonical version before just enabling CA's.
  • CA1851 (possible multiple enumeration of IEnumerable) — real perf/correctness bug when a lazy sequence is enumerated twice (e.g., once for Count(), once for a foreach) and the source isn't idempotent. Materialize once: var items = source.ToList();.
  • CA1859/CA1860/CA1861/CA1865–CA1867 — cheaper API selection (CA1859: use the concrete type instead of the interface when nothing needs the abstraction; CA1860: .Count == 0 over .Any() on a collection; CA1861: hoist a constant array literal out of a hot call site; CA1865–CA1867: the char overloads of StartsWith/EndsWith/IndexOf). The char overloads don't exist on netstandard2.0/.NET Framework — repos multi-targeting those disable CA1865–CA1867 for exactly that reason; don't blanket-enable them without checking your TFMs.
  • CA2263 (prefer the generic overload when the type is known at the call site) — replaces GetService(typeof(T))-shaped calls with GetService<T>().
  • SYSLIB0xxx (runtime obsoletions — BinaryFormatter, CAS, Thread.Abort, weak crypto defaults, …) — error, not warning, in every repo surveyed. These mark APIs the runtime no longer supports correctly; there is no "later" for these, only "someone hits it in production."
  • SYSLIB1045 ([GeneratedRegex] instead of new Regex(...)/Regex.IsMatch with a constant pattern) and SYSLIB1054 ([LibraryImport] instead of [DllImport]) — both source-generator suggestions, both error in the surveyed repos. See csharp-aot-trimming for why these matter beyond style: the generated code is trim/AOT-safe where reflection-based Regex/P/Invoke marshalling is not.
  • SYSLIB1030 (JsonSourceGenerator did not generate metadata for a type reachable from a [JsonSerializable] root) — a real gap in a JsonSerializerContext, not noise; see csharp-json-source-generation.

Writing your own analyzer: describe the defect, not another tool's rule ID

If you're writing a first-party analyzer (see roslyn-analyzer-performance for making its hot paths cheap), its diagnostic message and docs describe the defect in its own terms — "this sequence is enumerated twice" — not "see CA1851" or "duplicate of SonarAnalyzer S1234". A consumer without that other tool installed still needs the message to make sense, and the .editorconfig comment convention above (# covered by PSH1103) is where cross-tool relationships belong — in the consuming repo's config, not in the rule's own shipped text.

Checklist

  • Analyzer packages referenced once (Directory.Packages.props version + Directory.Build.props PackageReference PrivateAssets="all"), not per-project
  • AnalysisLevel=latest, AnalysisMode picked deliberately (Recommended vs All), EnforceCodeStyleInBuild=true, TreatWarningsAsErrors=true
  • First-party preset (recommended*.editorconfig) adopted or merged, not reinvented rule by rule
  • Every non-default severity in .editorconfig has an inline reason, especially a "canonical rule" cross-reference when two analyzers cover the same defect
  • Test-directory .editorconfig relaxations are scoped to the test tree, inherit (no root = true), and each line says why
  • No repo-wide NoWarn used to silence a rule that should be fixed; suppressions are scoped and justified
  • SYSLIB0xxx obsoletions and SYSLIB1045/1054/1030 source-gen suggestions are enabled, not left at their (often lower) shipped default
  • dotnet format analyzers --include <file> --diagnostics <ID> used to apply one fixer to exactly the changed files, not a bare dotnet format
  • Verified with a real build, per csharp-verification — not assumed from reading the rule

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.