dotnet-roslyn-analyzers
dotnet-roslyn-analyzers
Guidance for authoring custom Roslyn analyzers, code fix providers, code refactoring providers, and diagnostic suppressors. Covers project setup, DiagnosticDescriptor conventions, analysis context registration, code fix actions, code refactoring actions, multi-Roslyn-version targeting (3.8 through 4.14), testing with Microsoft.CodeAnalysis.Testing, NuGet packaging, and performance best practices.
For extended code examples (CodeRefactoringProvider implementation, multi-version project structure, test matrix configuration), see details.md in this skill directory.
Scope
- DiagnosticAnalyzer authoring and analysis context registration
- CodeFixProvider and CodeRefactoringProvider implementation
- Multi-Roslyn-version targeting (3.8 through 4.14)
- Testing with Microsoft.CodeAnalysis.Testing
- NuGet packaging for analyzer assemblies
Out of scope
- Consuming and configuring existing analyzers (CA rules, severity) -- see [skill:dotnet-add-analyzers]
- Authoring source generators (IIncrementalGenerator) -- see [skill:dotnet-csharp-source-generators]
- Naming conventions -- see [skill:dotnet-csharp-coding-standards]
Cross-references: [skill:dotnet-csharp-source-generators] for shared Roslyn packaging concepts and IIncrementalGenerator patterns, [skill:dotnet-add-analyzers] for consuming and configuring analyzers, [skill:dotnet-testing-strategy] for general test organization and framework selection, [skill:dotnet-csharp-coding-standards] for naming conventions applied to analyzer code.
Project Setup
Analyzer projects must target netstandard2.0. The compiler loads analyzers into various host processes (Visual Studio on .NET Framework/Mono, MSBuild on .NET Core, dotnet build CLI) -- targeting net8.0+ breaks compatibility with hosts that do not run on that runtime.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" PrivateAssets="all" />
</ItemGroup>
</Project>
EnforceExtendedAnalyzerRulesenables RS-series meta-diagnostics that catch common analyzer authoring mistakes.IsRoslynComponentenables IDE tooling support for the project.LangVersion>latestlets you write modern C# in the analyzer itself while still targetingnetstandard2.0.- All Roslyn SDK packages must use
PrivateAssets="all"to avoid shipping them as transitive dependencies.
DiagnosticAnalyzer
Every analyzer inherits from DiagnosticAnalyzer and must be decorated with [DiagnosticAnalyzer(LanguageNames.CSharp)].
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class NoPublicFieldsAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "MYLIB001";
private static readonly DiagnosticDescriptor Rule = new(
id: DiagnosticId,
title: "Public fields should be properties",
messageFormat: "Field '{0}' is public; use a property instead",
category: "Design",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: $"https://example.com/docs/rules/{DiagnosticId}");
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
= ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterSymbolAction(AnalyzeField, SymbolKind.Field);
}
private static void AnalyzeField(SymbolAnalysisContext context)
{
var field = (IFieldSymbol)context.Symbol;
if (field.DeclaredAccessibility == Accessibility.Public
&& !field.IsConst && !field.IsReadOnly)
{
context.ReportDiagnostic(Diagnostic.Create(Rule, field.Locations[0], field.Name));
}
}
}
Analysis Context Registration
| Method | Granularity | Use When |
|---|---|---|
RegisterSyntaxNodeAction |
Individual syntax nodes | Pattern matching on specific syntax |
RegisterSymbolAction |
Declared symbols | Checking symbol-level properties |
RegisterOperationAction |
IL-level operations | Analyzing semantic operations |
RegisterSyntaxTreeAction |
Entire syntax tree | File-level checks |
RegisterCompilationStartAction |
Compilation start | Accumulate state across compilation |
RegisterCompilationAction |
Full compilation | One-shot analysis after all files |
DiagnosticDescriptor Conventions
ID Prefix Patterns
| Pattern | Example | When |
|---|---|---|
PROJ### |
MYLIB001 |
Single-project analyzers |
AREA#### |
PERF0001 |
Category-scoped analyzers |
XX#### |
MA0042 |
Short-prefix convention |
Avoid prefixes reserved by the .NET platform: CA, CS, RS, IDE, IL, SYSLIB.
Severity Selection
| Severity | Use When |
|---|---|
Error |
Code will not work correctly at runtime |
Warning |
Code works but violates best practices |
Info |
Suggestion for improvement |
Hidden |
IDE-only refactoring suggestion |
Default to Warning for most rules. Always provide a non-null helpLinkUri (RS1015 enforces this).
CodeFixProvider
Code fix providers offer automated corrections for diagnostics. Key patterns:
- EquivalenceKey: Every
CodeActionmust have a uniqueequivalenceKeyfor FixAll support (RS1010, RS1011) - Document vs. Solution modification: Use
createChangedDocumentfor single-file fixes,createChangedSolutionfor cross-file renames - Trivia preservation: Always transfer leading/trailing trivia from replaced nodes
- FixAllProvider: Return
WellKnownFixAllProviders.BatchFixerfor batch-applicable fixes
See details.md for the complete CodeFixProvider implementation with property conversion.
DiagnosticSuppressor
Conditionally suppresses diagnostics from other analyzers when EditorConfig cannot express the suppression condition. Requires Roslyn 3.8+.
| Approach | Use When |
|---|---|
| EditorConfig severity override | Suppression applies unconditionally |
[SuppressMessage] attribute |
Suppression applies to a specific location |
DiagnosticSuppressor |
Suppression depends on code structure or patterns |
Multi-Roslyn-Version Targeting
Version Boundaries
| Roslyn Version | Ships With | Key APIs Added |
|---|---|---|
| 3.8 | VS 16.8 / .NET 5 SDK | DiagnosticSuppressor |
| 4.2 | VS 17.2 / .NET 6 SDK | Improved incremental analysis |
| 4.4 | VS 17.4 / .NET 7 SDK | ForAttributeWithMetadataName |
| 4.8 | VS 17.8 / .NET 8 U1 | CollectionExpression support |
| 4.14 | VS 17.14 / .NET 10 SDK | Latest API surface |
Use conditional compilation constants (ROSLYN_X_Y_OR_GREATER) and version-specific NuGet paths (analyzers/dotnet/roslyn{version}/cs/). See details.md for the complete multi-version project structure.
Testing Analyzers
Use Microsoft.CodeAnalysis.Testing for ergonomic analyzer testing:
using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<
NoPublicFieldsAnalyzer,
Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
public class NoPublicFieldsAnalyzerTests
{
[Fact]
public async Task PublicField_ReportsDiagnostic()
{
var test = """
public class MyClass
{
public int {|MYLIB001:Value|};
}
""";
await Verify.VerifyAnalyzerAsync(test);
}
}
Diagnostic Markup Syntax
| Markup | Meaning |
|---|---|
| `[ | text |
| `{ | DIAG_ID:text |
NuGet Packaging
Analyzers ship as NuGet packages with assemblies in analyzers/dotnet/cs/, not lib/.
<PropertyGroup>
<IncludeBuildOutput>false</IncludeBuildOutput>
<DevelopmentDependency>true</DevelopmentDependency>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
</PropertyGroup>
<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll"
Pack="true"
PackagePath="analyzers/dotnet/cs" />
</ItemGroup>
Performance Best Practices
- Resolve types once per compilation inside
RegisterCompilationStartAction, not per-node/symbol callbacks - Cache
SupportedDiagnosticsasImmutableArrayfield, not expression-bodied property - Enable concurrent execution -- always call
context.EnableConcurrentExecution() - Filter early -- register for the most specific
SyntaxKindpossible - Avoid
Compilation.GetSemanticModel()-- use theSemanticModelfrom the analysis context (RS1030)
Common Meta-Diagnostics (RS-Series)
| ID | Title | What It Catches |
|---|---|---|
| RS1001 | Missing DiagnosticAnalyzerAttribute |
Analyzer class missing attribute |
| RS1008 | Avoid storing per-compilation data | Instance fields with compilation data |
| RS1010 | Create code actions with unique EquivalenceKey |
Missing equivalence key |
| RS1015 | Provide non-null helpLinkUri |
Empty help link |
| RS1016 | Code fix providers should provide FixAll support | Missing GetFixAllProvider() |
| RS1024 | Symbols should be compared for equality | Using == instead of SymbolEqualityComparer |
| RS1026 | Enable concurrent execution | Missing EnableConcurrentExecution() |
| RS1030 | Do not invoke Compilation.GetSemanticModel() |
Using wrong semantic model source |
| RS1041 | Compiler extensions should target netstandard2.0 |
Wrong target framework |
References
More from novotnyllc/dotnet-artisan
dotnet-csharp
Baseline C# skill loaded for every .NET code path. Guides language patterns (records, pattern matching, primary constructors, C# 8-15), coding standards, async/await, DI, LINQ, serialization, domain modeling, concurrency, Roslyn analyzers, globalization, native interop (P/Invoke, LibraryImport, ComWrappers), WASM interop (JSImport/JSExport), and type design. Spans 25 topics. Do not use for ASP.NET endpoint architecture, UI framework patterns, or CI/CD guidance.
127dotnet-api
Builds ASP.NET Core APIs, EF Core data access, gRPC, SignalR, and backend services with middleware, security (OAuth, JWT, OWASP), resilience, messaging, OpenAPI, .NET Aspire, Semantic Kernel, HybridCache, YARP reverse proxy, output caching, Office documents (Excel, Word, PowerPoint), PDF, and architecture patterns. Spans 32 topic areas. Do not use for UI rendering patterns or CI/CD pipeline authoring.
90dotnet-testing
Defines .NET test strategy and implementation patterns across xUnit v3 (Facts, Theories, fixtures, IAsyncLifetime), integration testing (WebApplicationFactory, Testcontainers), Aspire testing (DistributedApplicationTestingBuilder), snapshot testing (Verify, scrubbing), Playwright E2E browser automation, BenchmarkDotNet microbenchmarks, code coverage (Coverlet), mutation testing (Stryker.NET), UI testing (page objects, selectors), and AOT WASM test compilation. Spans 13 topic areas. Do not use for production API architecture or CI workflow authoring.
86dotnet-advisor
Routes .NET/C# requests to the correct domain skill and loads coding standards as baseline for all code paths. Determines whether the task needs API, UI, testing, devops, tooling, or debugging guidance based on prompt analysis and project signals, then invokes skills in the right order. Always invoked after [skill:using-dotnet] detects .NET intent. Do not use for deep API, UI, testing, devops, tooling, or debugging implementation guidance.
60dotnet-debugging
Debugs Windows and Linux/macOS applications (native, .NET/CLR, mixed-mode) with WinDbg MCP (crash dumps, !analyze, !syncblk, !dlk, !runaway, !dumpheap, !gcroot, BSOD), dotnet-dump, lldb with SOS, createdump, and container diagnostics (Docker, Kubernetes). Hang/deadlock diagnosis, high CPU triage, memory leak investigation, kernel debugging, and dotnet-monitor for production. Spans 17 topic areas. Do not use for routine .NET SDK profiling, benchmark design, or CI test debugging.
57dotnet-devops
Configures .NET CI/CD pipelines (GitHub Actions with setup-dotnet, NuGet cache, reusable workflows; Azure DevOps with DotNetCoreCLI, templates, multi-stage), containerization (multi-stage Dockerfiles, Compose, rootless), packaging (NuGet authoring, source generators, MSIX signing), release management (NBGV, SemVer, changelogs, GitHub Releases), and observability (OpenTelemetry, health checks, structured logging, PII). Spans 18 topic areas. Do not use for application-layer API or UI implementation patterns.
52