.NET Core 通過Roslyn代碼分析技術(shù)規(guī)范提升代碼質(zhì)量
轉(zhuǎn)自:Eric zhou cnblogs.com/tianqing/p/12815747.html
前言
隨著團(tuán)隊(duì)越來越多,越來越大,需求更迭越來越快,每天提交的代碼變更由原先的2位數(shù),暴漲到3位數(shù),每天幾百次代碼Check In,補(bǔ)丁提交,大量的代碼審查消耗了大量的資源投入。
如何確保提交代碼的質(zhì)量和提測(cè)產(chǎn)品的質(zhì)量,這兩個(gè)是非常大的挑戰(zhàn)。
工欲善其事,必先利其器。在上述需求背景下,今年我們準(zhǔn)備用工具和技術(shù),全面把控并提升代碼質(zhì)量和產(chǎn)品提測(cè)質(zhì)量。即:
1、代碼質(zhì)量提升:通過自定義代碼掃描規(guī)則,將有問題的代碼、不符合編碼規(guī)則的代碼掃描出來,禁止簽入
2、產(chǎn)品提測(cè)質(zhì)量:通過單元測(cè)試覆蓋率和執(zhí)行通過率,嚴(yán)控產(chǎn)品提交質(zhì)量,覆蓋率和通過率達(dá)不到標(biāo)準(zhǔn),無(wú)法提交測(cè)試。
準(zhǔn)備用2篇文章,和大家分享我們是如何提升代碼質(zhì)量和產(chǎn)品提測(cè)質(zhì)量的。
先分享第一篇:通過Roslyn代碼分析全面提升代碼質(zhì)量。
一、什么是Roslyn
Roslyn 是微軟開源的 .NET 編譯平臺(tái)(.NET Compiler Platform)。 編譯平臺(tái)支持 C# 和 Visual Basic 代碼編譯,并提供豐富的代碼分析 API。
利用Roslyn可以生成代碼分析器和代碼修補(bǔ)程序,從而發(fā)現(xiàn)和更正編碼錯(cuò)誤。
分析器不僅理解代碼的語(yǔ)法和結(jié)構(gòu),還能檢測(cè)應(yīng)更正的做法。代碼修補(bǔ)程序建議一處或多處修復(fù),以修復(fù)分析器發(fā)現(xiàn)的編碼錯(cuò)誤。
我們寫下面一堆代碼,Roslyn編譯器會(huì)有如下提示:

通過編寫分析器和代碼修補(bǔ)程序,主要服務(wù)以下場(chǎng)景:
強(qiáng)制執(zhí)行團(tuán)隊(duì)編碼標(biāo)準(zhǔn)(Local)
提供庫(kù)包方面的指導(dǎo)約束(Nuget)
提供代碼分析器相關(guān)的VSIX擴(kuò)展插件(Visual Studio Marketplace)
Roslyn是如何做到代碼分析的呢?這背后依賴于一套強(qiáng)大的語(yǔ)法分析和API:

上圖中:Language Service:語(yǔ)言層面的服務(wù),可以簡(jiǎn)單理解為我們?cè)赩S中編碼時(shí),可以實(shí)現(xiàn)的語(yǔ)法高亮、查找所有引用、重命名、轉(zhuǎn)到定義、格式化、抽取方法等操作
Compiler API:編譯器API,這里提供了Syntax Tree API代碼語(yǔ)法樹API,Symbol API代碼符號(hào)API
Binding and Flow Anllysis APIs綁定和流分析API(https://joshvarty.com/2015/02/05/learn-roslyn-now-part-8-data-flow-analysis/),
Emit API編譯反射發(fā)出API(https://joshvarty.com/2016/01/16/learn-roslyn-now-part-16-the-emit-api/)
這里我們?cè)敿?xì)看一下語(yǔ)法樹、符號(hào)、語(yǔ)義模型、工作區(qū):
1、語(yǔ)法樹是一種由編譯器 API 公開的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)。這些樹表示源代碼的詞法和語(yǔ)法結(jié)構(gòu)。其包含:
語(yǔ)法節(jié)點(diǎn):是語(yǔ)法樹的一個(gè)主要元素。這些節(jié)點(diǎn)表示聲明、語(yǔ)句、子句和表達(dá)式等語(yǔ)法構(gòu)造。
語(yǔ)法標(biāo)記:表示代碼的最小語(yǔ)法片段。語(yǔ)法標(biāo)記包含關(guān)鍵字、標(biāo)識(shí)符、文本和標(biāo)點(diǎn)。
瑣碎內(nèi)容:對(duì)正常理解代碼基本上沒有意義的源文本部分,例如空格、注釋和預(yù)處理器指令。
范圍:每個(gè)節(jié)點(diǎn)、標(biāo)記或瑣碎內(nèi)容在源文本內(nèi)的位置和包含的字符數(shù)。
種類:標(biāo)識(shí)節(jié)點(diǎn)、標(biāo)記或瑣碎內(nèi)容所表示的確切語(yǔ)法元素。
錯(cuò)誤:表示源文本中包含的語(yǔ)法錯(cuò)誤。
看一張語(yǔ)法樹的圖:

2、符號(hào):符號(hào)表示源代碼聲明的不同元素,或作為元數(shù)據(jù)從程序集中導(dǎo)出。每個(gè)命名空間、類型、方法、屬性、字段、事件、參數(shù)或局部變量都由符號(hào)表示。
3、語(yǔ)義模型:語(yǔ)義模型表示單個(gè)源文件的所有語(yǔ)義信息??墒褂谜Z(yǔ)義模型查找到以下內(nèi)容:
在源中特定位置引用的符號(hào)。
任何表達(dá)式的結(jié)果類型。
所有診斷(錯(cuò)誤和警告)。
變量流入和流出源區(qū)域的方式。
更多推理問題的答案。
4、工作區(qū):工作區(qū)是對(duì)整個(gè)解決方案執(zhí)行代碼分析和重構(gòu)的起點(diǎn)。相關(guān)的API可以實(shí)現(xiàn):
將解決方案中項(xiàng)目的全部相關(guān)信息組織為單個(gè)對(duì)象模型,可讓用戶直接訪問編譯器層對(duì)象模型(如源文本、語(yǔ)法樹、語(yǔ)義模型和編譯),而無(wú)需分析文件、配置選項(xiàng),或管理項(xiàng)目?jī)?nèi)依賴項(xiàng)。

了解了Roslyn的大致情況之后,我們開始基于Roslyn做一些“不符合編程規(guī)范要求(團(tuán)隊(duì)自定義的)”的代碼分析。
二、基于Roslyn進(jìn)行代碼分析
接下來講通過Show case的方法,通過實(shí)際的場(chǎng)景和大家分享。在我們編寫實(shí)際的代碼分析器之前,我們先把開發(fā)環(huán)境準(zhǔn)備好 :
使用VS2017創(chuàng)建一個(gè)Analyzer with Code Fix工程
因?yàn)槲冶緳C(jī)的VS2019找了好久沒找到對(duì)應(yīng)的工程,這個(gè)章節(jié),使用VS2017吧

創(chuàng)建完成會(huì)有兩個(gè)工程:

其中,TeldCodeAnalyzer.Vsix工程,主要用以生成VSIX擴(kuò)展文件
TeldCodeAnalyzer工程,主要用于編寫代碼分析器。
工程轉(zhuǎn)換好之后,我們開始編碼吧。
1、catch 吞掉異常場(chǎng)景
問題:catch吞掉異常后,線上很難排查問題,同時(shí)確定哪塊代碼有問題
示例代碼:
try
{
var logService = HSFService.Proxy<ILogService>();
logService.SendMsg(new SysActionLog());
}
catch (Exception ex)
{
}需求:當(dāng)開發(fā)人員在catch吞掉異常時(shí),給與編程提示:異常吞掉時(shí)必須上報(bào)監(jiān)控或者日志
明確了上述需要,我們開始編寫Roslyn代碼分析器。ExceptionCatchWithMonitorAnalyzer

我們?cè)敿?xì)解讀一下:
① ExceptionCatchWithMonitorAnalyzer必須繼承抽象類DiagnosticAnalyzer
② 重寫方法SupportedDiagnostics,注冊(cè)代碼掃描規(guī)則: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,注冊(cè)Microsoft.CodeAnalysis.SyntaxNode完成Catch語(yǔ)句的語(yǔ)義分析后的事件Action
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeDeclaration,
SyntaxKind.CatchClause);
}④ 實(shí)現(xiàn)語(yǔ)法分析AnalyzeDeclaration,檢查對(duì)catch語(yǔ)句中代碼實(shí)現(xiàn)
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()));
}
}代碼實(shí)現(xiàn)后的效果(直接調(diào)試VSIX工程即可)

代碼編譯后也有對(duì)應(yīng)Warnning提示
2、在For循環(huán)中進(jìn)行服務(wù)調(diào)用
問題:for循環(huán)中調(diào)用RPC服務(wù),每次訪問都會(huì)發(fā)起一次RPC請(qǐng)求,如果循環(huán)次數(shù)太多,性能很差,建議使用批量處理的RPC方法
示例代碼:
foreach (var item in items)
{
var logService = HSFService.Proxy<ILogService>();
logService.SendMsg(new SysActionLog());
}需求:當(dāng)開發(fā)人員在For循環(huán)中調(diào)用HSF服務(wù)時(shí),給與編程提示:不建議在循環(huán)中調(diào)用HSF服務(wù), 建議調(diào)用批量處理方法.
明確了上述需要,我們開始編寫Roslyn代碼分析器。HSFForLoopAnalyzer
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class HSFForLoopAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "TA001";
internal const string Title = "增加循環(huán)中HSF服務(wù)調(diào)用檢查";
public const string MessageFormat = "不建議在循環(huán)中調(diào)用HSF服務(wù), 建議調(diào)用批量處理方法.";
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;
}
}
}
}
}
}
}基本的實(shí)現(xiàn)方式,和上一個(gè)差不多,唯一不同的邏輯是在實(shí)際的代碼分析過程中,AnalyzeMethodForLoop。大家可以根據(jù)自己的需要寫一下。
實(shí)際的效果:

還有幾個(gè)代碼檢查場(chǎng)景,基本都是同樣的實(shí)現(xiàn)思路,再次不一一羅列了。
在這里還可以自動(dòng)完成代理修補(bǔ)程序,這個(gè)地方我們還在研究中,可能每個(gè)業(yè)務(wù)代碼的場(chǎng)景不同,很難給出一個(gè)通用的改進(jìn)代碼,所以這個(gè)地方等后續(xù)我們完成后,再和大家分享。
三、通過Roslyn實(shí)現(xiàn)靜態(tài)代碼掃描
線上很多代碼已經(jīng)寫完了,發(fā)布上線了,對(duì)已有的代碼進(jìn)行代碼掃描也是非常重要的。因此,我們對(duì)catch吞掉異常的代碼進(jìn)行了一次集中掃描和改進(jìn)。
那么基于Roslyn如何實(shí)現(xiàn)靜態(tài)代碼掃描呢?主要的步驟有:
① 創(chuàng)建一個(gè)編譯工作區(qū)MSBuildWorkspace.Create()
② 打開解決方案文件OpenSolutionAsync(slnPath);
③ 遍歷Project中的Document
④ 拿到代碼語(yǔ)法樹、找到Catch語(yǔ)句CatchClauseSyntax
⑤ 判斷是否有throw語(yǔ)句,如果沒有,收集數(shù)據(jù)進(jìn)行通知改進(jìn)
看一下具體代碼實(shí)現(xiàn):
先看一下Nuget引用:
Microsoft.CodeAnalysis
Microsoft.CodeAnalysis.Workspaces.MSBuild

代碼的具體實(shí)現(xiàn):

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代碼分析全面提升代碼質(zhì)量的一些具體實(shí)踐,分享給大家。
