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

news/2024/12/23 14:23:07/文章来源:https://www.cnblogs.com/vipwan/p/18401835

之前写过两篇关于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 regexprivate 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);#endregionpublic sealed override ImmutableArray<string> FixableDiagnosticIds{//重写FixableDiagnosticIds,返回分析器的报告Id,表示当前修复器能修复的对应Idget { 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 // 指定 IFormatProviderstring? date = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
#pragma warning restore CA1305 // 指定 IFormatProviderif (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: 万雅虎 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/793649.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

线性dp:LeetCode516 .最长回文子序列

LeetCode516 .最长回文子序列 题目叙述: 力扣题目链接(opens new window) 给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。 子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。 示例 1: 输入:s = "bbbab&…

202409071506,开始写代码,从0开始 验证基本架子

由于视频教程里面 用的VS2105 所以 照抄。开发环境是VS2015 ,WIN10. VS2015 在今天看来是一个很古老的开发环境了,估计都很难找到安装包。(各种安装包:https://www.cnblogs.com/zjoch/p/5694013.html) 用:vs2015.ent_chs.iso (3.88 GB (4,172,560,384 字节))这个安装…

PR出现冲突无法直接解决

举例:存在p-dev 分支,申请合入 master 分支,产生pr 无法直接自动将pr 合入到master中 需要在本地解决 解决:git checkout p-dev,切换分支dev git pull ,更新到最新的 git merge origin master, 此时会出现冲突,通过vscode 或者smartgit 去解决 解决完冲突的文件,需要…

彻底理解字节序

1.基本理论计算机发送数据从内存低地址开始. 计算机接收数据的保存从低地址开始.2.非数值型网络数据传输如上图例子所示,发送端发送了四个字节内容,分别为0x12,0x34,0x56,0x78,假设这四个字节不表示数值例如unsigned int,而是图片内容数据。发送端从低内存地址开始发送四个…

跳跃表

概述 跳跃表(SkipList)是链表加多级索引组成的数据结构。链表的数据结构的查询复条度是 O(N)。为了提高查询效率,可以在链表上加多级索引来实现快速查询。跳跃表不仅能提高搜索性能。也能提高插入和删除操作的性能。索引的层数也叫作跳跃表的高度查找 在跳跃表的结构中会首先从…

Docker 镜像的分层概念

来更深入地理解镜像的概念40.镜像的分层概念 来更深入地理解镜像的概念 ‍ 镜像的分层 镜像,是一种轻量级、可执行的独立软件包,它包含运行某个软件所需的所有内容,我们把应用程序和配置依赖打包好形成一个可交付的运行环境(包括代码、运行时需要的库、环境变量和配置文件等…

prometheus学习笔记之kube-state-metrics

一、kube-state-metrics简介Kube-state-metrics:通过监听 API Server 生成有关资源对象的状态指标,比如 Deployment、Node、Pod,需要注意的是 kube-state-metrics 只是简单的提供一个 metrics 数据, 并不会存储这些指标数据, 所以我 们可以使用 Prometheus 来抓取这些数据然…

事务发件箱模式在 .NET 云原生开发中的应用(基于Aspire)

原文:Transactional Outbox in .NET Cloud Native Development via Aspire 作者:Oleksii Nikiforov总览 这篇文章提供了使用 Aspire、DotNetCore.CAP、Azure Service Bus、Azure SQL、Bicep 和 azd 实现 Outbox 模式的示例。源代码: https://github.com/NikiforovAll/cap-as…

Css 斜线生成案例_Css 斜线/对角线整理

一、Css 斜线,块斜线,对角线 块的宽度高度任意支持<!DOCTYPE html> <html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><tit…

mac升级node到指定版本

node版本升级到稳定版18.16.1 (1)node -v(2)npm cache clean -f 在使用npm cache clear --force清除缓存的时候,报npm WARN using --force Recommended protections disabled的错误,有可能是镜相源过期的问题换为npm cache verify(3) sudo n stable // 把当前系统的 Node 更…

HashMap深入讲解

HashMap是Java中最常用的集合类框架,也是Java语言中非常典型的数据结构, 而HashSet和HashMap者在Java里有着相同的实现,前者仅仅是对后者做了一层包装,也就是说HashSet里面有一个HashMap(适配器模式)。因此了解HashMap源码也就了解HashSet了 介绍Key的存储方式是基于哈希表…

如何实现mysql高可用

1.机器资源耗尽 2.单点故障 3.认为操作 4.网络单点故障解决方案? 1.搭建mysql主从集群(双主,一主多从,多主多从) 2. 可以用MyCat, ShardingJdbc实现A节点同步到B节点流程?1. 从库通过IO线程, 连接到主库,并且向主库要对应的bin log文件 2. 主库通过dump线程获取binlog文…