使用 `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)

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

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

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

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

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

实现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即可!

大功告成,我们来看效果:
使用 `Roslyn` 分析器和修复器对.cs源代码添加头部注释

以上代码就完成了整个源生成步骤,最后你可以使用我发布的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

发表评论

相关文章