前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用 Roslyn 分析代码注释,给 TODO 类型的注释添加负责人、截止日期和 issue 链接跟踪

使用 Roslyn 分析代码注释,给 TODO 类型的注释添加负责人、截止日期和 issue 链接跟踪

作者头像
walterlv
发布2023-10-23 18:08:19
3821
发布2023-10-23 18:08:19
举报
文章被收录于专栏:walterlv - 吕毅的博客

如果某天改了一点代码但是没有完成,我们可能会在注释里面加上 // TODO。如果某个版本为了控制影响范围临时使用不太合适的方法解了 Bug,我们可能也会在注释里面加上 // TODO。但是,对于团队项目来说,一个人写的 TODO 可能过了一段时间就淹没在大量的 TODO 堆里面了。如果能够强制要求所有的 TODO 被跟踪,那么代码里面就比较容易能够控制住 TODO 的影响了。

本文将基于 Roslyn 开发代码分析器,要求所有的 TODO 注释具有可被跟踪的负责人等信息。

预备知识

如果你对基于 Roslyn 编写分析器和代码修改器不了解,建议先阅读我的一篇入门教程:

  • 基于 Roslyn 同时为 Visual Studio 插件和 NuGet 包开发 .NET/C# 源代码分析器 Analyzer 和修改器 CodeFixProvider - walterlv

分析器

我们先准备一些公共的信息:

代码语言:javascript
复制
namespace Walterlv.Demo
{
    internal static class DiagnosticIds
    {
        /// <summary>
        /// 标记了待办事项的代码必须被追踪。WAL 是我名字(walterlv)的前三个字母。
        /// </summary>
        public const string TodoMustBeTracked = "WAL302";
    }
}

在后面的代码分析器和修改器中,我们将都使用此公共的字符串常量来作为诊断 Id。

我们先添加分析器(TodoMustBeTrackedAnalyzer)最基础的代码:

代码语言:javascript
复制
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class TodoMustBeTrackedAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
        DiagnosticIds.TodoMustBeTracked,
        "任务必须被追踪",
         "未完成的任务缺少负责人和完成截止日期:{0}",
        "Maintainability",
        DiagnosticSeverity.Error,
        isEnabledByDefault: true,
        description: "未完成的任务必须有对应的负责人和截止日期(// TODO @lvyi 2019-08-01),最好有任务追踪系统(如 JIRA)跟踪。");

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

    public override void Initialize(AnalysisContext context)
        => context.RegisterSyntaxTreeAction(AnalyzeSingleLineComment);

    private void AnalyzeSingleLineComment(SyntaxTreeAnalysisContext context)
    {
        // 这里将是我们分析器的主要代码。
    }
}

接下来我们则是要完善语法分析的部分,我们需要找到单行注释和多行注释。

注释在语法节点中不影响代码含义,这些不影响代码含义的语法部件被称作 Trivia(闲杂部件)。这跟我前面入门教程部分说的语法节点不同,其 API 会少一些,但也更加简单。

我们从语法树的 DescendantTrivia 方法中可以拿到文档中的所有的 Trivia 然后过滤掉获得其中的注释部分。

比如,我们要分析下面的这个注释:

代码语言:javascript
复制
// TODO 林德熙在这个版本写的逗比代码,下个版本要改掉。

在语法节点中判断注释的袋子性,然后使用正则表达式匹配 TODO、负责人以及截止日期即可。

代码语言:javascript
复制
private static readonly Regex TodoRegex = new Regex(@"//\s*todo", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex AssigneeRegex = new Regex(@"@\w+", RegexOptions.Compiled);
private static readonly Regex DateRegex = new Regex(@"[\d]{4}\s?[年\-\.]\s?[01]?[\d]\s?[月\-\.]\s?[0123]?[\d]\s?日?", RegexOptions.Compiled);

private void AnalyzeSingleLineComment(SyntaxTreeAnalysisContext context)
{
    var root = context.Tree.GetRoot();

    foreach (var comment in root.DescendantTrivia()
        .Where(x =>
            x.IsKind(SyntaxKind.SingleLineCommentTrivia)
            || x.IsKind(SyntaxKind.MultiLineCommentTrivia)))
    {
        var value = comment.ToString();
        var todoMatch = TodoRegex.Match(value);
        if (todoMatch.Success)
        {
            var assigneeMatch = AssigneeRegex.Match(value);
            var dateMatch = DateRegex.Match(value);

            if (!assigneeMatch.Success || !dateMatch.Success)
            {
                var diagnostic = Diagnostic.Create(Rule, comment.GetLocation(), value);
                context.ReportDiagnostic(diagnostic);
            }
        }
    }
}

将上面的类组装起来运行 Visual Studio 调试即可看到效果。没有负责人和截止日期的 TODO 注释将报告编译错误。

TodoMustBeTrackedAnalyzer 类型的完整代码如下:

代码语言:javascript
复制
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Text.RegularExpressions;
using Walterlv.Demo;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Walterlv.Analyzers.Maintainability
{
    [DiagnosticAnalyzer(LanguageNames.CSharp)]
    public class TodoMustBeTrackedAnalyzer : DiagnosticAnalyzer
    {
        private static readonly LocalizableString Title = "任务必须被追踪";
        private static readonly LocalizableString MessageFormat = "未完成的任务缺少负责人和完成截止日期:{0}";
        private static readonly LocalizableString Description = "未完成的任务必须有对应的负责人和截止日期(// TODO @lvyi 2019-08-01),最好有任务追踪系统(如 JIRA)跟踪。";
        private static readonly Regex TodoRegex = new Regex(@"//\s*todo", RegexOptions.Compiled | RegexOptions.IgnoreCase);
        private static readonly Regex AssigneeRegex = new Regex(@"@\w+", RegexOptions.Compiled);
        private static readonly Regex DateRegex = new Regex(@"[\d]{4}\s?[年\-\.]\s?[01]?[\d]\s?[月\-\.]\s?[0123]?[\d]\s?日?", RegexOptions.Compiled);

        private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
            DiagnosticIds.TodoMustBeTracked,
            Title, MessageFormat,
            Categories.Maintainability,
            DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description);

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

        public override void Initialize(AnalysisContext context)
        {
            context.EnableConcurrentExecution();
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
            context.RegisterSyntaxTreeAction(AnalyzeSingleLineComment);
        }

        private void AnalyzeSingleLineComment(SyntaxTreeAnalysisContext context)
        {
            var root = context.Tree.GetRoot();

            foreach (var comment in root.DescendantTrivia()
                .Where(x =>
                    x.IsKind(SyntaxKind.SingleLineCommentTrivia)
                    || x.IsKind(SyntaxKind.MultiLineCommentTrivia)))
            {
                var value = comment.ToString();
                var todoMatch = TodoRegex.Match(value);
                if (todoMatch.Success)
                {
                    var assigneeMatch = AssigneeRegex.Match(value);
                    var dateMatch = DateRegex.Match(value);

                    if (!assigneeMatch.Success || !dateMatch.Success)
                    {
                        var diagnostic = Diagnostic.Create(Rule, comment.GetLocation(), value);
                        context.ReportDiagnostic(diagnostic);
                    }
                }
            }
        }
    }
}

代码修改器

只是报错的话,开发者看到错误可能会一脸懵逼,因为从未见过注释还会报告编译错误的,不知道怎么改。

于是我们需要编写一个代码修改器以便自动完成注释的修改,添加负责人和截止日期。我这里代码修改器修改后的结果就像下面这样:

生成一个新的注释字符串然后替换即可:

代码语言:javascript
复制
using System;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Walterlv.Demo;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;

namespace Walterlv.Analyzers.Maintainability
{
    [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(TodoMustBeTrackedCodeFixProvider)), Shared]
    public class TodoMustBeTrackedCodeFixProvider : CodeFixProvider
    {
        private const string Title = "添加任务负责人 / 完成日期 / JIRA Id 追踪";
        private static readonly Regex AssigneeRegex = new Regex(@"@\w+", RegexOptions.Compiled);
        private static readonly Regex DateRegex = new Regex(@"[\d]{4}\s?[年\-\.]\s?[01]?[\d]\s?[月\-\.]\s?[0123]?[\d]\s?日?", RegexOptions.Compiled);

        public sealed override ImmutableArray<string> FixableDiagnosticIds =>
            ImmutableArray.Create(DiagnosticIds.TodoMustBeTracked);

        public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

        public sealed override Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            var diagnostic = context.Diagnostics.First();
            context.RegisterCodeFix(CodeAction.Create(
                Title,
                c => FormatTrackableTodoAsync(context.Document, diagnostic, c),
                nameof(TodoMustBeTrackedCodeFixProvider)),
                diagnostic);
            return Task.CompletedTask;
        }

        private async Task<Document> FormatTrackableTodoAsync(
            Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
        {
            var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

            var oldTrivia = root.FindTrivia(diagnostic.Location.SourceSpan.Start);
            var oldComment = oldTrivia.ToString();
            if (oldComment.Length > 3)
            {
                oldComment = oldComment.Substring(2).Trim();
                if (oldComment.StartsWith("todo", StringComparison.CurrentCultureIgnoreCase))
                {
                    oldComment = oldComment.Substring(4).Trim();
                }
            }

            var comment = $"// TODO @{Environment.UserName} {DateTime.Now:yyyy年M月d日} {oldComment}";
            var newTrivia = SyntaxFactory.ParseTrailingTrivia(comment);

            var newRoot = root.ReplaceTrivia(oldTrivia, newTrivia);
            return document.WithSyntaxRoot(newRoot);
        }
    }
}

如果你觉得编写生成代码的语法树很麻烦,可以使用使用 林晓lx 的 RoslynSyntaxTool 工具互相转换 C# 代码与语法树代码

本文会经常更新,请阅读原文: https://blog.walterlv.com/post/comment-analyzer-and-code-fix-using-roslyn.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://blog.walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 ([email protected])

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-07-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 预备知识
  • 分析器
  • 代码修改器
相关产品与服务
腾讯云代码分析
腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,助力维护团队卓越代码文化。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档