Location>code7788 >text

Adding header comments to .cs source code with `Roslyn` analyzer and fixer

Popularity:954 ℃/2024-09-07 16:32:34

I've written about it before.Two use cases for generating source code with the Roslyn source generatorCode Fixer for Roslyn today.CodeFixProviderImplementing a cs file header comment function.

The code fixer will involve bothCodeFixProvidercap (a poem)DiagnosticAnalyzer,

Implementing FileHeaderAnalyzer

First of all, we know that the prerequisite for the fixer is the analyzer, for example, here, if you want to add header comments to the code, then the analyzer must give the corresponding analysis of the reminder.

We start by implementing an implementation calledFileHeaderAnalyzerThe analyzer.

[DiagnosticAnalyzer()]
public class FileHeaderAnalyzer : DiagnosticAnalyzer
{
    public const string DiagnosticId = "GEN050";
    private static readonly LocalizableString Title = "File missing header information";
    private static readonly LocalizableString MessageFormat = "File missing header information";
    private static readonly LocalizableString Description = "Each file should contain header information.";
    private const string Category = "Document";

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

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule];

    public override void Initialize(AnalysisContext context)
    {
        if (context is null)
            return;

        ();
        ();
        (AnalyzeSyntaxTree);
    }

    private static void AnalyzeSyntaxTree(SyntaxTreeAnalysisContext context)
    {
        var root = ();
        var firstToken = ();

        // Check if the file starts with a comment
        var hasHeaderComment = (trivia => () || ());

        if (!hasHeaderComment)
        {
            var diagnostic = (Rule, (, (0, 0)));
            (diagnostic);
        }
    }
}

FileHeaderAnalyzer analyzer principle is very simple, you need to overload a few methods, focusing on theInitializemethod, where theRegisterSyntaxTreeActioni.e., the core code.SyntaxTreeAnalysisContextobject fetches the current source code of theSyntaxNoderoot node, and then determine the first of TA'sSyntaxTokenWhether or not it is a comment line (|)

If it's not a comment line, then notify the parser!

Having implemented the above code let's look at the effect.

image

and the analyzer will display a list of warnings in the error panel when compiling:.

image

Implementing CodeFixProvider

The analyzer is done, and now we'll implement the program calledAddFileHeaderCodeFixProviderThe restorers, the

/// <summary>
/// Automatically add header comments to documents
/// </summary>
[ExportCodeFixProvider(, Name = nameof(AddFileHeaderCodeFixProvider))]
[Shared]
public class AddFileHeaderCodeFixProvider : CodeFixProvider
{
    private const string Title = "Add file header information";
    //Name of the convention template file
    private const string ConfigFileName = "";
    private const string VarPrefix = "$";//prefix of a variable
    //Default comment text if the template does not exist
    private const string DefaultComment = """
        // Licensed to the {Product} under one or more agreements.
        // The {Product} licenses this file to you under the MIT license.
        // See the LICENSE file in the project root for more information.
        """;

    #region regex

    private const RegexOptions ROptions =  | ;
    private static readonly Regex VersionRegex = new(@"<Version>(.*?)</Version>", ROptions);
    private static readonly Regex CopyrightRegex = new(@"<Copyright>(.*?)</Copyright>", ROptions);
    private static readonly Regex CompanyRegex = new(@"<Company>(.*?)</Company>", ROptions);
    private static readonly Regex DescriptionRegex = new(@"<Description>(.*?)</Description>", ROptions);
    private static readonly Regex AuthorsRegex = new(@"<Authors>(.*?)</Authors>", ROptions);
    private static readonly Regex ProductRegex = new(@"<Product>(.*?)</Product>", ROptions);
    private static readonly Regex TargetFrameworkRegex = new(@"<TargetFramework>(.*?)</TargetFramework>", ROptions);
    private static readonly Regex TargetFrameworksRegex = new(@"<TargetFrameworks>(.*?)</TargetFrameworks>", ROptions);
    private static readonly Regex ImportRegex = new(@"<Import Project=""(.*?)""", ROptions);

    #endregion

    public sealed override ImmutableArray<string> FixableDiagnosticIds
    {
        //rewriteFixableDiagnosticIds,Return to Analyzer ReportId,Indicates that the current repairer is capable of repairing the correspondingId
        get { return []; }
    }

    public sealed override FixAllProvider GetFixAllProvider()
    {
        return ;
    }

    public override Task RegisterCodeFixesAsync(CodeFixContext context)
    {
        var diagnostic = [0];
        var diagnosticSpan = ;

        (
            (
                title: Title,
                createChangedDocument: c => FixDocumentAsync(, diagnosticSpan, c),
                equivalenceKey: Title),
            diagnostic);

        return ;
    }


    private static async Task<Document> FixDocumentAsync(Document document, TextSpan span, CancellationToken ct)
    {
        var root = await (ct).ConfigureAwait(false);

        //Getting file header information from the project configuration
        var projFilePath =  ?? "C:\\";//No file path for unit test,So use the default path

        var projectDirectory = (projFilePath);
        var configFilePath = (projectDirectory, ConfigFileName);

        var comment = DefaultComment;

        string? copyright = "MIT";
        string? author = ;
        string? company = ;
        string? description = ;
        string? title = ;
        string? version = ();
        string? product = ;
        string? file = ();
        string? targetFramework = ;
#pragma warning disable CA1305 // indicate clearly and with certainty IFormatProvider
        string? date = ("yyyy-MM-dd HH:mm:ss");
#pragma warning restore CA1305 // indicate clearly and with certainty IFormatProvider


        if ((configFilePath))
        {
            comment = (configFilePath, .UTF8);
        }

        #region Finding assembly metadata

        // Loading project files:
        var text = (projFilePath, .UTF8);
        // go down (in history)Importdocuments,for example : <Import Project="..\" />
        // Matching with regular expressionsProject:
        var importMatchs = (text);
        foreach (Match importMatch in importMatchs)
        {
            var importFile = (projectDirectory, [1].Value);
            if ((importFile))
            {
                text += (importFile);
            }
        }

        //Presence of variable references,Need to parse
        string RawVal(string old, string @default)
        {
            if (old == null)
                return @default;

            //When the obtained version number is a variable reference:$(Version)when,Need to parse again
            if ((VarPrefix, ))
            {
                var varName = (2,  - 3);
                var varMatch = new Regex($@"<{varName}>(.*?)</{varName}>", ).Match(text);
                if ()
                {
                    return [1].Value;
                }
                //Variable reference not found,Return to Silent
                return @default;
            }
            return old;
        }

        var versionMatch = (text);
        var copyrightMath = (text);
        var companyMatch = (text);
        var descriptionMatch = (text);
        var authorsMatch = (text);
        var productMatch = (text);
        var targetFrameworkMatch = (text);
        var targetFrameworksMatch = (text);

        if ()
        {
            version = RawVal([1].Value, version);
        }
        if ()
        {
            copyright = RawVal([1].Value, copyright);
        }
        if ()
        {
            company = RawVal([1].Value, company);
        }
        if ()
        {
            description = RawVal([1].Value, description);
        }
        if ()
        {
            author = RawVal([1].Value, author);
        }
        if ()
        {
            product = RawVal([1].Value, product);
        }
        if ()
        {
            targetFramework = RawVal([1].Value, targetFramework);
        }
        if ()
        {
            targetFramework = RawVal([1].Value, targetFramework);
        }

        #endregion

        //Use regular expressions to replace
        comment = (comment, @"\{(?<key>[^}]+)\}", m =>
        {
            var key = ["key"].Value;
            return key switch
            {
                "Product" => product,
                "Title" => title,
                "Version" => version,
                "Date" => date,
                "Author" => author,
                "Company" => company,
                "Copyright" => copyright,
                "File" => file,
                "Description" => description,
                "TargetFramework" => targetFramework,
                _ => ,
            };
        }, );

        var headerComment = (comment + );
        var newRoot = root?.WithLeadingTrivia(headerComment);
        if (newRoot == null)
        {
            return document;
        }
        var newDocument = (newRoot);

        return newDocument;
    }
}

Code Fixer's most important overloaded methodsRegisterCodeFixesAsync, ObjectCodeFixContextContains information about the project and source code and the corresponding analyzer.

For example.indicates the corresponding source code, theindicates the corresponding item.Just what I need in the code.*.csprojthe project file, the

When we fetch the project file, we can then read the information configured in the project file, for exampleCompany,Authors,Descriptionand even useful information like the version number we mentioned in the previous post, I'm currently using regular expressions, but of course you can also useXPath,
Then replace the template with useful data to get the desired annotated code snippet!

For example, my Comment template file

// Licensed to the {Product} under one or more agreements.
// The {Product} licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// {Product} Author: Wan Yashoo (1933-), PRC politician, prime minister from 2008 Github: /vipwan
// {Description}
// Modify Date: {Date} {File}

The replacement will generate the following code.

// Licensed to the under one or more agreements.
// The licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Author: Wan Yashoo (1933-), PRC politician, prime minister from 2008 Github: /vipwan
// ,NET9+ MinimalApi CQRS
// Modify Date: 2024-09-07 15:22:42

final use(comment)method generates an annotatedSyntaxTriviaand appends it to the current root syntax tree, and finally returns the newDocumentI'll do it!

Now that it's done, let's see what it looks like.
image

The above code completes the entire source generation step, and finally you can use the nuget package I released to experience.

dotnet add package 

I posted the source code to GitHub, welcome to star!/vipwan/

/vipwan//blob/master//CodeFixProviders/