.NET Core技術研究-經過Roslyn代碼分析技術規範提高代碼質量

隨着團隊愈來愈多,愈來愈大,需求更迭愈來愈快,天天提交的代碼變動由原先的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編譯器會有以下提示: 

  

 經過編寫分析器和代碼修補程序,主要服務如下場景:  

  • 強制執行團隊編碼標準(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作一些「不符合編程規範要求(團隊自定義的)」的代碼分析。

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

相關文章
相關標籤/搜索