All skills
wshaddix avatar

/dotnet-roslyn-analyzers

@e4caaf0

Authoring Roslyn analyzers. DiagnosticAnalyzer, CodeFixProvider, CodeRefactoring, multi-version.

Use this Skill: https://skilld.dev/gh/wshaddix/dotnet-skills/dotnet-roslyn-analyzers

This session only. Nothing lands on disk.

details.md

≈3k tokens on demand. Your agent reads this file only when SKILL.md points to it.

dotnet-roslyn-analyzers -- Detailed Examples

Extended code examples for CodeRefactoringProvider authoring, multi-Roslyn-version targeting, and multi-version test matrix configuration.


CodeRefactoringProvider: Full Extract Interface Example

A complete CodeRefactoringProvider that extracts an interface from a class. The provider is offered when the cursor is on a class identifier and the class has at least one public method.

using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = nameof(ExtractInterfaceRefactoring))]
[Shared]
public sealed class ExtractInterfaceRefactoring : CodeRefactoringProvider
{
    public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
    {
        var root = await context.Document
            .GetSyntaxRootAsync(context.CancellationToken)
            .ConfigureAwait(false);

        var node = root?.FindNode(context.Span);
        var classDecl = node?.FirstAncestorOrSelf<ClassDeclarationSyntax>();

        if (classDecl is null)
            return;

        // Only offer when cursor is on the class identifier
        if (!classDecl.Identifier.Span.IntersectsWith(context.Span))
            return;

        var publicMethods = classDecl.Members
            .OfType<MethodDeclarationSyntax>()
            .Where(m => m.Modifiers.Any(SyntaxKind.PublicKeyword))
            .ToList();

        if (publicMethods.Count == 0)
            return;

        context.RegisterRefactoring(
            CodeAction.Create(
                title: $"Extract interface I{classDecl.Identifier.Text}",
                createChangedSolution: ct =>
                    ExtractInterfaceAsync(context.Document, classDecl, publicMethods, ct),
                equivalenceKey: "ExtractInterface"));
    }

    private static async Task<Solution> ExtractInterfaceAsync(
        Document document,
        ClassDeclarationSyntax classDecl,
        List<MethodDeclarationSyntax> methods,
        CancellationToken cancellationToken)
    {
        // Build interface members from public method signatures
        var interfaceMembers = methods.Select(m =>
            SyntaxFactory.MethodDeclaration(m.ReturnType, m.Identifier)
                .WithParameterList(m.ParameterList)
                .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
                .WithLeadingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed)
            ).Cast<MemberDeclarationSyntax>();

        var interfaceName = $"I{classDecl.Identifier.Text}";
        var interfaceDecl = SyntaxFactory.InterfaceDeclaration(interfaceName)
            .WithModifiers(SyntaxFactory.TokenList(
                SyntaxFactory.Token(SyntaxKind.PublicKeyword)))
            .WithMembers(SyntaxFactory.List(interfaceMembers));

        // Add interface to the same document after the class
        var root = await document.GetSyntaxRootAsync(cancellationToken)
            .ConfigureAwait(false);
        var newRoot = root!.InsertNodesAfter(classDecl, new[] { interfaceDecl });

        // Add base type to the class
        var updatedClass = newRoot.DescendantNodes()
            .OfType<ClassDeclarationSyntax>()
            .First(c => c.Identifier.Text == classDecl.Identifier.Text);

        var baseList = updatedClass.BaseList ?? SyntaxFactory.BaseList();
        var newBaseList = baseList.AddTypes(
            SyntaxFactory.SimpleBaseType(SyntaxFactory.ParseTypeName(interfaceName)));

        newRoot = newRoot.ReplaceNode(updatedClass,
            updatedClass.WithBaseList(newBaseList));

        return document.WithSyntaxRoot(newRoot).Project.Solution;
    }
}

CodeRefactoringProvider Testing

Use CSharpCodeRefactoringVerifier<T> to test refactoring providers. Use the framework-agnostic package with DefaultVerifier (the framework-specific .XUnit suffix packages are obsolete):

<PropertyGroup>
  <!-- Enable Microsoft.Testing.Platform v2 runner -->
  <UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
</PropertyGroup>

<ItemGroup>
  <!-- NuGet: Microsoft.CodeAnalysis.CSharp.CodeRefactoring.Testing (framework-agnostic) -->
  <PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeRefactoring.Testing" Version="1.1.3" />
  <!-- NuGet: xunit.v3 (xUnit v3 test framework) -->
  <PackageReference Include="xunit.v3" Version="3.2.2" />
</ItemGroup>
using Microsoft.CodeAnalysis.Testing;
using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeRefactoringVerifier<
    ExtractInterfaceRefactoring,
    Microsoft.CodeAnalysis.Testing.DefaultVerifier>;

public class ExtractInterfaceRefactoringTests
{
    [Fact]
    public async Task ClassWithPublicMethods_OffersRefactoring()
    {
        var test = new Verify.Test
        {
            TestCode = """
                public class [|MyService|]
                {
                    public void DoWork() { }
                    public int Calculate(int x) => x * 2;
                    private void InternalHelper() { }
                }
                """,
            FixedCode = """
                public class MyService : IMyService
                {
                    public void DoWork() { }
                    public int Calculate(int x) => x * 2;
                    private void InternalHelper() { }
                }
                public interface IMyService
                {
                    void DoWork();
                    int Calculate(int x);
                }
                """
        };

        await test.RunAsync();
    }

    [Fact]
    public async Task ClassWithNoPublicMethods_NoRefactoring()
    {
        var test = """
            public class [|MyService|]
            {
                private void InternalHelper() { }
            }
            """;

        // Verify no refactoring is offered (no expected FixedCode)
        var verifyTest = new Verify.Test
        {
            TestCode = test,
            FixedCode = test
        };

        await verifyTest.RunAsync();
    }
}

The markup [|text|] indicates where the cursor/selection triggers the refactoring. The verifier checks that the refactoring is offered and produces the expected FixedCode.


Multi-Roslyn-Version Project Structure

A complete multi-version analyzer solution structure following the Meziantou.Analyzer pattern:

MyAnalyzers/
  Directory.Build.props          # Shared properties
  Directory.Build.targets        # Conditional compilation constants
  src/
    MyAnalyzers/
      MyAnalyzers.csproj         # Analyzer project
      MyAnalyzer.cs
    MyAnalyzers.CodeFixes/
      MyAnalyzers.CodeFixes.csproj
      MyCodeFix.cs
  test/
    MyAnalyzers.Tests/
      MyAnalyzers.Tests.csproj   # Test project
      MyAnalyzerTests.cs
  pack/
    MyAnalyzers.Package/
      MyAnalyzers.Package.csproj # Packaging project

Directory.Build.props

<Project>
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>latest</LangVersion>
    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
    <IsRoslynComponent>true</IsRoslynComponent>
    <RoslynVersion Condition="'$(RoslynVersion)' == ''">3.8</RoslynVersion>
  </PropertyGroup>
</Project>

Directory.Build.targets

<Project>
  <!-- Roslyn version conditional compilation constants -->
  <PropertyGroup Condition="'$(RoslynVersion)' >= '3.8'">
    <DefineConstants>$(DefineConstants);ROSLYN_3_8;ROSLYN_3_8_OR_GREATER</DefineConstants>
  </PropertyGroup>
  <PropertyGroup Condition="'$(RoslynVersion)' >= '4.2'">
    <DefineConstants>$(DefineConstants);ROSLYN_4_2;ROSLYN_4_2_OR_GREATER</DefineConstants>
  </PropertyGroup>
  <PropertyGroup Condition="'$(RoslynVersion)' >= '4.4'">
    <DefineConstants>$(DefineConstants);ROSLYN_4_4;ROSLYN_4_4_OR_GREATER</DefineConstants>
  </PropertyGroup>
  <PropertyGroup Condition="'$(RoslynVersion)' >= '4.6'">
    <DefineConstants>$(DefineConstants);ROSLYN_4_6;ROSLYN_4_6_OR_GREATER</DefineConstants>
  </PropertyGroup>
  <PropertyGroup Condition="'$(RoslynVersion)' >= '4.8'">
    <DefineConstants>$(DefineConstants);ROSLYN_4_8;ROSLYN_4_8_OR_GREATER</DefineConstants>
  </PropertyGroup>
  <PropertyGroup Condition="'$(RoslynVersion)' >= '4.14'">
    <DefineConstants>$(DefineConstants);ROSLYN_4_14;ROSLYN_4_14_OR_GREATER</DefineConstants>
  </PropertyGroup>
</Project>

CI Multi-Version Test Matrix (GitHub Actions)

Uses xUnit v3 with Microsoft.Testing.Platform v2 (MTP2). Set UseMicrosoftTestingPlatformRunner in the test project (see CodeRefactoringProvider Testing above) and parameterize $(RoslynVersion):

jobs:
  test:
    strategy:
      matrix:
        roslyn-version: ['3.8', '4.2', '4.4', '4.6', '4.8']
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0.x'
      - name: Test with Roslyn ${{ matrix.roslyn-version }}
        run: >
          dotnet test
          -p:RoslynVersion=${{ matrix.roslyn-version }}
          --logger "trx;LogFileName=results-${{ matrix.roslyn-version }}.trx"

Packaging Project (.csproj)

The packaging project references each version-specific build output and places them in the correct NuGet paths:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <IncludeBuildOutput>false</IncludeBuildOutput>
    <DevelopmentDependency>true</DevelopmentDependency>
    <SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
    <PackageId>MyAnalyzers</PackageId>
  </PropertyGroup>

  <ItemGroup>
    <!-- Fallback: lowest supported Roslyn version -->
    <None Include="..\build\roslyn3.8\MyAnalyzers.dll"
          Pack="true" PackagePath="analyzers/dotnet/cs" />
    <!-- Version-specific overrides -->
    <None Include="..\build\roslyn3.8\MyAnalyzers.dll"
          Pack="true" PackagePath="analyzers/dotnet/roslyn3.8/cs" />
    <None Include="..\build\roslyn4.2\MyAnalyzers.dll"
          Pack="true" PackagePath="analyzers/dotnet/roslyn4.2/cs" />
    <None Include="..\build\roslyn4.4\MyAnalyzers.dll"
          Pack="true" PackagePath="analyzers/dotnet/roslyn4.4/cs" />
  </ItemGroup>

  <ItemGroup>
    <None Include="_._" Pack="true" PackagePath="lib/netstandard2.0" />
  </ItemGroup>
</Project>

Version-Gated API Usage

public sealed class MyAdvancedAnalyzer : DiagnosticAnalyzer
{
    public override void Initialize(AnalysisContext context)
    {
        context.EnableConcurrentExecution();
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

        // Base registration works on all versions
        context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Method);

#if ROSLYN_3_8_OR_GREATER
        // DiagnosticSuppressor support (Roslyn 3.8+)
        // Register compilation-level suppression analysis
        context.RegisterCompilationStartAction(compilationCtx =>
        {
            compilationCtx.RegisterSymbolAction(
                AnalyzeForSuppression, SymbolKind.NamedType);
        });
#endif

#if ROSLYN_4_8_OR_GREATER
        // CollectionExpression operation kind available in Roslyn 4.8+
        context.RegisterOperationAction(AnalyzeCollectionExpression,
            OperationKind.CollectionExpression);
#endif
    }

    // ... analysis methods
}

Pack Verification for Multi-Version Packages

dotnet pack -c Release
# Verify version-specific paths exist
unzip -l ./bin/Release/MyAnalyzers.1.0.0.nupkg | grep 'analyzers/'
# Expected output:
#   analyzers/dotnet/cs/MyAnalyzers.dll
#   analyzers/dotnet/roslyn3.8/cs/MyAnalyzers.dll
#   analyzers/dotnet/roslyn4.2/cs/MyAnalyzers.dll
#   analyzers/dotnet/roslyn4.4/cs/MyAnalyzers.dll

Source: SKILL.md on GitHub

No alerts14d4 checks · Risk SAFE
  • Gen Agent Trust Hub14d

    This skill provides templates and technical guidance for developing .NET Roslyn analyzers and code fixes. It follows official Microsoft best practices, including the use of meta-analyzers for quality and safety. No security risks were identified.

  • Socket14d

    No alerts

  • Snyk14d

    Risk: LOW · No issues

  • Runlayer6mo

    1/2 files flagged

Signed by skilld at e4caaf0. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Dormantupdated 7 months ago

README badge

README badge for wshaddix/dotnet-skills/dotnet-roslyn-analyzers