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

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

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

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

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

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

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

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

代碼編譯后也有對應(yīng)Warnning提示
2、在For循環(huán)中進(jìn)行服務(wù)調(diào)用
問題:for循環(huán)中調(diào)用RPC服務(wù),每次訪問都會發(fā)起一次RPC請求,如果循環(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ù)時,給與編程提示:不建議在循環(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;
}
}
}
}
}
}
}基本的實現(xiàn)方式,和上一個差不多,唯一不同的邏輯是在實際的代碼分析過程中,AnalyzeMethodForLoop。大家可以根據(jù)自己的需要寫一下。
實際的效果:

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

代碼的具體實現(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ì)量的一些具體實踐,分享給大家。
好文章,我在看??
