随着团队越来越多,越来越大,需求更迭越来越快,每天提交的代码变更由原先的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()));
}
}

  补充一下Common.IsReallyContains方法:

class Common
{
public static bool IsReallyContains(SyntaxNode node, string statement)
{
return node.ToString().Contains(statement) && node.DescendantNodes().OfType<LiteralExpressionSyntax>().Count(p => p.ToString().Contains(statement)) ==0 ;
}
}

  

代码实现后的效果(直接调试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代码分析全面提升代码质量的一些具体实践,分享给大家。

周国庆

2020/5/2

.NET Core技术研究-通过Roslyn代码分析技术规范提升代码质量的更多相关文章

  1. .NET Core技术研究系列-索引篇

    随着.NET Core相关技术研究的深入,现在将这一系列的文章,整理到一个索引页中,方便大家翻阅查找,同时,后续也会不断补充进来. .NET Core技术研究-WebApi迁移ASP.NET Core ...

  2. .NET Core技术研究-主机

    前一段时间,和大家分享了 ASP.NET Core技术研究-探秘Host主机启动过程 但是没有深入说明主机的设计.今天整理了一下主机的一些知识,结合先前的博文,完整地介绍一下.NET Core的主机的 ...

  3. ASP.NET Core技术研究-全面认识Web服务器Kestrel

    因为IIS不支持跨平台的原因,我们在升级到ASP.NET Core后,会接触到一个新的Web服务器Kestrel.相信大家刚接触这个Kestrel时,会有各种各样的疑问. 今天我们全面认识一下ASP. ...

  4. ASP.NET Core技术研究-探秘Host主机启动过程

    当我们将原有ASP.NET 应用程序升级迁移到ASP.NET Core之后,我们发现代码工程中多了两个类Program类和Startup类. 接下来我们详细探秘一下通用主机Host的启动过程. 一.P ...

  5. .Net Core技术研究-Span<T>和ValueTuple<T>

    性能是.Net Core一个非常关键的特性,今天我们重点研究一下ValueTuple<T>和Span<T>. 一.方法的多个返回值的实现,看ValueTuple<T> ...

  6. .NET Core技术研究-配置读取

    升级ASP.NET Core后,配置的读取是第一个要明确的技术.原先的App.Config.Web.Config.自定义Config在ASP.NET Core中如何正常使用.有必要好好总结整理一下,相 ...

  7. .NET Core技术研究-中间件的由来和使用

    我们将原有ASP.NET应用升级到ASP.NET Core的过程中,会遇到一个新的概念:中间件. 中间件是ASP.NET Core全新引入的概念.中间件是一种装配到应用管道中以处理请求和响应的软件.  ...

  8. ASP.NET Core技术研究-探秘依赖注入框架

    ASP.NET Core在底层内置了一个依赖注入框架,通过依赖注入的方式注册服务.提供服务.依赖注入不仅服务于ASP.NET Core自身,同时也是应用程序的服务提供者. 毫不夸张的说,ASP.NET ...

  9. .Net Core技术研究-WebApi迁移ASP.NET Core2.0

    随着ASP.NET Core 2.0发布之后,原先运行在Windows IIS中的ASP.NET WebApi站点,就可以跨平台运行在Linux中.我们有必要先说一下ASP.NET Core. ASP ...

随机推荐

  1. shell 数组遍历加引号和不加引号的区别?

    前言 shell 是一个比较神奇的国度,里面有太多的坑需要填,今天需要填的坑就是,数组遍历在使用时加了引号和不加引号的区别. 案例 解析: 不加引号,数组中元素间的“空格”就会编程换行符 加引号,  ...

  2. Linux环境下django初入

    python -m pip install --upgrade pip 终端中 一. 创建项目: 1.django-admin startproject mysite(第一种比较好) 2.django ...

  3. FJUT2019暑假第二次周赛题解

    A 服务器维护 题目大意: 给出时间段[S,E],这段时间需要人维护服务器,给出n个小时间段[ai,bi],代表每个人会维护的时间段,每个人维护这段时间有一个花费,现在问题就是维护服务器[S,E]这段 ...

  4. 【乱码问题】IDEA控制台使用了GBK字符集

    什么Tomcat乱码设置IDEA的初始编码,瞎搞 终于在这个帖子看到了真相 https://blog.csdn.net/weixin_42617398/article/details/81806438 ...

  5. 设计模式中巧记I/O

    一.I/O 1. I/O操作中的设计模式 概要 以设计模式角度,自顶向下理解I/O源码结构 理解字节与字符的关系 1.1 装饰者模式(输入流为例) 背景:通过继承扩展对象耦合度高,使用装饰者扩展可以在 ...

  6. Thinking in Java,Fourth Edition(Java 编程思想,第四版)学习笔记(十一)之Holding Your Objects

    To solve the general programming problem, you need to create any number of objects, anytime, anywher ...

  7. B - Raising Modulo Numbers

    People are different. Some secretly read magazines full of interesting girls' pictures, others creat ...

  8. 深入理解Java线程状态转移

    目录 前言 状态转移图 1.0 新建态到就绪态 1.1 就绪态到运行态 1.2 运行态到就绪态 1.2.1 时间片用完 1.2.2 t1.yield() .Thread.yield(); 1.3 运行 ...

  9. [V&N2020 公开赛] Web misc部分题解

    0x00 前言 写了一天题目,学到了好多东西, 简单记录一下 0x01 Web HappyCTFd 直接使用网上公开的cve打: 解题思路:先注册一个admin空格账号,注意这里的靶机无法访问外网,邮 ...

  10. bytectf2019 boring_code的知识学习&&无参数函数执行&&上海市大学生CTF_boring_code+

    参赛感悟 第三次还是第二次参加这种CTF大赛了,感悟和学习也是蛮多的,越发感觉跟大佬的差距明显,但是还是要努力啊,都大三了,也希望出点成绩.比赛中一道WEB都没做出来,唯一有点思路的只有EZCMS,通 ...