C# > Source Generators > Using Roslyn for Code Generation > Analyzers and CodeFix Providers

Simple Analyzer and Code Fix Provider

This example demonstrates a basic analyzer that identifies methods named 'ObsoleteMethod' and provides a code fix to rename them to 'NewMethod'. It showcases the fundamental concepts of analyzers and code fix providers using Roslyn.

Analyzer Implementation

ObsoleteMethodAnalyzer extends DiagnosticAnalyzer. It registers a syntax node action to analyze method declarations. If a method named 'ObsoleteMethod' is found, a diagnostic is reported. The DiagnosticDescriptor defines the rule's ID, title, message format, category, severity, and description. The Initialize method configures the analyzer to operate on all code and enables concurrent execution.

// Analyzer.cs
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;

namespace RoslynAnalyzers
{
    [DiagnosticAnalyzer(LanguageNames.CSharp)]
    public class ObsoleteMethodAnalyzer : DiagnosticAnalyzer
    {
        public const string DiagnosticId = "ObsoleteMethodUsage";
        private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.AnalyzerTitle), Resources.ResourceManager, typeof(Resources));
        private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.AnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources));
        private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.AnalyzerDescription), Resources.ResourceManager, typeof(Resources));
        private const string Category = "Naming";

        private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }

        public override void Initialize(AnalysisContext context)
        {
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
            context.EnableConcurrentExecution();
            context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.MethodDeclaration);
        }

        private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
        {
            var methodDeclaration = (MethodDeclarationSyntax)context.Node;

            if (methodDeclaration.Identifier.Text == "ObsoleteMethod")
            {
                var diagnostic = Diagnostic.Create(Rule, methodDeclaration.GetLocation(), methodDeclaration.Identifier.Text);
                context.ReportDiagnostic(diagnostic);
            }
        }
    }
}

Code Fix Provider Implementation

ObsoleteMethodCodeFixProvider extends CodeFixProvider. It identifies the diagnostics it can fix through FixableDiagnosticIds. The RegisterCodeFixesAsync method is called when the analyzer reports a diagnostic. It finds the relevant MethodDeclarationSyntax and registers a code action to rename the method. The RenameMethodAsync method uses Roslyn's Renamer to rename the symbol (method) across the entire solution.

// CodeFixProvider.cs
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Rename;
using Microsoft.CodeAnalysis.Text;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;

namespace RoslynAnalyzers
{
    [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ObsoleteMethodCodeFixProvider)), Shared]
    public class ObsoleteMethodCodeFixProvider : CodeFixProvider
    {
        private const string title = "Rename to NewMethod";

        public sealed override ImmutableArray<string> FixableDiagnosticIds
        {
            get { return ImmutableArray.Create(ObsoleteMethodAnalyzer.DiagnosticId); }
        }

        public sealed override FixAllProvider GetFixAllProvider()
        {
            // See https://github.com/dotnet/roslyn/blob/main/docs/analyzers/FixAllProvider.md for more information on Fix All Providers
            return WellKnownFixAllProviders.BatchFixer;
        }

        public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

            var diagnostic = context.Diagnostics.First();
            var diagnosticSpan = diagnostic.Location.SourceSpan;

            var methodDeclaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<MethodDeclarationSyntax>().FirstOrDefault();

            if (methodDeclaration != null)
            {
                context.RegisterCodeFix(
                    CodeAction.Create(
                        title: title,
                        createChangedSolution: c => RenameMethodAsync(context.Document, methodDeclaration, c),
                        equivalenceKey: title),
                    diagnostic);
            }
        }

        private async Task<Solution> RenameMethodAsync(Document document, MethodDeclarationSyntax methodDeclaration, CancellationToken cancellationToken)
        {
            // Produce a new solution that has all references to that method renamed, including the declaration.
            var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
            var methodSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, cancellationToken);
            var solution = document.Project.Solution;
            return await Renamer.RenameSymbolAsync(solution, methodSymbol, "NewMethod", solution.Workspace.Options, cancellationToken).ConfigureAwait(false);
        }
    }
}

Concepts behind the snippet

This snippet showcases the core concepts of Roslyn analyzers and code fix providers. Analyzers identify code that violates certain rules or patterns. Code fix providers offer automated solutions to correct these violations, improving code quality and maintainability. Analyzers are triggered during compilation, and code fixes appear in the IDE as suggestions. This is a simple example that highlights the interaction between finding a problem (analyzer) and offering a solution (code fix).

Real-Life Use Case Section

Imagine a scenario where a company decides to deprecate a certain method or API. Using an analyzer, you can flag all usages of the deprecated method throughout the codebase. The code fix provider can then automatically replace those usages with the new, recommended method, minimizing manual effort and potential errors during refactoring.

Best Practices

  • Keep Analyzers Focused: Each analyzer should address a specific problem domain.
  • Write Unit Tests: Thoroughly test your analyzer and code fix provider to ensure they function correctly under various scenarios.
  • Handle Edge Cases: Consider different code structures and potential edge cases when implementing your analyzer logic.
  • Provide Clear Error Messages: The error messages provided by the analyzer should be informative and guide the developer to the correct solution.

Interview Tip

Be prepared to discuss the lifecycle of an analyzer and code fix provider, including how they are registered, triggered, and how they interact with the Roslyn compiler. Explain the difference between syntax analysis and semantic analysis, and how each is used in the context of analyzers.

When to use them

Use analyzers and code fix providers when you need to enforce coding standards, detect potential bugs, automate refactoring tasks, or provide guidance to developers within a team or organization. They are particularly useful for large codebases where manual code reviews are impractical.

Memory footprint

Analyzers and code fix providers can add a small overhead to the compilation process. It's important to keep them performant by optimizing their logic and avoiding unnecessary computations. Profiling your analyzers can help identify performance bottlenecks.

Alternatives

Alternatives to analyzers and code fix providers include manual code reviews, static analysis tools (e.g., SonarQube), and traditional compiler warnings. However, analyzers and code fix providers offer the advantage of being integrated directly into the IDE, providing real-time feedback during development.

Pros

  • Automated Code Quality: Enforce coding standards and detect potential issues automatically.
  • Improved Maintainability: Simplify refactoring tasks and reduce technical debt.
  • Real-time Feedback: Provide immediate feedback to developers in the IDE.
  • Customizable: Tailor rules and fixes to meet the specific needs of your project or organization.

Cons

  • Learning Curve: Requires understanding of the Roslyn API and compiler concepts.
  • Performance Overhead: Can add a small overhead to the compilation process.
  • Complexity: Implementing complex analyzers and code fix providers can be challenging.
  • Maintenance: Requires ongoing maintenance to ensure compatibility with new versions of the C# language and Roslyn compiler.

FAQ

  • What is Roslyn?

    Roslyn is the .NET Compiler Platform that provides open-source C# and Visual Basic compilers with rich code analysis APIs.
  • How do I install an analyzer?

    Analyzers are typically installed as NuGet packages in your C# project. Visual Studio will then automatically recognize and run the analyzer during compilation.
  • How do I debug an analyzer?

    You can debug an analyzer by attaching a debugger to the Visual Studio instance that is running the analyzer. Set breakpoints in your analyzer code and then compile a project that uses your analyzer. The debugger will break at the breakpoints.