3

.NET Core 如何通过Roslyn代码分析技术规范提升代码质量?

 3 years ago
source link: https://zhuanlan.zhihu.com/p/347886630
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.

.NET Core 如何通过Roslyn代码分析技术规范提升代码质量?

文章已收录入:布莱恩特:.NET Core开发精选文章目录,持续更新,欢迎投稿!

欢迎关注http://ASP.NET Core 知识专栏:.NET Core开发者精选

随着团队越来越多,越来越大,需求更迭越来越快,每天提交的代码变更由原先的2位数,暴涨到3位数,每天几百次代码Check In,补丁提交,大量的代码审查消耗了大量的资源投入。

如何确保提交代码的质量和提测产品的质量,这两个是非常大的挑战。

工欲善其事,必先利其器。在上述需求背景下,今年我们准备用工具和技术,全面把控并提升代码质量和产品提测质量。即:

1、代码质量提升:通过自定义代码扫描规则,将有问题的代码、不符合编码规则的代码扫描出来,禁止签入。

2、产品提测质量:通过单元测试覆盖率和执行通过率,严控产品提交质量,覆盖率和通过率达不到标准,无法提交测试。

准备用2篇文章,和大家分享我们是如何提升代码质量和产品提测质量的。

先分享第一篇:通过Roslyn代码分析全面提升代码质量。

一、什么是Roslyn?

Roslyn 是微软开源的 .NET 编译平台(.NET Compiler Platform)。 编译平台支持 C# 和 Visual Basic 代码编译,并提供丰富的代码分析 API。

利用Roslyn可以生成代码分析器和代码修补程序,从而发现和更正编码错误。

分析器不仅理解代码的语法和结构,还能检测应更正的做法。代码修补程序建议一处或多处修复,以修复分析器发现的编码错误。

我们写下面一堆代码,Roslyn编译器会有如下提示:

通过编写分析器和代码修补程序,主要服务以下场景:

  • 强制执行团队编码标准(Local)
  • 提供库包方面的指导约束(Nuget)
  • 提供代码分析器相关的VSIX扩展插件(Visual Studio Marketplace)

Roslyn是如何做到代码分析的呢?这背后依赖于一套强大的语法分析和API:

上图中:Language Service:语言层面的服务,可以简单理解为我们在VS中编码时,可以实现的语法高亮、查找所有引用、重命名、转到定义、格式化、抽取方法等操作。

Compiler API:编译器API,这里提供了Syntax Tree API代码语法树API,Symbol API代码符号API。

Binding and Flow Anllysis APIs绑定和流分析API(https://joshvarty.com/2015/02/05/learn-roslyn-now-part-8-data-flow-analysis/),

Emit API编译反射发出API(https://joshvarty.com/2016/01/16/learn-roslyn-now-part-16-the-emit-api/

这里我们详细看一下语法树、符号、语义模型、工作区:

1、语法树是一种由编译器 API 公开的基础数据结构。这些树表示源代码的词法和语法结构。其包含:

  • 语法节点:是语法树的一个主要元素。这些节点表示声明、语句、子句和表达式等语法构造。
  • 语法标记:表示代码的最小语法片段。语法标记包含关键字、标识符、文本和标点。
  • 琐碎内容:对正常理解代码基本上没有意义的源文本部分,例如空格、注释和预处理器指令。
  • 范围:每个节点、标记或琐碎内容在源文本内的位置和包含的字符数。
  • 种类:标识节点、标记或琐碎内容所表示的确切语法元素。
  • 错误:表示源文本中包含的语法错误。

看一张语法树的图:

2、符号:符号表示源代码声明的不同元素,或作为元数据从程序集中导出。每个命名空间、类型、方法、属性、字段、事件、参数或局部变量都由符号表示。

3、语义模型:语义模型表示单个源文件的所有语义信息。可使用语义模型查找到以下内容:

  • 在源中特定位置引用的符号。
  • 任何表达式的结果类型。
  • 所有诊断(错误和警告)。
  • 变量流入和流出源区域的方式。
  • 更多推理问题的答案。

4、工作区:工作区是对整个解决方案执行代码分析和重构的起点。相关的API可以实现:

将解决方案中项目的全部相关信息组织为单个对象模型,可让用户直接访问编译器层对象模型(如源文本、语法树、语义模型和编译),而无需分析文件、配置选项,或管理项目内依赖项。

了解了Roslyn的大致情况之后,我们开始基于Roslyn做一些“不符合编程规范要求(团队自定义的)”的代码分析。

二、基于Roslyn进行代码分析

接下来讲通过Show case的方法,通过实际的场景和大家分享。在我们编写实际的代码分析器之前,我们先把开发环境准备好 :

使用VS2017创建一个Analyzer with Code Fix工程

因为我本机的VS2019找了好久没找到对应的工程,这个章节,使用VS2017吧

创建完成会有两个工程:

其中,TeldCodeAnalyzer.Vsix工程,主要用以生成VSIX扩展文件

TeldCodeAnalyzer工程,主要用于编写代码分析器。

工程转换好之后,我们开始编码吧。

1、catch 吞掉异常场景

问题:catch吞掉异常后,线上很难排查问题,同时确定哪块代码有问题

示例代码:

try
{
     var logService = HSFService.Proxy<ILogService>();
     logService.SendMsg(new SysActionLog());
}
catch (Exception ex)
{
}

需求:当开发人员在catch吞掉异常时,给与编程提示:异常吞掉时必须上报监控或者日志

明确了上述需要,我们开始编写Roslyn代码分析器。ExceptionCatchWithMonitorAnalyzer

我们详细解读一下:

① ExceptionCatchWithMonitorAnalyzer必须继承抽象类DiagnosticAnalyzer

② 重写方法SupportedDiagnostics,注册代码扫描规则:DiagnosticDescriptor

internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

③ 重写方法Initialize,注册Microsoft.CodeAnalysis.SyntaxNode完成Catch语句的语义分析后的事件Action

public override void Initialize(AnalysisContext context)
{
 
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.None);

      context.EnableConcurrentExecution();
      context.RegisterSyntaxNodeAction(AnalyzeDeclaration,
                          SyntaxKind.CatchClause);

}

④ 实现语法分析AnalyzeDeclaration,检查对catch语句中代码实现

private void AnalyzeDeclaration(SyntaxNodeAnalysisContext context)
{
    var catchClause = (CatchClauseSyntax)context.Node;
    var block = catchClause.Block;
    foreach (var statement in block.Statements)
    {
        if (statement is ThrowStatementSyntax)
        {
            return;
        }
    }
    if (Common.IsReallyContains(block, "MonitorClient") == false)
    {
        context.ReportDiagnostic(Diagnostic.Create(Rule, block.GetLocation()));
    }
}

代码实现后的效果(直接调试VSIX工程即可)

代码编译后也有对应Warnning提示

2、在For循环中进行服务调用

问题:for循环中调用RPC服务,每次访问都会发起一次RPC请求,如果循环次数太多,性能很差,建议使用批量处理的RPC方法

示例代码:

foreach (var item in items)
{
    var logService = HSFService.Proxy<ILogService>();
    logService.SendMsg(new SysActionLog());
}

需求:当开发人员在For循环中调用HSF服务时,给与编程提示:不建议在循环中调用HSF服务, 建议调用批量处理方法.

明确了上述需要,我们开始编写Roslyn代码分析器。HSFForLoopAnalyzer

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class HSFForLoopAnalyzer : DiagnosticAnalyzer
{
    public const string DiagnosticId = "TA001";
    internal const string Title = "增加循环中HSF服务调用检查";
    public const string MessageFormat = "不建议在循环中调用HSF服务, 建议调用批量处理方法.";
    internal const string Category = "CodeSmell";
    internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category,
        DiagnosticSeverity.Warning, isEnabledByDefault: true);
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
    public override void Initialize(AnalysisContext context)
    {
        context.RegisterSyntaxNodeAction(AnalyzeMethodForLoop, SyntaxKind.InvocationExpression);
    }

    private static void AnalyzeMethodForLoop(SyntaxNodeAnalysisContext context)
    {
        var expression = (InvocationExpressionSyntax)context.Node;
        string exressionText = expression.ToString();
        if (Common.IsReallyContains(expression, "HSFService.Proxy<"))
        {
            var loop = expression.Ancestors().FirstOrDefault(p => p is ForStatementSyntax || p is ForEachStatementSyntax || p is DoStatementSyntax || p is WhileStatementSyntax);
            if (loop != null)
            {
                var diagnostic = Diagnostic.Create(Rule, expression.GetLocation());
                context.ReportDiagnostic(diagnostic);
                return;
            }
            if (Common.IsReallyContains(expression, ">.") == false)
            {
                var syntax = expression.Ancestors().FirstOrDefault(p => p is LocalDeclarationStatementSyntax);
                if (syntax != null)
                {
                    var declaration = (LocalDeclarationStatementSyntax)syntax;
                    var variable = declaration.Declaration.Variables.SingleOrDefault();

                  
                    var method = declaration.Ancestors().First(p => p is MethodDeclarationSyntax);
                    var expresses = method.DescendantNodes().Where(p => p is InvocationExpressionSyntax);
                    foreach (var express in expresses)
                    {
                        loop = express.Ancestors().FirstOrDefault(p => p is ForStatementSyntax || p is ForEachStatementSyntax || p is DoStatementSyntax || p is WhileStatementSyntax);
                        if (loop != null)
                        {
                            var diagnostic = Diagnostic.Create(Rule, expression.GetLocation()); context.ReportDiagnostic(diagnostic);
                            return;
                        }
                    }
                }
            }
        }
    }
}

基本的实现方式,和上一个差不多,唯一不同的逻辑是在实际的代码分析过程中,AnalyzeMethodForLoop。大家可以根据自己的需要写一下。

实际的效果:

还有几个代码检查场景,基本都是同样的实现思路,再次不一一罗列了。

在这里还可以自动完成代理修补程序,这个地方我们还在研究中,可能每个业务代码的场景不同,很难给出一个通用的改进代码,所以这个地方等后续我们完成后,再和大家分享。

三、通过Roslyn实现静态代码扫描

线上很多代码已经写完了,发布上线了,对已有的代码进行代码扫描也是非常重要的。因此,我们对catch吞掉异常的代码进行了一次集中扫描和改进。

那么基于Roslyn如何实现静态代码扫描呢?主要的步骤有:

① 创建一个编译工作区MSBuildWorkspace.Create()

② 打开解决方案文件OpenSolutionAsync(slnPath);

③ 遍历Project中的Document

④ 拿到代码语法树、找到Catch语句CatchClauseSyntax

⑤ 判断是否有throw语句,如果没有,收集数据进行通知改进

看一下具体代码实现,先看一下Nuget引用:

Microsoft.CodeAnalysis

Microsoft.CodeAnalysis.Workspaces.MSBuild

代码的具体实现:

public async Task<List<CodeCheckResult>> CheckSln(string slnPath)
{
    var slnFile = new FileInfo(slnPath);
    var results = new List<CodeCheckResult>();
    var solution = await MSBuildWorkspace.Create().OpenSolutionAsync(slnPath);
    if (solution.Projects != null && solution.Projects.Count() > 0)
    {
        foreach (var project in solution.Projects.ToList())
        {
            var documents = project.Documents.Where(x => x.Name.Contains(".cs"));
            foreach (var document in documents)
            {
                var tree = await document.GetSyntaxTreeAsync();
                var root = tree.GetCompilationUnitRoot();
                if (root.Members == null || root.Members.Count == 0) continue;
                //member
                var firstmember = root.Members[0];
                //命名空间Namespace
                var namespaceDeclaration = (NamespaceDeclarationSyntax)firstmember;
                foreach (var classDeclare in namespaceDeclaration.Members)
                {
                    var programDeclaration = classDeclare as ClassDeclarationSyntax;
                    foreach (var method in programDeclaration.Members)
                    {
                        //方法 Method
                        var methodDeclaration = (MethodDeclarationSyntax)method;
                        var catchNode = methodDeclaration.DescendantNodes().FirstOrDefault(i => i is CatchClauseSyntax);
                        if (catchNode != null)
                        {
                            var catchClause = catchNode as CatchClauseSyntax;
                            if (catchClause != null || catchClause.Declaration != null)
                            {
                                if (catchClause.DescendantNodes().OfType<ThrowStatementSyntax>().Count() == 0)
                                {
                                    results.Add(new CodeCheckResult()
                                    {
                                        Sln = slnFile.Name,
                                        ProjectName = project.Name,
                                        ClassName = programDeclaration.Identifier.Text,
                                        MethodName = methodDeclaration.Identifier.Text,
                                    });
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    return results;
}

以上是通过Roslyn代码分析全面提升代码质量的一些具体实践,分享给大家。

原文:.NET Core技术研究-通过Roslyn代码分析技术规范提升代码质量


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK