🕌

C#静的解析で品質を上げよう!(Analyzer①)

2022/06/12に公開

初めまして。大手SIerで働いている少佐と言います!
「コーディング規約を守らせたい」「セキュリティリスクがあるコードをなくしたい」「よりパフォーマンスがよくなるコードにしたい」これらの悩みを抱えてる方はぜひ読んでみてください。

今回扱うのは「.NET Compiler Platform(通称:Roslyn)」(以下、Roslynと記載)です。

Roslynとは

公式サイトにはこのように書かれています。

.NET コンパイラ プラットフォーム("Roslyn")は、オープンソースC#およびVisual Basicコンパイラと豊富なコード分析APIを備えています。MicrosoftがVisual Studioの実装に使用しているのと同じAPIを使用してコード分析ツールを構築できます

詳しく話すと取っつきにくい内容になってしまうので、ここではVisualStudioで使用されているものを自分で構築が可能ということだけ意識してください。

Roslynでできることとは

  • ソースコード解析
  • ソースコード生成
  • エラーの検出
  • リファクタリング
    他にもいくつかありますが、まずはここまでの説明をしていきます。

Roslynの使用方法

拡張機能・Nugetで公開したい場合

Visual Studio Installerにて
ワークロード > VisualStudio拡張機能の開発にチェック
個別のコンポーネント > .NET Compiler Platform SDKにチェックし、インストールしてください。

このインストールによって

  1. プロジェクトテンプレートとして選択が可能になる
  2. Syntax Visualizerが使用可能になる(説明は後述)

Analyzer With Code Fixのテンプレートを選択すると、5つのプロジェクトが生成されます。

  1. Analyzerプロジェクト
      ソースコード解析用のプロジェクト
     解析・エラーの検出を行う
  2. CodeFixプロジェクト
      コード修正用のプロジェクト
     VisualStudioの修正提案を作成する
  3. Packageプロジェクト
      AnalyzerとCodeFixのNuGetパッケージを生成する
  4. Testプロジェクト
      単体テスト用のプロジェクト
  5. Vsixプロジェクト
      Analyzerが読み込まれたVisualStudioを別インスタンスで起動するプロジェクト
     いわゆるVisualStudio拡張の確認用

コマンドラインで使用したい場合

そもそもコマンドラインで使用したいケースを説明すると、

  1. CI/CDで定期実行して、コードの品質を高めたい要件があるケース
  2. VisualStudio2015未満のプロジェクトに対して解析したいケース

Roslynを使用する際にVisualStudio2013上でもコンパイラとして動作はします。
しかし、内部的に異なる部分があるため文字化けが起きるなど使い勝手がかなり悪くなります。
そのため、VisualStudio2010や2013で保守しているソースコードは、コマンドラインアプリでの解析を推奨します。

前置きが長くなりましたが、
stand-aloneプロジェクトテンプレートを選択すると、コンソールアプリとして作成が可能です。
具体的な実装方法に大差がないので、Tipsとして別途記載します。

Analyzerについて

必要になる基本用語の説明

ここが静的解析において最初に躓く可能性がある部分です。
大きな単位から順に記載するので、まずは大まかに関係性を認識すると良いです。

  • SyntaxTree:
     構文ツリーのこと
     字句および構文構造を表します
     解析をする際のスタート地点になります
     RoslynがSyntaxTreeを作成してくれるために容易に解析が可能になります

  • SyntaxNode:
     構文ノードのこと
     構文ツリーのプライマリ要素の1つ
     宣言、ステートメント、句、および式などの構文構造を表します
     (例:IfStatementSyntax、MethodDeclarationSyntax)
     解析時に最もよく使用する要素ですが、多数あるSyntaxNodeをすべて覚える必要はありません

  • SyntaxToken:
     構文トークンのこと
     コードの最小の構文フラグメントを表す、言語文法の終端です
     Tokenが他のToken・Nodeの親になることはありません
     構文トークンは、キーワード、識別子、リテラル、および句読点で構成されます
     (例:IfKeyword、OpenParenToken)
     括弧を強制的に使わせたい場合などに使用します

  • SyntaxTrivia:
     構文トリビアのこと
     空白、コメント、およびプリプロセッサ ディレクティブなど、
     コードを通常に理解するためにはさほど重要ではないソース テキストの部分を表します
     (例:WhiteSpaceTrivia)
     空白行を2行続けないようにしたい場合などに使用します

  • Span:
     Node、Token、Triviaのソーステキスト内の各自の位置と構成文字数を表します
     VisualStudio上でエラーを表示させる箇所にも関係するプロパティです

  • SyntaxKind:
     構文の種類を表します
     特定のSyntaxNodeだけを解析したい場合などによく使います
     解析する際には構文の種類を意識しておかなければ例外になりうるので、意識が必要です

まずは大きくこの6つに関して認識していれば問題ないです。
次にいくつかの実装を明示し、必要なメソッド・プロパティ等を説明します。

Analyzerの作成

Analyzerは説明することが多いので、細かく分けて解説していきます。

DiagnosticAnalyzerクラス

新しいAnalyzerを作る際は.csファイルを追加していきます。
今回はLowerCaseAnalzyerという名称で新規作成しているケースを想定しています。

まず、[DiagnosticAnalyzer(LanguageNames.CSharp)]属性は、このクラスがAnalyzerを提供するという意味を表します。

namespace Roslyn
{
  [DiagnosticAnalyzer(LanguageNames.CSharp)]
  public class LowerCaseAnalyzer : DiagnosticAnalyzer
  {
    ……省略……
  }
}

LowerCaseAnalyzerはDiagnosticAnalyzer抽象クラスを継承しています。
この抽象クラスのメンバーを見ると、abstractなものは以下の2つになります。

namespace Microsoft.CodeAnalysis.Diagnostics
{
    public abstract class DiagnosticAnalyzer
    {
        public abstract ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
        public abstract void Initialize(AnalysisContext context);
    }
}

SupportedDiagnosticsプロパティ

Diagnostics(診断)機能に関する説明を外部から取得するためのプロパティです。
説明はDiagnosticsDescriptorクラスによって指定するが、必要な情報はこのクラスのコンストラクタで与えることになります。

パラメータ一覧

  • id
     Diagnosticsを一意に識別するためのID
     CodeFixProviderクラス(修正提案用のクラス)からも参照する必要がある
     そのため、定数フィールドで定義することが多い

  • title
     Diagnostics自体のタイトル

  • messageFormat
     Diagnosticsの詳細メッセージを生成するためのフォーマット

  • category
     Diagnosticsのカテゴリ

  • defaultSeverity
     既定の重要度
     Errorを指定するとコンパイルエラー扱いにすることも可能
     プロジェクト単位で重要度の設定は可能

  • isEnabledByDefault
     既定で有効化するかどうか

  • helpLink
     Diagnosticsの詳細な情報があるURLを記載
     ユーザーがエラーの詳細・発生条件を理解するために必要になるもの

  • description
     追加説明(省略可能)

  • customTags
     可変長引数として指定するタグ(省略可能)

title/messageFormat/descriptionの3つは、多言語化のためのLocalizableResourceStringクラスを利用可能です(string型も可能)。

いろいろ書きましたが、これらの情報が反映される場所を認識していれば問題ありません。
VisualStudioのエラー一覧を見ると、説明、重要度やリンクが存在していることがわかると思います。
つまり、SupportedDiagnosticsはエラー一覧に関連するということです。

Initializeメソッド

このメソッドは引数にAnalysisContext型のインスタンスを取ります。
この引数(context)が持つRegisterXXXActionメソッドに検知したいルールを実装することで、エラーや警告などを検知することができます。

1つのAnalyzer内で複数のActionを登録することもでき、また複数のAnalyzerに分けることも可能です。
⚠注意として、Analyzerはそれぞれ並列に実行されうるため、Analyzer間での状態の共有は不可能です(状態を共有したい場合は同一Analyzerに記載が必要)。

同一Analyzerに複数のActionを登録した場合は、RegisterXXXActionメソッドの種類によって優先順位が決まっています。
ただし、優先順位が同じ場合は実行順は不定になるので、状態を共有させることはやめておいた方がよいです。

どのRegisterXXXActionメソッドを使用するかは、どういったActionを実装したいかによってかわってきます。
例えば、クラス名が大文字始まりかどうか判断したい場合はRegisterSymbolActionメソッドを呼び出す必要があります。

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class LowerCaseAnalyzer : DiagnosticAnalyzer
{
  ……省略……

  public override void Initialize(AnalysisContext context)
  {
    // 第一引数:実行したいActionを登録
    // 第二引数以降:Actionの引数に渡す種類を決める
    context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
  }

  private static void AnalyzeSymbol(SymbolAnalysisContext context)
  {
    // SymbolKind.NamedTypeで登録したので対応するINamedTypeSymbolオブジェクトが渡される
    var namedTypeSymbol = (INamedTypeSymbol)context.Symbol;
    if (char.IsLower(namedTypeSymbol.Name.ToCharArray()[0]))
    {
      var diagnostic = Diagnostic.Create(Rule, namedTypeSymbol.Locations[0], namedTypeSymbol.Name);
      context.ReportDiagnostic(diagnostic);
    }
  }
}

ここまでで、Analyzerの基本的な部分は説明が終わりました。
ただこれだけだと具体的にどのように実装していくのが良いか。。とわからないと思います。
次回記事にてSyntaxVisualizerを用いて、実装の考え方について解説していきます。

Discussion