隨着團隊愈來愈多,愈來愈大,需求更迭愈來愈快,天天提交的代碼變動由原先的2位數,暴漲到3位數,天天幾百次代碼Check In,補丁提交,大量的代碼審查消耗了大量的資源投入。express
如何確保提交代碼的質量和提測產品的質量,這兩個是很是大的挑戰。編程
工欲善其事,必先利其器。在上述需求背景下,今年咱們準備用工具和技術,全面把控並提高代碼質量和產品提測質量。即:api
1. 代碼質量提高:經過自定義代碼掃描規則,將有問題的代碼、不符合編碼規則的代碼掃描出來,禁止簽入數據結構
2. 產品提測質量:經過單元測試覆蓋率和執行經過率,嚴控產品提交質量,覆蓋率和經過率達不到標準,沒法提交測試。async
準備用2篇文章,和你們分享咱們是如何提高代碼質量和產品提測質量的。今天分享第一篇:經過Roslyn代碼分析全面提高代碼質量。ide
1、什麼是Roslyn工具
Roslyn 是微軟開源的 .NET 編譯平臺(.NET Compiler Platform)。 編譯平臺支持 C# 和 Visual Basic 代碼編譯,並提供豐富的代碼分析 API。oop
利用Roslyn能夠生成代碼分析器和代碼修補程序,從而發現和更正編碼錯誤。 性能
分析器不只理解代碼的語法和結構,還能檢測應更正的作法。 代碼修補程序建議一處或多處修復,以修復分析器發現的編碼錯誤。單元測試
咱們寫下面一堆代碼,Roslyn編譯器會有以下提示:
經過編寫分析器和代碼修補程序,主要服務如下場景:
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作一些「不符合編程規範要求(團隊自定義的)」的代碼分析。
2、基於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。你們能夠根據本身的須要寫一下。
實際的效果:
還有幾個代碼檢查場景,基本都是一樣的實現思路,再次不一一羅列了。
在這裏還能夠自動完成代理修補程序,這個地方咱們還在研究中,可能每一個業務代碼的場景不一樣,很難給出一個通用的改進代碼,因此這個地方等後續咱們完成後,再和你們分享。
3、經過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