1

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

 2 months ago
source link: http://blog.walterlv.com/post/comment-analyzer-and-code-fix-using-roslyn.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

吕毅 发表于 2019-07-22 更新于 1 小时前

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

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


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

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

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

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

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

[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 然后过滤掉获得其中的注释部分。

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

// TODO 林德熙在这个版本写的逗比代码,下个版本要改掉。

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

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 类型的完整代码如下:

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);
                    }
                }
            }
        }
    }
}

代码修改器

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

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

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

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 ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 (walter.lv@qq.com)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK