Comprehensive guidance for creating Roslyn analyzers and code fix providers in the Rewrite.RoslynRecipe project. Use when developing new analyzers, code fixes, or when asked about analyzer implementation patterns, testing requirements, or best practices for C# code analysis.
This skill provides comprehensive guidance for developing Roslyn analyzers and code fix providers in the OpenRewrite C# project.
src/Rewrite.RoslynRecipe/ projectsrc/Rewrite.RoslynRecipe/ projecttests/Rewrite.RoslynRecipe.Tests/src/Rewrite.RoslynRecipe/Helpers/[Category][Action]Analyzer.cs (e.g., NamingConventionAnalyzer.cs, ActionContextAccessorObsoleteAnalyzer.cs)[Category][Action]CodeFixProvider.cs (e.g., NamingConventionCodeFixProvider.cs)[AnalyzerName]Tests.cs and [CodeFixProviderName]Tests.csIActionContextAccessor), the analyzer name should drop the I from the prefix as that clashes with naming convention identying interfaces - an analyzer is not an interface. Instead, it should name it like ActionContextAccessorObsoleteAnalyzer.DiagnosticId starting with characters OR (meaning OpenRewrite developed recipes). The next set of characters shall identify the target framework or package that the diagnostic is targeting. For example for recipes that help identify / fix issues when migration .NET framework versions, it would include NETX. The X is the roman numeral of the version 10 of .NET implying that the recipes targets migration to version 10. Roman numerals are used here as to not clash with the last digits of diagnostic id which are sequentially imported. The last number are 4 digits incrementing number for that specific issue for the target version. Full example for analyzer targeting issues in .NET 10: ORNETX0001.When targeting specific APIs, semantic analysis must ALWAYS be performed to determine if the targeted node correctly corresponds to fully qualified semantic target. Use helper method Rewrite.RoslynRecipes.Helpers.SemanticAnalysisUtil.IsSymbolOneOf for this purpose (public static bool IsSymbolOneOf(this SyntaxNode node, SemanticModel semanticModel, params IEnumerable<string> symbolNames)). This method operates on strings that are mapped to symbol.GetDocumentationCommentId() and will have one of the corresponding prefixes (ex: T:Microsoft.AspNetCore.Mvc.Infrastructure.IActionContextAccessor)
| Prefix | Symbol kind | Roslyn symbol type |
| ------ | ----------------------------------------------- | ------------------ |
| N: | Namespace | INamespaceSymbol |
| T: | Type (class, struct, interface, enum, delegate) | INamedTypeSymbol |
| M: | Method (includes constructors, operators) | IMethodSymbol |
| P: | Property (includes indexers) | IPropertySymbol |
| F: | Field (includes enum members) | IFieldSymbol |
| E: | Event | IEventSymbol |
| A: | Assembly | IAssemblySymbol |
All editing of the syntax tree MUST be done using DocumentEditor, which provides a simplified and robust way to make multiple edits to the document and access to the original semantic model. Remember that semantic model is tied to the original syntax, so all decision need to be made on the original nodes, not any subsequent mutations.
If the codefix only targets the immediate code marked by the analyzer, semantic confirmation is not required since it's already guaranteed to have been done in the analyzer. However, if the refactoring requires nodes outside of what was selected by the diagnostic (other areas of the document), semantic model should always be used to ensure accurate targeting.
When introducing symbol usages from other namespaces that are not guaranteed to be in the document, they an annotation must be placed on the node to indicate semantic type it resolves to. Use SymbolAnnotation.Create method for this. Example
editor.ReplaceNode(memberAccess.Node, (current, gen) =>
SyntaxFactory.ParseExpression($"HttpContext?.GetRouteData()")
.SymbolAnnotation.Create("T:Microsoft.AspNetCore.Routing.RoutingHttpContextExtensions")
.FormatterAnnotated());
We need to ensure new code is properly formatted but not try to format existing user code. To do this, whenever doing any operation on the DocumentEditor (ex. replace/insert), call .FormatterAnnotated() on the new node. After all edits are done, format only edited blocks via call like this when obtaining the final document:
editor.ReplaceNode(oldNode, (current, gen) =>
SyntaxFactory.ParseExpression($"HttpContext?.GetRouteData()")
.FormatterAnnotated());
var changedDocument = await editor.GetChangedDocumentFormatted(cancellationToken);
When removing nodes from the syntax tree, these operations MUST be done after all other edits are complete (exception formatting). This is to ensure that any edits do not try to operate on nodes that have been subsequently removed.
When calling editor.ReplaceNode, use ONLY overload that uses a delegate for computeReplacement:
editor.ReplaceNode(memberAccess.Node, (current, gen) => MutateMyNode(current));
// or
editor.ReplaceNode(memberAccess.Node, (current, gen) => replacementNode);
var actionContextAccessorType = semanticModel.Compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Mvc.Infrastructure.IActionContextAccessor")
?? throw new InvalidOperationException($"Can't resolve type Microsoft.AspNetCore.Mvc.Infrastructure.IActionContextAccessor");
var httpContextAccessorType = semanticModel.Compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Http.IHttpContextAccessor")
?? throw new InvalidOperationException($"Can't resolve type Microsoft.AspNetCore.Http.IHttpContextAccessor");
editor.ReplaceType(semanticModel, actionContextAccessorType, httpContextAccessorType);
Accurately renames a symbol using semantic model
var nodeSymbol = semanticModel.GetDeclaredSymbol(someNode, cancellationToken);
editor.RenameSymbol(semanticModel, nodeSymbol, "NewName");
Ensures that symbol is of a particular semantic type. Ideally
var propertiesOfTypeActionDescriptor = containingType.DescendantNodes()
.OfType<PropertyDeclarationSyntax>()
.Where(x => x.IsSymbolOneOf(semanticModel, "P:Microsoft.AspNetCore.Mvc.ActionContext.ActionDescriptor"))
.ToList();
Every analyzer and code fix MUST have comprehensive tests including:
using Microsoft.CodeAnalysis.Testing;
using Rewrite.RoslynRecipe.Tests.Verifiers;
using TUnit.Core;
using Verifier = Rewrite.RoslynRecipe.Tests.Verifiers.CSharpAnalyzerVerifier<
Rewrite.RoslynRecipe.[Name]Analyzer>;
namespace Rewrite.RoslynRecipe.Tests
{
public class [Name]AnalyzerTests
{
/// <summary>
/// Tests that [specific scenario] triggers diagnostic.
/// </summary>
[Test]
public async Task [Scenario]_TriggersDiagnostic()
{
const string source = """
// Code with issue
{|ORNETX[Number]:ProblemCode|}
""";
await Verifier.VerifyAnalyzerAsync(source,
Assemblies.AspNet100 // Or appropriate reference assemblies
.AddPackage("PackageName", "Version"))
.ConfigureAwait(false);
}
/// <summary>
/// Tests that [valid scenario] does not trigger diagnostic.
/// </summary>
[Test]
public async Task [ValidScenario]_NoDiagnostic()
{
const string source = """
// Valid code
ValidCode
""";
await Verifier.VerifyAnalyzerAsync(source,
Assemblies.AspNet100)
.ConfigureAwait(false);
}
}
}
using Microsoft.CodeAnalysis.Testing;
using Rewrite.RoslynRecipe.Tests.Verifiers;
using TUnit.Core;
using Verifier = Rewrite.RoslynRecipe.Tests.Verifiers.CSharpCodeFixVerifier<
Rewrite.RoslynRecipe.[Name]Analyzer,
Rewrite.RoslynRecipe.[Name]CodeFixProvider>;
namespace Rewrite.RoslynRecipe.Tests
{
public class [Name]CodeFixTests
{
/// <summary>
/// [Tests that [specific transformation] is correctly applied.]
/// </summary>
[Test]
public async Task [Scenario]_CorrectlyFixed()
{
const string source = """
// Code before fix
{|ORNETX[Number]:ProblemCode|}
""";
const string fixedSource = """
// Code after fix
FixedCode
""";
await Verifier.VerifyCodeFixAsync(source, fixedSource,
Assemblies.AspNet100
.AddPackage("PackageName", "Version"))
.ConfigureAwait(false);
}
}
}
Assemblies.Net100.AddPackage("Microsoft.Extensions.Options") the version will be set to 10.0.0 automatically and should be omitted. However, if the package version is does not follow the same version train as the target framework, then explicit version should be specified like this: Assemblies.Net90.AddPackage("Microsoft.AspNetCore.HttpOverrides","2.3.9"). If you see an error in tests that a particular package is not available when using version-less overload, find the latest version of the package on nuget.org (ex: https://www.nuget.org/packages/microsoft.aspnetcore.httpoverrides) and use explicit version number when calling AddPackageNet90 reference assemblies.When refactoring delegates, test ALL variations:
x => x.Method()x => { return x.Method(); }obj.Methodasync x => await x.Method()x => Task.FromResult(x.Method())Keep test code concise:
// GOOD: Minimal, focused test
const string source = """
using Microsoft.AspNetCore.Builder;
var app = WebApplication.Create();
app.MapGet("/", () => "test")
.{|ORNETX0010:ProblemMethod|}();
""";
// BAD: Unnecessary complexity
const string source = """
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
namespace MyApp
{
public class Startup
{
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", () => "test")
.{|ORNETX0010:ProblemMethod|}();
});
}
}
}
""";
ALL public APIs MUST include complete XML documentation:
/// <summary>
/// Analyzes C# code for [specific issue] and reports diagnostics.
/// </summary>
/// <remarks>
/// This analyzer detects [detailed explanation of what it finds].
/// </remarks>
public class [Name]Analyzer : DiagnosticAnalyzer
{
/// <summary>
/// Registers analysis actions for [specific syntax kinds].
/// </summary>
/// <param name="context">The analysis context to register actions with. Must not be null.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="context"/> is null.</exception>
public override void Initialize(AnalysisContext context)
{
// Implementation
}
}
When creating analyzer / codefix from a URL article, link URL must appear in the xml comments
src/Rewrite.RoslynRecipe/symbol.GetDocumentationCommentId to determine symbol matchnpx skills add openrewrite/csharp-analyzer-developer下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer