首页 > 其他分享 >使用 `Roslyn` 分析器和修复器对.cs源代码添加头部注释

使用 `Roslyn` 分析器和修复器对.cs源代码添加头部注释

时间:2024-09-07 16:25:34浏览次数:8  
标签:string private var readonly static context Roslyn cs 源代码

之前写过两篇关于Roslyn源生成器生成源代码的用例,今天使用Roslyn的代码修复器CodeFixProvider实现一个cs文件头部注释的功能,

代码修复器会同时涉及到CodeFixProviderDiagnosticAnalyzer,

实现FileHeaderAnalyzer

首先我们知道修复器的先决条件是分析器,比如这里,如果要对代码添加头部注释,那么分析器必须要给出对应的分析提醒:

我们首先实现实现名为FileHeaderAnalyzer的分析器:

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class FileHeaderAnalyzer : DiagnosticAnalyzer
{
    public const string DiagnosticId = "GEN050";
    private static readonly LocalizableString Title = "文件缺少头部信息";
    private static readonly LocalizableString MessageFormat = "文件缺少头部信息";
    private static readonly LocalizableString Description = "每个文件应包含头部信息.";
    private const string Category = "Document";

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

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

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

        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();
        context.RegisterSyntaxTreeAction(AnalyzeSyntaxTree);
    }

    private static void AnalyzeSyntaxTree(SyntaxTreeAnalysisContext context)
    {
        var root = context.Tree.GetRoot(context.CancellationToken);
        var firstToken = root.GetFirstToken();

        // 检查文件是否以注释开头
        var hasHeaderComment = firstToken.LeadingTrivia.Any(trivia => trivia.IsKind(SyntaxKind.SingleLineCommentTrivia) || trivia.IsKind(SyntaxKind.MultiLineCommentTrivia));

        if (!hasHeaderComment)
        {
            var diagnostic = Diagnostic.Create(Rule, Location.Create(context.Tree, TextSpan.FromBounds(0, 0)));
            context.ReportDiagnostic(diagnostic);
        }
    }
}

FileHeaderAnalyzer分析器的原理很简单,需要重载几个方法,重点是Initialize方法,这里的RegisterSyntaxTreeAction即核心代码,SyntaxTreeAnalysisContext对象取到当前源代码的SyntaxNode根节点,然后判断TA的第一个SyntaxToken是否为注释行(SyntaxKind.SingleLineCommentTrivia|SyntaxKind.MultiLineCommentTrivia)

如果不为注释行,那么就通知分析器!

实现了上面的代码我们看一下效果:

image

并且编译的时候分析器将会在错误面板中显示警告清单:

image

实现CodeFixProvider

分析器完成了,现在我们就来实现名为AddFileHeaderCodeFixProvider的修复器,

/// <summary>
/// 自动给文件添加头部注释
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AddFileHeaderCodeFixProvider))]
[Shared]
public class AddFileHeaderCodeFixProvider : CodeFixProvider
{
    private const string Title = "添加文件头部信息";
    //约定模板文件的名称
    private const string ConfigFileName = "Biwen.AutoClassGen.Comment";
    private const string VarPrefix = "$";//变量前缀
    //如果模板不存在的时候的默认注释文本
    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 = RegexOptions.Compiled | RegexOptions.Singleline;
    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
    {
        //重写FixableDiagnosticIds,返回分析器的报告Id,表示当前修复器能修复的对应Id
        get { return [FileHeaderAnalyzer.DiagnosticId]; }
    }

    public sealed override FixAllProvider GetFixAllProvider()
    {
        return WellKnownFixAllProviders.BatchFixer;
    }

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

        context.RegisterCodeFix(
            CodeAction.Create(
                title: Title,
                createChangedDocument: c => FixDocumentAsync(context.Document, diagnosticSpan, c),
                equivalenceKey: Title),
            diagnostic);

        return Task.CompletedTask;
    }


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

        //从项目配置中获取文件头部信息
        var projFilePath = document.Project.FilePath ?? "C:\\test.csproj";//单元测试时没有文件路径,因此使用默认路径

        var projectDirectory = Path.GetDirectoryName(projFilePath);
        var configFilePath = Path.Combine(projectDirectory, ConfigFileName);

        var comment = DefaultComment;

        string? copyright = "MIT";
        string? author = Environment.UserName;
        string? company = string.Empty;
        string? description = string.Empty;
        string? title = document.Project.Name;
        string? version = document.Project.Version.ToString();
        string? product = document.Project.AssemblyName;
        string? file = Path.GetFileName(document.FilePath);
        string? targetFramework = string.Empty;
#pragma warning disable CA1305 // 指定 IFormatProvider
        string? date = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
#pragma warning restore CA1305 // 指定 IFormatProvider


        if (File.Exists(configFilePath))
        {
            comment = File.ReadAllText(configFilePath, System.Text.Encoding.UTF8);
        }

        #region 查找程序集元数据

        // 加载项目文件:
        var text = File.ReadAllText(projFilePath, System.Text.Encoding.UTF8);
        // 载入Import的文件,例如 : <Import Project="..\Version.props" />
        // 使用正则表达式匹配Project:
        var importMatchs = ImportRegex.Matches(text);
        foreach (Match importMatch in importMatchs)
        {
            var importFile = Path.Combine(projectDirectory, importMatch.Groups[1].Value);
            if (File.Exists(importFile))
            {
                text += File.ReadAllText(importFile);
            }
        }

        //存在变量引用的情况,需要解析
        string RawVal(string old, string @default)
        {
            if (old == null)
                return @default;

            //当取得的版本号为变量引用:$(Version)的时候,需要再次解析
            if (version.StartsWith(VarPrefix, StringComparison.Ordinal))
            {
                var varName = old.Substring(2, old.Length - 3);
                var varMatch = new Regex($@"<{varName}>(.*?)</{varName}>", RegexOptions.Singleline).Match(text);
                if (varMatch.Success)
                {
                    return varMatch.Groups[1].Value;
                }
                //未找到变量引用,返回默
                return @default;
            }
            return old;
        }

        var versionMatch = VersionRegex.Match(text);
        var copyrightMath = CopyrightRegex.Match(text);
        var companyMatch = CompanyRegex.Match(text);
        var descriptionMatch = DescriptionRegex.Match(text);
        var authorsMatch = AuthorsRegex.Match(text);
        var productMatch = ProductRegex.Match(text);
        var targetFrameworkMatch = TargetFrameworkRegex.Match(text);
        var targetFrameworksMatch = TargetFrameworksRegex.Match(text);

        if (versionMatch.Success)
        {
            version = RawVal(versionMatch.Groups[1].Value, version);
        }
        if (copyrightMath.Success)
        {
            copyright = RawVal(copyrightMath.Groups[1].Value, copyright);
        }
        if (companyMatch.Success)
        {
            company = RawVal(companyMatch.Groups[1].Value, company);
        }
        if (descriptionMatch.Success)
        {
            description = RawVal(descriptionMatch.Groups[1].Value, description);
        }
        if (authorsMatch.Success)
        {
            author = RawVal(authorsMatch.Groups[1].Value, author);
        }
        if (productMatch.Success)
        {
            product = RawVal(productMatch.Groups[1].Value, product);
        }
        if (targetFrameworkMatch.Success)
        {
            targetFramework = RawVal(targetFrameworkMatch.Groups[1].Value, targetFramework);
        }
        if (targetFrameworksMatch.Success)
        {
            targetFramework = RawVal(targetFrameworksMatch.Groups[1].Value, targetFramework);
        }

        #endregion

        //使用正则表达式替换
        comment = Regex.Replace(comment, @"\{(?<key>[^}]+)\}", m =>
        {
            var key = m.Groups["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,
                _ => m.Value,
            };
        }, RegexOptions.Singleline);

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

        return newDocument;
    }
}

代码修复器最重要的重载方法RegisterCodeFixesAsync,对象CodeFixContext包含项目和源代码以及对应分析器的信息:

比如:CodeFixContext.Document表示对应的源代码,CodeFixContext.Document.Project表示对应项目,CodeFixContext.Document.Project.FilePath就是代码中我需要的*.csproj的项目文件,

我们取到项目文件,那么我们就可以读取配置在项目文件中的信息,比如Company,Authors,Description,甚至上一篇我们提到的版本号等有用信息,当前我用的正则表达式,当然如果可以你也可以使用XPath,
然后取到的有用数据替换模板即可得到想要的注释代码片段了!

比如我的Comment模板文件Biwen.AutoClassGen.Comment

// 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: {Author} Github: https://github.com/vipwan
// {Description}
// Modify Date: {Date} {File}

替换后将会生成如下的代码:

// Licensed to the Biwen.QuickApi under one or more agreements.
// The Biwen.QuickApi licenses this file to you under the MIT license. 
// See the LICENSE file in the project root for more information.
// Biwen.QuickApi Author: 万雅虎 Github: https://github.com/vipwan
// Biwen.QuickApi ,NET9+ MinimalApi CQRS
// Modify Date: 2024-09-07 15:22:42 Verb.cs

最后使用SyntaxFactory.Comment(comment)方法生成一个注释的SyntaxTrivia并附加到当前的根语法树上,最后返回这个新的Document即可!

大功告成,我们来看效果:
image

以上代码就完成了整个源生成步骤,最后你可以使用我发布的nuget包体验:

dotnet add package Biwen.AutoClassGen

源代码我发布到了GitHub,欢迎star! https://github.com/vipwan/Biwen.AutoClassGen

https://github.com/vipwan/Biwen.AutoClassGen/blob/master/Biwen.AutoClassGen.Gen/CodeFixProviders/AddFileHeaderCodeFixProvider.cs

标签:string,private,var,readonly,static,context,Roslyn,cs,源代码
From: https://www.cnblogs.com/vipwan/p/18401835

相关文章

  • 前端必知必会-CSS 按钮
    文章目录CSS按钮Buttons基本按钮样式按钮颜色按钮大小圆角按钮彩色按钮边框可悬停按钮阴影按钮已禁用按钮按钮宽度按钮组带边框的按钮组垂直按钮组图像上的按钮总结CSS按钮Buttons了解如何使用CSS设置按钮样式。基本按钮样式示例.button{background-colo......
  • CSP模拟2
    A.不相邻集合考虑值域上连续的段,可以发现连续的长度为\(x\)的段的贡献必定为\(\lceil{\frac{x}{2}}\rceil\)考虑并查集维护值域连续的段的大小,每次询问求出全部连续段的\(\lceil{\frac{size}{2}}\rceil\)之和即为答案合并操作:在值域上加入\(x\),尝试合并\(x-1\)与\(x+1......
  • Css 斜线生成案例_Css 斜线/对角线整理
    一、Css斜线,块斜线,对角线块的宽度高度任意支持<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width,initial-scale=1.0"><title......
  • prometheus学习笔记之kube-state-metrics
    一、kube-state-metrics简介Kube-state-metrics:通过监听APIServer生成有关资源对象的状态指标,比如Deployment、Node、Pod,需要注意的是kube-state-metrics只是简单的提供一个metrics数据,并不会存储这些指标数据,所以我们可以使用Prometheus来抓取这些数据然后存储,......
  • 探索最佳 Shell 工具:全面测评 Bash、Zsh、Fish、Tcsh 和 Ksh
    感谢浪浪云支持发布浪浪云活动链接:https://langlangy.cn/?i8afa52文章目录1.简介2.测评工具3.测评标准4.Bash测评4.1易用性4.2功能特性4.3性能4.4可定制性4.5社区和支持5.Zsh测评5.1易用性5.2功能特性5.3性能5.4可定制性5.5社区和支持6.Fish测......
  • 详解CSS
    目录CSS语法引入方式选择器标签选择器 类选择器ID选择器通配符选择器复合选择器常用CSScolorfont-sizeborderwidth和heightpadding外边距CSSCSS(CascadingStyleSheet),层叠样式表,⽤于控制⻚⾯的样式.CSS能够对⽹⻚中元素位置的排版进⾏像素级精确......
  • 【零基础玩转树莓派】03-USB摄像头和CSI摄像头的使用
    摄像头USB摄像头的使用环境搭建FSWebcam是一个简洁明了的网络摄像头应用程序,软件安装命令如下:sudoaptinstallfswebcam添加用户权限:sudousermod-a-Gvideo示例:添加pi用户权限到群组中:sudousermod-a-Gvideopi检查用户是否已正确添加到群组中:groups查看USB......
  • 响应式单位rpx及搭配UI产品工具和@import导入css样式和scss变量与用法static目录以及p
    1.响应式单位rpx及搭配UI产品工具1.对于rpx的认知和用法以及对于px的区别我们可以去官网先了解一下我们的rpx用法和注意以及与px的区别。链接:页面样式与布局|uni-app官网(dcloud.net.cn)我平时使用的px是我们的屏幕像素的单位,我们在一个页面创建一个view组件,并且给其在......
  • css中的响应式单位rpx,vue3中@import的导入
    尺寸单位,px和rpx对比px单位像素是图像的基本采样单位,它不是一个确定的物理量,不同的设备,其图像基本单位是不同的,比如显示器的点距,可以认为是显示器的物理像素rpx单位可以根据屏幕宽度进行自适应。rpx其实是微信对于rem的一种应用的规定,或者说一种设计的方案,官方上规定屏幕......
  • 475. 简单的学校网页 大学生期末大作业 Web前端网页制作 html+css
    目录一、网页概述二、网页文件 三、网页效果四、代码展示1.html2.CSS五、总结1.简洁实用2.使用方便3.整体性好4.形象突出5.交互式强六、更多推荐欢迎光临仙女的网页世界!这里有各行各业的Web前端网页制作的案例,样式齐全新颖,并持续更新!感谢CSDN,提供了这么好......