TIG コアテクノロジーユニットの山田です。ソースやドキュメントを解析してファクト分析を行う仕事をしています。
今回は循環的複雑度の計算を題材に、Roslynを用いてVB.NETに対する解析コード1 をC#で書いてみました。本記事では、メソッド単位の循環的複雑度の計算を、クエリメソッドを用いて構文木を走査する方法とVisualBasicSyntaxWalkerを継承したクラスを用いて構文木を走査する方法の2通りの方法を紹介します。それぞれの方法の特徴は以下の通りです。
クエリメソッドを用いる方法
あるノードの子ノードや子孫ノード、祖先ノードをクエリメソッドにより列挙して処理する形になります
いわゆるXMLに対するXPathやHTMLに対するCSSセレクタのようなインタフェースです
VisualBasicSyntaxWalkerを継承したクラスを用いる方法
どの型のノードを訪問した際にどんな処理をするかというのを記述する方式となります
いわゆるVisitorパターンです
Roslynとは Roslynとは.NET Compiler Platformのコードネームで、コード解析ツール構築のためのAPIを持つコンパイラです2 3 。Roslynが提供しているAPIを用いることで簡単に静的解析を行うことが出来ます。Microsoft公式で開発されているため、安心感がありますね。
循環的複雑度について 循環的複雑度とはコードの品質を表す指標の一つで、循環的複雑度が高いほど複雑な構造であるといえます。4 5
循環的複雑度は制御フローグラフのノード数nとエッジ数eから e - n + 2
という形で計算できます。 制御フローグラフに分岐が一つもない場合、この値は1となり、分岐が増えるごとに値が増えていくため、分岐の数 + 1という形で簡単に求めることができます。
今回の記事では、If
, ElseIf
, For
, For Each
, While
, Case
, Catch
の数を数えて循環的複雑度を算出6 することにします。 なお、IIf
は一見三項演算子のようですが、扱いとしてはただの関数なので今回は対象としませんでした。自前で算出すると自由に計算ロジックを変えられるので、プロジェクトのルールに応じてカスタマイズできますね。
具体的には以下のようにカウントします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 Public Class A Public Shared Sub Main() Dim i As Integer = 1 If i = 1 Then Console.WriteLine("A" ) ElseIf i = 0 Then Console.WriteLine("B" ) Else Console.WriteLine("C" ) End If While i > 0 i -= 1 End While For index As Integer = 0 To 1 Next index Dim lst As New List(Of String ) From {"A" , "B" , "C" } For Each elem As String In lst Console.WriteLine("{0}" , elem) Next Try Throw New Exception() Catch ex As Exception Console.WriteLine("Catch" ) End Try Dim s As String = If (Nothing , "hoge" ) i = If (0 = 0 , 1 , 2 ) i = IIf(0 = 0 , 1 , 2 ) Select Case 2 Case 0 Console.WriteLine("0" ) Case 1 Console.WriteLine("1" ) Case Else Console.WriteLine("Else" ) End Select End Sub End Class
環境構築 下記ツールをインストールします
.NET 6.0 SDK
Visual Studio Code
プロジェクト作成 下記コマンドでプロジェクトを作成します。(-o
で指定しているのはプロジェクト名です) これによりカレントフォルダに RoslynBlog.csproj
ファイルが作成されます。
1 dotnet new console -o RoslynBlog
デバッガの設定 さて、ここまでで C# の開発環境とプロジェクトの作成が済みましたが、まだデバッガが使用出来ない状態です。続いてデバッガの設定をしていきましょう。 といっても手順は簡単で、Visual Studio Codeのデバッグパネルを開き、 create a launch.json file
をクリックするだけです。 これにより、launch.json
が作成され、このようにデバッグが可能となります。
これで無事にデバッグできるようになりました。
開発 パッケージ追加 まず今回使用するパッケージを追加します。
1 2 3 4 5 6 dotnet add package Microsoft.Build.Locator --version 1.4.1 dotnet add package Microsoft.CodeAnalysis --version 4.2.0 dotnet add package Microsoft.CodeAnalysis.VisualBasic --version 4.2.0 dotnet add package Microsoft.CodeAnalysis.VisualBasic.Workspaces --version 4.2.0 dotnet add package Microsoft.CodeAnalysis.Workspaces.Common --version 4.2.0 dotnet add package Microsoft.CodeAnalysis.Workspaces.MSBuild --version 4.2.0
RoslynBlog.csproj
ファイルをエディタで開くことで、依存パッケージが追加されていることが確認できます。言語とビルドツールが統合されており便利ですね。
計算対象のプロジェクトの読み込み MSBuildWorkspace
を用いてプロジェクトを読み込み、各ドキュメントの各メソッドごとの循環的複雑度を計算して返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 static async Task Main (){ MSBuildLocator.RegisterDefaults(); using var workspace = MSBuildWorkspace.Create(); var projectPath = @"../TestProjVB/TestProjVB.vbproj" ; var project = await workspace.OpenProjectAsync(projectPath); foreach (var document in project.Documents) { var syntaxTree = await document.GetSyntaxTreeAsync(); if (syntaxTree == null ) { continue ; } var cyclomaticComplexityByQueryMethod = CalcCyclomaticComplexityByQueryMethod(syntaxTree); foreach (var (methodName, cyclomaticComplexity) in cyclomaticComplexityByQueryMethod) { Console.WriteLine("CalcCyclomaticComplexityByQueryMethod({0})={1}" , methodName, cyclomaticComplexity); } var cyclomaticComplexityBySyntaxWalker = CalcCyclomaticComplexityBySyntaxWalker(syntaxTree); foreach (var (methodName, cyclomaticComplexity) in cyclomaticComplexityBySyntaxWalker) { Console.WriteLine("CalcCyclomaticComplexityBySyntaxWalker({0})={1}" , methodName, cyclomaticComplexity); } } }
クエリメソッドによる循環的複雑度の計算 こちら で紹介されているクエリメソッドを用いて循環的複雑度を計算します。 循環的複雑度の加算対象となるノードは以下のように判定できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static bool IsDecisionNode (SyntaxNode node ){ if (node.IsKind(SyntaxKind.CaseElseStatement)) { return false ; } return node is IfStatementSyntax || node is ElseIfStatementSyntax || node is WhileStatementSyntax || node is ForStatementSyntax || node is ForEachStatementSyntax || node is CatchStatementSyntax || node is CaseStatementSyntax || node is TernaryConditionalExpressionSyntax || node is BinaryConditionalExpressionSyntax; }
上記メソッドを用いて、メソッド毎の循環的複雑度は下記のように計算できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static Dictionary<string , int > CalcCyclomaticComplexityByQueryMethod (SyntaxTree syntaxTree ){ var cyclomaticComplexityDict = new Dictionary<string , int >(); foreach (var methodBlockSyntax in syntaxTree.GetRoot().DescendantNodes().OfType<MethodBlockSyntax>()) { var methodStatementSyntax = methodBlockSyntax.ChildNodes().OfType<MethodStatementSyntax>().First(); var methodName = methodStatementSyntax.Identifier.Text; var methodCyclomaticComplexity = methodBlockSyntax.DescendantNodes().Where(node => CyclomaticComplexity.IsDecisionNode(node)).Count() + 1 ; cyclomaticComplexityDict[methodName] = methodCyclomaticComplexity; } return cyclomaticComplexityDict; }
SyntaxWalkerによる循環的複雑度の計算 構文木を走査しながら循環的複雑度を計算するSyntaxWalkerクラスを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 internal class CyclomaticComplexitySyntaxWalker : VisualBasicSyntaxWalker { public Dictionary<string , int > CyclomaticComplexityDict { get ; } = new Dictionary<string , int >(); private string _currentMethodName = "" ; public override void VisitMethodStatement (MethodStatementSyntax node ) { _currentMethodName = node.Identifier.Text; CyclomaticComplexityDict[_currentMethodName] = 1 ; base .VisitMethodStatement(node); } public override void VisitIfStatement (IfStatementSyntax node ) { CyclomaticComplexityDict[_currentMethodName] += 1 ; base .VisitIfStatement(node); } public override void VisitElseIfStatement (ElseIfStatementSyntax node ) { CyclomaticComplexityDict[_currentMethodName] += 1 ; base .VisitElseIfStatement(node); } public override void VisitWhileStatement (WhileStatementSyntax node ) { CyclomaticComplexityDict[_currentMethodName] += 1 ; base .VisitWhileStatement(node); } public override void VisitForStatement (ForStatementSyntax node ) { CyclomaticComplexityDict[_currentMethodName] += 1 ; base .VisitForStatement(node); } public override void VisitForEachStatement (ForEachStatementSyntax node ) { CyclomaticComplexityDict[_currentMethodName] += 1 ; base .VisitForEachStatement(node); } public override void VisitCatchStatement (CatchStatementSyntax node ) { CyclomaticComplexityDict[_currentMethodName] += 1 ; base .VisitCatchStatement(node); } public override void VisitCaseStatement (CaseStatementSyntax node ) { if (!node.IsKind(SyntaxKind.CaseElseStatement)) { CyclomaticComplexityDict[_currentMethodName] += 1 ; } base .VisitCaseStatement(node); } public override void VisitTernaryConditionalExpression (TernaryConditionalExpressionSyntax node ) { CyclomaticComplexityDict[_currentMethodName] += 1 ; base .VisitTernaryConditionalExpression(node); } public override void VisitBinaryConditionalExpression (BinaryConditionalExpressionSyntax node ) { CyclomaticComplexityDict[_currentMethodName] += 1 ; base .VisitBinaryConditionalExpression(node); } }
作成した CyclomaticComplexitySyntaxWalker
クラス用いて、下記のように循環的複雑度が計算できます。
1 2 3 4 5 6 7 8 static Dictionary<string , int > CalcCyclomaticComplexityBySyntaxWalker (SyntaxTree syntaxTree ){ var walker = new CyclomaticComplexitySyntaxWalker(); walker.Visit(syntaxTree.GetRoot()); return walker.CyclomaticComplexityDict; }
まとめ 今回はRoslynのSyntax APIを使い、VB.NETのプロジェクトを解析し循環的複雑度の計算をしてみました。 Roslynを使うことで(Solutionや)Projectを簡単に読み込み、解析することができることがわかりました。 C#の循環的複雑度もノードの型が違うだけで、ほぼ同じ形で作ることができます。
今回の記事とは関係ないですが、C#だと Scripting API により、C#のコードをevalすることが可能なのですが、VB.NETのScripting APIは開発中止になった ようなので今後使える見込みはなさそうです。