October 2015

Volume 30 Number 10

コード分析 - Roslyn の統合コード分析を備えたライブラリのビルドと NuGet への配置

Alessandro Del Del | October 2015 | サンプル コードのダウンロード: C#   VB

マイクロソフトの .NET コンパイラ プラットフォーム (別名 "Roslyn" コード ベース) は、C# と Visual Basic のオープン ソース コンパイラを提供し、さまざまな機能を公開します。中でも機能豊富なコード分析 API は、Visual Studio 2015 のコード エディターに統合されるライブ分析の規則を開発者がビルドできるようにします。.NET コンパイラ プラットフォームにより、開発者は、ドメイン固有のカスタム コード アナライザーとリファクタリングを作成できるため、コード入力の途中で Visual Studio がコードの問題を検出し、警告やエラー メッセージを表示できるようになります。.NET コンパイラ プラットフォームの大きなメリットは、コード アナライザーを API にバンドルできるところにあります。たとえば、ライブラリや再利用可能なユーザー コントロールをビルドする場合、そのライブラリと一緒にアナライザーをリリースして、開発者のコーディング エクスペリエンスを向上させることができます。

今回は、オンライン配置用にライブラリとアナライザーを NuGet パッケージにバンドルする方法と、開発する API に Roslyn の統合コード分析を提供する方法を説明します。そのため、.NET コンパイラ プラットフォームの概念と、コード アナライザーの作成方法についての基礎知識が必要になります。これらのトピックは、以前の MSDN マガジンで Alex Turner が執筆した「C# と Visual Basic: Roslyn を使用した API 向けライブ コード アナライザーの作成」(msdn.microsoft.com/magazine/dn879356) と「C# - Roslyn アナライザーへのコード修正の追加」(msdn.microsoft.com/magazine/dn904670) で取り上げられているため、本コラムの前にこちらの 2 つのコラムを一読されることを強くお勧めします。

カスタム API のシミュレーションを行うサンプル ライブラリの準備

まず、カスタム API のシミュレーションを行うクラス ライブラリを作成します。今回のサンプル ライブラリは、RSS フィードから一般的な情報を取得して、フィード項目のコレクションを返す、シンプルなパブリック メソッドを公開します。Visual Studio 2015 で、C# または Visual Basic のどちらかを使って、FeedLibrary という新しいポータブル クラス ライブラリを作成し、最小ターゲットを Windows 8.1、Windows Phone 8.1、および Microsoft .NET Framework 4.5.1 にします。ターゲットをこのように設定すると、ライブラリは追加要件なしで Async/Await パターンも利用できるようになります。

Class1.vb (Class1.cs) ファイルの名前を FeedItem.vb (FeedItem.cs) に変更します。このクラスの C# コードを図 1 に示し、Visual Basic コードを図 2 に示します。

図 1 RSS フィードから一般的な項目を取得するクラスの C# 実装

using System.Net.Http;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace FeedLibrary
{
  // Represent a single content in the RSS feed
  public class FeedItem
  {
    // Properties representing information,
    // which is common to any RSS feed
    public string Title { get; set; }
    public string Author { get; set; }
    public string Description { get; set; }
    public DateTimeOffset PubDate { get; set; }
    public Uri Link { get; set; }
    // Return a collection of FeedItem objects from a RSS feed
    public static async Task<IEnumerable<FeedItem>> ParseFeedAsync(
      string feedUrl)
    {
      var client = new HttpClient();
      // Download the feed content as a string
      var result = await client.GetStringAsync(new Uri(feedUrl));
      // If no result, throw an exception
      if (result == null)
      {
        throw new InvalidOperationException(
          "The specified URL returned a null result");
      }
      else
      {
        // LINQ to XML: Convert the returned string into an XDocument object
        var doc = XDocument.Parse(result);
        var dc = XNamespace.Get("http://purl.org/dc/elements/1.1/");
        // Execute a LINQ query over the XML document
        // and return a collection of FeedItem objects
        var query = (from entry in doc.Descendants("item")
                     select new FeedItem
                     {
                         Title = entry.Element("title").Value,
                         Link = new Uri(entry.Element("link").Value),
                         Author = entry.Element(dc + "creator").Value,
                         Description = entry.Element("description").Value,
                         PubDate = DateTimeOffset.Parse(
                           entry.Element("pubDate").Value,
                     System.Globalization.CultureInfo.InvariantCulture)
                     });
        return query;
      }
    }
  }
}

図 2 RSS フィードから一般的な項目を取得するクラスの Visual Basic 実装

Imports System.Net.Http
Imports <xmlns:dc="http://purl.org/dc/elements/1.1/">
'Represent a single content in the RSS feed
Public Class FeedItem
  'Properties representing information
  'which is common to any RSS feed
  Public Property Title As String = String.Empty
  Public Property Author As String = String.Empty
  Public Property Description As String = String.Empty
  Public Property PubDate As DateTimeOffset
  Public Property Link As Uri
  'Return a collection of FeedItem objects from a RSS feed
  Public Shared Async Function ParseFeedAsync(feedUrl As String) As _
    Task(Of IEnumerable(Of FeedItem))
    Dim client As New HttpClient
    'Download the feed content as a string
    Dim result = Await client.GetStringAsync(New Uri(feedUrl, UriKind.Absolute))
    'If no result, throw an exception
    If result Is Nothing Then
       Throw New InvalidOperationException(
         "The specified URL returned a null result")
    Else
      'LINQ to XML: Convert the returned string
      'into an XDocument object
      Dim document = XDocument.Parse(result)
      'Execute a LINQ query over the XML document
      'and return a collection of FeedItem objects
      Dim query = From item In document...<item>
                  Select New FeedItem With {
                  .Title = item.<title>.Value,
                  .Author = item.<dc:creator>.Value,
                  .Description = item.<description>.Value,
                  .PubDate = DateTimeOffset.Parse(item.<pubDate>.Value),
                  .Link = New Uri(item.<link>.Value)}
      Return query
    End If
  End Function
End Class

コードは非常にシンプルです。指定した RSS フィードの URL からシンジケート コンテンツをダウンロードし、フィード項目ごとに FeedItem クラスのインスタンスを作成して、最後に項目の新しいコレクションを返します。このライブラリを使用するには、C# では以下のように静的メソッド ParseFeedAsyncAsync を呼び出します。

// Replace the argument with a valid URL
var items = await FeedItem.ParseFeedAsyncAsync("http://sampleurl.com/rss");

Visual Basic では以下のように呼び出します。

'Replace the argument with a valid URL
Dim items = Await FeedItem.ParseFeedAsyncAsync("http://sampleurl.com/rss")

この呼び出しでは、IEnumerable<FeedItem> が返ります。これ以降、必要に応じてこのコレクションを利用します。この時点でソリューション構成を [Release] に選択してプロジェクトをビルドすると、FeedLibrary.dll というライブラリが Visual Studio 2015 によって作成されます。このライブラリは後ほど使用します。

Roslyn アナライザーの作成

今度は、カスタム API 向けにドメイン固有のライブ分析規則を提供する Roslyn アナライザーを作成します。このアナライザーは、Uri.IsWellFormedUriString メソッドを使って、ParseFeedAsyncAsync メソッドの引数として指定される URL の形式が正しいかどうかを検知し、URL の形式が正しくなければ、入力中に警告を表示します。当然、URL が無効かどうかを検出する方法はたくさんありますが、今回はシンプルさを優先してこの方法を使用します。同じシンプルさを理由に、アナライザーはライブ分析によって警告を行いますが、コードの修正は提供しません。コードの修正については、演習として残しておきます。そこで、以下の手順を実行します。

  1. ソリューション エクスプローラーで、ソリューション名を右クリックし、[追加]、[新しいプロジェクト] の順にクリックします。
  2. プロジェクト テンプレートの一覧にある [‏Extensibility] ノードで [Analyzer with Code Fix (NuGet + VSIX)] を選択します。既定では、[Extensibility] ノードが表示されません。Analyzer with Code Fix テンプレート プロジェクトを利用するには、最初に .NET Compiler Platform SDK をダウンロードする必要があります。[新しいプロジェクトの追加] ダイアログで「Analyzers」を検索すると、テンプレート プロジェクトが表示され、.NET Compiler Platform SDK をダウンロードできます。
  3. 新しいアナライザーに「FeedLibraryAnalyzer」という名前を付け、[OK] をクリックします。
  4. 新しいプロジェクトの準備が整ったら、CodeFixProvider.cs (.vb) ファイルを削除します。

DiagnosticAnalyzer.cs (.vb) ファイルでは、最初にコーディング エクスペリエンス中にアナライザーを特定する文字列を指定します。アナライザーの実装をシンプルにするため、今回はアナライザーをローカライズする必要はないものとして、LocalizableString オブジェクトとリソース ファイルではなく、標準の文字列を使用します。C# では、DiagnosticId、Title、Message、Description、および Category を次のように書き換えます。

public const string DiagnosticId = "RSS001";
internal static readonly string Title = "RSS URL analysis";
internal static readonly string MessageFormat = "URL is invalid";
internal static readonly string Description =
  "Provides live analysis for the FeedLibrary APIs";
internal const string Category = "Syntax";

Visual Basic では、以下のようにします。

Public Const DiagnosticId = "RSS001"
Friend Shared ReadOnly Title As String = "RSS URL analysis"
Friend Shared ReadOnly MessageFormat As String = "URL is invalid"
Friend Shared ReadOnly Description As String =
  "Provides live analysis for the FeedLibrary APIs"
Friend Const Category = "Syntax"

diagnostic severity は既定で Warning となり、今回の例では適切な選択肢となるため変更しません。ここからは、分析ロジックに目を向けます。アナライザーでは、コードから ParseFeedAsync メソッドを呼び出しているかどうかをチェックする必要があります。呼び出していたら、指定された URL の形式が正しいかどうかをチェックします。Syntax Visualizer を利用すると、ParseFeedAsync メソッドの呼び出しが、InvocationExpressionSyntax 型のオブジェクトにマップされた InvocationExpression によってどのように表現されるのかが理解できます (図 3 参照)。

正しい構文ノード表現を検索できる Syntax Visualizer
図 3 正しい構文ノード表現を検索できる Syntax Visualizer

これにより、アナライザーは InvocationExpressionSyntax 型のオブジェクトにのみ注目します。このオブジェクトを見つけると、オブジェクトに関連付けられた式を、MemberAccessExpressionSyntax 型のオブジェクトに変換します。このオブジェクトが、メソッド呼び出しについての情報を含みます。変換に成功すると、アナライザーはそのメソッドが ParseFeedAsync かどうかをチェックしてから、最初の引数を取得し、その値に対してライブ分析を実行します。この動作は、AnalyzeMethod という、SyntaxNode レベルで機能する新しいメソッドにより実現されます。C# でのこのメソッドを 図 4、Visual Basic でのこのメソッドを 図 5 に示します。

図 4 C# での引数 ParseFeedAsync の問題検出

private static void AnalyzeMethodInvocation(SyntaxNodeAnalysisContext context)
{
  // Convert the current syntax node into an InvocationExpressionSyntax,
  // which represents a method call
  var invocationExpr = (InvocationExpressionSyntax)context.Node;
  // Convert the associated expression into a MemberAccessExpressionSyntax,
  // which represents a method's information
  // If the expression is not a MemberAccessExpressionSyntax, return
  if (!(invocationExpr.Expression is MemberAccessExpressionSyntax))
  {
    return;
  }
  var memberAccessExpr = (MemberAccessExpressionSyntax)invocationExpr.Expression;
  // If the method name is not ParseFeedAsync, return
  if (memberAccessExpr?.Name.ToString() != "ParseFeedAsync") { return; }
  // If the method name is ParseFeedAsync, check for the symbol
  // info and see if the return type matches
  var memberSymbol = context.SemanticModel.
                     GetSymbolInfo(memberAccessExpr).
                     Symbol as IMethodSymbol;
  if (memberSymbol == null) { return; }
  var result = memberSymbol.ToString();
  if (memberSymbol?.ReturnType.ToString() !=
  "System.Threading.Tasks.Task<
    System.Collections.Generic.IEnumerable<FeedLibrary.FeedItem>>")
  {
      return;
  }
  // Check if the method call has the required argument number
  var argumentList = invocationExpr.ArgumentList;
  if (argumentList?.Arguments.Count != 1) {
      return; }
  // Convert the expression for the first method argument into
  // a LiteralExpressionSyntax. If null, return
  var urlLiteral = (LiteralExpressionSyntax)invocationExpr.ArgumentList.
      Arguments[0].Expression;
  if (urlLiteral == null) { return; }
  // Convert the actual value for the method argument into string
  // If null, return
  var urlLiteralOpt = context.SemanticModel.GetConstantValue(urlLiteral);
  var urlValue = (string)urlLiteralOpt.Value;
  if (urlValue == null) { return; }
  // If the URL is not well-formed, create a diagnostic
  if (Uri.IsWellFormedUriString(urlValue, UriKind.Absolute) == false)
  {
    var diagn = Diagnostic.Create(Rule, urlLiteral.GetLocation(),
      "The specified parameter Is Not a valid RSS feed");
    context.ReportDiagnostic(diagn);
  }
}

図 5 Visual Basic での引数 ParseFeedAsync の問題検出

Private Sub Shared AnalyzeMethodInvocation(context As SyntaxNodeAnalysisContext)
  'Convert the current syntax node into an InvocationExpressionSyntax
  'which represents a method call
  Dim invocationExpr = CType(context.Node, InvocationExpressionSyntax)
  'Convert the associated expression into a MemberAccessExpressionSyntax
  'which represents a method's information
  'If the expression Is Not a MemberAccessExpressionSyntax, return
  If TypeOf invocationExpr.Expression IsNot MemberAccessExpressionSyntax Then Return
  Dim memberAccessExpr = DirectCast(invocationExpr.Expression,
    MemberAccessExpressionSyntax)
  'If the method name Is Not ParseFeedAsync, return
  If memberAccessExpr?.Name.ToString <> "ParseFeedAsync" Then Return
  'If the method name is ParseFeedAsync, check for the symbol info
  'and see if the return type matches
  Dim memberSymbol = TryCast(context.SemanticModel.
      GetSymbolInfo(memberAccessExpr).Symbol, IMethodSymbol)
  If memberSymbol Is Nothing Then Return
  Dim result = memberSymbol.ToString
  If Not memberSymbol?.ReturnType.ToString =
    "System.Threading.Tasks.Task(Of System.Collections.Generic.IEnumerable(
    Of FeedLibrary.FeedItem))"
    Then Return
  'Check if the method call has the required argument number
  Dim argumentList = invocationExpr.ArgumentList
  If argumentList?.Arguments.Count <> 1 Then Return
  'Convert the expression for the first method argument into
  'a LiteralExpressionSyntax. If null, return
  Dim urlLiteral =
    DirectCast(invocationExpr.ArgumentList.Arguments(0).GetExpression,
      LiteralExpressionSyntax)
  If urlLiteral Is Nothing Then Return
  'Convert the actual value for the method argument into string
  'If null, return
  Dim urlLiteralOpt = context.SemanticModel.GetConstantValue(urlLiteral)
  Dim urlValue = DirectCast(urlLiteralOpt.Value, String)
  If urlValue Is Nothing Then Return
  'If the URL Is Not well-formed, create a diagnostic
  If Uri.IsWellFormedUriString(urlValue, UriKind.Absolute) = False Then
     Dim diagn = Diagnostic.Create(Rule, urlLiteral.GetLocation,
       "The specified parameter Is Not a valid RSS feed")
     context.ReportDiagnostic(diagn)
  End If
End Sub

この時点で、Initialize メソッドが新しく追加した AnalyzeMethodInvocation メソッドを呼び出すよう、C# では図 6、Visual Basic では図 7 に示すように変更します。

図 6 C# での Initialize メソッドの変更

public override void Initialize(AnalysisContext context)
{
  // Register an action when compilation starts
  context.
    RegisterCompilationStartAction((CompilationStartAnalysisContext ctx) =>
  {
    // Detect if the type metadata
    // exists in the compilation context
    var myLibraryType =
    ctx.Compilation.
    GetTypeByMetadataName("FeedLibrary.FeedItem");
    // If not, return
    if (myLibraryType == null)
        return;
    // Register an action against an InvocationExpression
    ctx.RegisterSyntaxNodeAction(AnalyzeMethodInvocation,
      SyntaxKind.InvocationExpression);
  });
}

図 7 Visual Basic での Initialize メソッドの変更

Public Overrides Sub Initialize(context As AnalysisContext)
  ' Register an action when compilation starts
  context.
    RegisterCompilationStartAction
    (Sub(ctx As CompilationStartAnalysisContext)
      'Detect if the type metadata
      'exists in the compilation context
      Dim myLibraryType =
        ctx.Compilation.
          GetTypeByMetadataName("FeedLibrary.FeedItem")
        'If not, return
        '(no reference to the library)
        If myLibraryType Is Nothing
Then Return
        'Register an action against
        'an InvocationExpression
        ctx.RegisterSyntaxNodeAction(
          AddressOf AnalyzeMethodInvocation,
          SyntaxKind.InvocationExpression)
     End Sub)
End Sub

コードでは、まず、ライブラリへの参照がプロジェクト内に存在するかどうかをチェックします。そのためには、この参照が追加されていれば現在のコンテキストに必ず存在する型の名前を引数として、Compilation.GetTypeMetadataName メソッドを呼び出します。この呼び出しで null が返ると型が存在しないため、ライブラリへの参照は追加されていません。この場合はコード分析アクションを登録してアナライザーのパフォーマンスを向上する必要はありません。ここで Visual Studio 2015 の Experimental インスタンスで F5 キーを押してアナライザーをテストします。この状態で FeedLibrary ライブラリへの参照を含む新しいプロジェクトを作成し、無効な URL を入力すると、警告が正しく行われることを確認できます (図 8 参照)。

不適切な形式の URL を指定すると行われるアナライザーの警告
図 8 不適切な形式の URL を指定すると行われるアナライザーの警告

ここまでで、API と関連するドメイン固有のコード分析規則を作成しました。次は、この 2 つを 1 つの NuGet パッケージにバンドルする方法を説明します。

API と アナライザーを含む Nuget パッケージの作成

Analyzer with Code Fix プロジェクト テンプレート向けの MSBuild の規則は、コンパイル済みのアナライザーを含む NuGet パッケージの生成を自動化します。このパッケージを NuGet リポジトリに公開することで、他の開発者と共有することができます。実際には、F5 キーを押してアナライザーをデバッグするたびに、またはアナライザー プロジェクトをビルドするときに、Visual Studio 2015 は、アナライザーの .dll ファイル (今回の例では FeedLibraryAnalyzer.dll) と、そのアナライザーを含む再頒布可能な NuGet パッケージをリビルドします。

ビルド プロセスでは VSIX パッケージも生成されます。このパッケージを Visual Studio ギャラリーに公開することができ、Visual Studio 2015 の実験的なインスタンスの内部でアナライザーをデバッグする際にも使用されます。ただし、このパッケージは今回で扱う範囲を超えるため、ここでは取り上げません。

Roslyn の統合分析を備えたライブラリを共有する場合、プロジェクトをビルドする際に、Visual Studio 2015 が生成する NuGet パッケージにそのライブラリを追加する必要があります。ただし、その前に、アナライザー用の NuGet パッケージを作成する方法についてもう少し説明しておきます。NuGet パッケージの拡張子は .nupkg ですが、実際は .zip アーカイブです。このため、エクスプローラーの圧縮フォルダー ツール、WinZip、WinRar などの .zip アーカイブ ツールを使用すれば、NuGet パッケージのコンテンツと構造を簡単に確認できます。NuGet パッケージの中でアナライザーを配置するために設計された最も重要な項目を以下にまとめます。

  • .nuspec ファイル: パッケージのメタデータを保持します。このメタデータは、パッケージ名、バージョン、説明、作成者、ライセンス URL など、公開に必要な情報です。.nuspec ファイルは、Diagnostic.nuspec ファイルに基づいて NuGet パッケージにバンドルされます。Diagnostic.nuspec ファイルは、ソリューション エクスプローラーを使ってアナライザー プロジェクトに含まれているのを確認できます。Diagnostic.nuspec は、まもなく Visual Studio 2015 で編集できるようになる予定です。
  • tools フォルダー: Visual Studio が特定のプロジェクト用のアナライザーをインストール (Install.ps1) およびアンインストール (Uninstall.ps1) するために使用する Windows PowerShell スクリプトを保持します。
  • analyzers フォルダー: 特定のサブフォルダーに整理されたアナライザーの .dll ファイル群を保持します。言語に依存しない (すべての言語をターゲットとする) アナライザー ライブラリは、dotnet というサブフォルダー直下に保持されます。C# をターゲットとするアナライザーは dotnet\cs というサブフォルダーに、Visual Studio をターゲットとするアナライザーは dotnet\vb というサブフォルダーに保持されます。dotnet は .NET Core 向けの NuGet プロファイルを表しています。これは、ユニバーサル Windows アプリや ASP .NET 5 プロジェクトなどのプロジェクトの種類に対応しています。

他にもさまざまな項目を NuGet パッケージにバンドルできますが、ここでは Roslyn アナライザー向けに生成する代表的なパッケージに注目しているため、必須項目のみを示しています。

Visual Studio から参照できるすべてのライブラリは、lib というフォルダーに編成する必要があります。ライブラリは、さまざまなバージョンの .NET Framework や Windows Phone、Windows ランタイム、または移植可能なサブセット (Xamarin ライブラリなど) といった多様なプラットフォームをターゲットにすることができるため、lib フォルダーにはターゲットにするプラットフォームごとに 1 つのサブフォルダーを用意し、配置するライブラリのコピーを含めます。各サブフォルダーの名前は、特定のプラットフォームを表すプロファイル名と一致していなければなりません。たとえば、.NET Framework 4.5.1 と Windows 8.1 をターゲットとするライブラリの場合は、以下のような構造になります。ここで、net451 は .NET Framework 4.5.1 のプロファイル名、netcore451 は Windows ランタイム用のプロファイル名です。

lib\net451\mylib.dll
lib\netcore451\mylib.dll

プロファイル名 uap10.0 は、Windows 10 アプリをビルドするためのユニバーサル Windows プラットフォーム (UWP) をターゲットにします。NuGet のドキュメントに、サポート対象のプロファイルがすべて掲載されています。先ほど作成したサンプル ライブラリは、.NET Framework 4.5.1、Windows 8.1、Windows Phone 8.1 をターゲットとするポータブル ライブラリです。このようなターゲットの場合、プロファイル名は portable-net451+netcore451+wpa81 になります。NuGet パッケージにライブラリを含めるサブフォルダーに名前を付ける場合、それぞれの名前を使用する必要があります。手動でサブフォルダーを作成してライブラリをコピーする必要はなく、Visual Studio 内で NuGet パッケージのメタデータ (Diagnostic.nuspec ファイル) を変更すればライブラリがコピーされます。図 9 は、公開に必要となる適切な情報 (ID、タイトル、作成者、説明、ライセンスなど) で更新したメタデータと、ソース ファイルとターゲット サブフォルダーを指定するファイル ノードの新しいファイル要素を示しています。

図 9 NuGet パッケージ メタデータの編集

<?xml version="1.0"?>
<package xmlns="https://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
  <metadata>
    <id>RSSFeedLibrary</id>
    <version>1.0.0.0</version>
    <title>RSS Feed Library with Roslyn analysis</title>
    <authors>Alessandro Del Sole</authors>
    <owners>Alessandro Del Sole</owners>
    <licenseUrl>http://opensource.org/licenses/MIT</licenseUrl>
    <!-- Removing these lines as they are not needed
    <projectUrl>http://PROJECT_URL_HERE_OR_DELETE_THIS_LINE</projectUrl>
    <iconUrl>http://ICON_URL_HERE_OR_DELETE_THIS_LINE</iconUrl>-->
    <requireLicenseAcceptance>true</requireLicenseAcceptance>
    <description>Companion sample for the "Build and Deploy Libraries
    with Integrated Roslyn Code Analysis to NuGet" article
    on MSDN Magazine</description>
    <releaseNotes>First release.</releaseNotes>
    <copyright>Copyright 2015, Alessandro Del Sole</copyright>
    <tags>RSS, analyzers, Roslyn</tags>
    <frameworkAssemblies>
      <frameworkAssembly assemblyName="System" targetFramework="" />
    </frameworkAssemblies>
  </metadata>
  <files>
    <file src="*.dll" target="analyzers\dotnet\cs"
          exclude="**\Microsoft.CodeAnalysis.*;
          **\System.Collections.Immutable.*;**\System.Reflection.Metadata.*;
          **\System.Composition.*" />
    <file src="tools\*.ps1" target="tools\" />
    <file src="lib\FeedLibrary.dll" target="lib\portable-net451+netcore451+wpa81\"/>
  </files>
</package>

src 属性はライブラリのソースの場所を示し、target 属性は、ターゲットのサブフォルダーを適切なプロファイル名に基づいて指定します。今回は、Visual Studio が lib フォルダー内で FeedLibrary.dll ライブラリを検索するため、lib フォルダーをカレント ディレクトリに作成する必要があります。target 属性は、コンパイル済みのアナライザーを保持するフォルダーを指定します。通常、選択するビルド構成によって、Release フォルダーまたは Debug フォルダーになります。今回の例では、Release フォルダー直下に lib というフォルダーを作成してから、FeedLibrary.dll (サンプル ライブラリの初回コンパイル時に生成) を lib フォルダーにコピーします。この作業が完了したら、ソリューションをリビルドするだけで、Visual Studio がライブラリと Roslyn アナライザーの両方を含む更新済みの NuGet パッケージを生成します。

プロジェクトをリビルドすると、Visual Studio 2015 は自動的にビルドを更新し、version タグで指定する数値とは無関係に、NuGet パッケージのバージョン番号を改訂します。.zip アーカイブ ツールを使用して更新した最新バージョンの NuGet パッケージを開き、更新されたコンテンツを簡単にチェックできます。id 要素の値を変更するたびに、その新しい ID を基にして名前が変更された NuGet パッケージが Visual Studio によって作成されます (図 9 参照)。今回は、id 値を RSSFeedLibrary という値に変更した結果、「RSSFeedLibrary.1.0.xxx.yyy.NuPkg」という NuGet パッケージが作成されます。xxx 部分はビルドのバージョン番号、yyy はリビジョン番号を指し、どちらもビルド時において自動的に設定されます。これで、Roslyn の統合分析を備えたカスタム API を 1 つの NuGet パッケージにまとめるという、最初の目的を達成しました。

もう 1 つの方法として、2 つの独立したパッケージと 3 つ目の空の NuGet パッケージを作成 (公開) することもできます。2 つのパッケージの 1 つはアナライザー用、もう 1 つはライブラリ用です。3 つ目は、この 2 つを依存関係として解決してまとめるためのものです。このアプローチでは、パッケージを最初から手作業で作成することになり、NuGet の記法に精通している必要がありますが、アナライザーを除いた API のみを使用することで、インストール サイズを比較的小さく保つことができるようになります。新しく生成したパッケージを NuGet のオンライン リポジトリに公開する前に、ローカルでそのパッケージをテストすることをお勧めします。

NuGet パッケージのローカルでのテスト

Visual Studio 2015 は、NuGet パッケージをローカル リポジトリから選択できるようにします。この機能は、使用頻度の高いパッケージをキャッシュする必要がある場合や、オフラインで作業する場合に特に便利です。このソリューションは、ローカル フォルダー内のパッケージ数が少ないときに適していますが、何百ものパッケージがある場合は処理が大幅に複雑になります。以前に生成した NuGet パッケージをテストするには、ディスクに LocalPackages という新しいフォルダーを作成し、RSSFeedLibrary.1.0.xxx.yyy.nupkg ファイルの最新バージョンをそのフォルダーの直下にコピーします。次に、Visual Studio 2015 が指定したローカル フォルダーのパッケージを選択できるようにします。そのためには、[ツール]、[オプション]、[NuGet パッケージ マネージャー]、[パッケージ ソース] の順に選択し、[利用可能なパッケージ ソース] ボックスで追加ボタン (緑色の [+] 記号) をクリックします (図 10 参照)。ここで、[名前] と [ソース] の各テキストボックスに、それぞれ「Local packages」と「C:\LocalPackages」を入力します。最後に [更新] をクリックして、パッケージ ソースの一覧を最新の状態に更新します。

NuGet パッケージのソースとしてローカル リポジトリを追加
図 10 NuGet パッケージのソースとしてローカル リポジトリを追加

これで、ローカル NuGet リポジトリが利用できるようになるので、ライブラリと Roslyn の分析パッケージをテストするための新しいコンソール アプリケーションを Visual Studio 2015 で作成します。プロジェクトを保存後、[プロジェクト]、[NuGet パッケージの管理] の順に選択します。表示される [NuGet パッケージ マネージャー] ウィンドウの [パッケージ ソース] コンボ ボックスで [Local packages] をソースに選択します。その結果、指定したリポジトリ内で利用可能なパッケージの一覧 (今回の場合はサンプル パッケージのみ) がパッケージ マネージャーに表示されます。

[インストール] をクリックします。NuGet パッケージがオンラインの場合と同様に、Visual Studio で選択したパッケージの概要情報が表示され、ライセンス契約に同意することを求められます。インストール完了後は、ソリューション エクスプローラーでライブラリと Roslyn アナライザーの両方を確認できます (図 11 参照)。

NuGet パッケージからインストールしたライブラリと Roslyn アナライザー
図 11 NuGet パッケージからインストールしたライブラリと Roslyn アナライザー

無効な URL を引数として渡すために FeedItem.ParseFeedAsync メソッドを使用するコードを作成すると、想定どおり、ライブ分析エンジンが警告メッセージを表示します (図 8 参照)。

アナライザーを作成した本人かどうかにかかわらず、アナライザーをインストール後は、ソリューション エクスプローラーで [References]、[Analyzers]、[<アナライザーの名前>] の順に展開すると、それぞれの規則の詳細を確認できます。今回の場合、FeedLibraryAnalyzer を展開すると、RSS URL analysis 規則が表示されます (図 11 参照)。規則の 1 つをクリックすると、[Default Serverity] や [Effective Severity] などの詳細規則が [プロパティ] ウィンドウに表示され、既定で有効になっていれば、規則の完全な説明も表示されます。さらに、Ruleset Editor を使用すると、プロジェクトに適用可能なすべての規則の表示、規則の重要度の表示や変更、アナライザーと規則の有効/無効の切り替えが可能になります。Ruleset Editor を開くには、ソリューション エクスプローラーで [プロパティ] をダブルクリックし、プロジェクトの [プロパティ] ウィンドウの [コード分析] タブを選択して、既定の規則セットを変更しないで [開く] をクリックします。

図 11 を見てわかるとおり、規則コードの横にあるチェック ボックスのオン/オフを切り替えることで、規則を有効または無効にすることができます。[DefaultSseverity」 を変更するには現在の重要度レベルの右側にある黒い下矢印をクリックします。また、ソリューション エクスプローラーで規則を右クリックし、コンテキスト メニューから [ルール セットの重要度を設定] を選択してもかまいません。ローカル テストの結果に納得したら、NuGet パッケージをオンラインに公開します。

オンラインでの NuGet パッケージのテスト

開発者は、専門的で高品質のパッケージを NuGet で見つけることができます。そのため、パッケージをオンラインの NuGet ギャラリーに公開する前に、プライベートな NuGet リポジトリとフィードを作成できるオンライン サービスを利用して完成品をテストし、作成したパッケージが安定した後、公式 NuGet リポジトリに送ります。お勧めのオンライン サービスは、個人や企業向け NuGet フィードに加え、VSIX、npm、Bower の各フィードも作成可能なオンライン サービス MyGet (myget.org、英語) です。MyGet は、NuGet パッケージの公開と利用に必要となる最も一般的な機能を備えた無料プランを提供しています。無料プランではプライベート フィード (有償プランが必要) を作成できませんが、パッケージが期待どおりに動作することをオンライン リポジトリからテストする目的であれば十分使えます。MyGet に登録するときに、NuGet フィードを作成するオプションを指定します。今回 MyGet に登録したパブリック フィードは、myget.org/F/­aledelsole/api/v2 から入手できます。MyGet のしくみはここで詳しく説明しませんが、ドキュメントでは NuGet フィードの構成方法が細部まで説明されています。フィードを作成してパッケージを公開したら、Visual Studio 2015 は MyGet フィードから NuGet パッケージを簡単に選択できるようになります。これを行うには、図 10 を参考にして前述の手順に従い、MyGet フィードの URL を入力します。Visual Studio でパッケージをダウンロードしてテストするには、前述の手順に従い、[NuGet パッケージ マネージャー] ウィンドウで MyGet フィードをソースとして明示的に選択します。

オンライン NuGet ギャラリーへのパッケージの公開

オンライン NuGet リポジトリにパッケージを公開するには、nuget.org (英語) を開き、アカウントを指定してサインインします。アカウントをお持ちでなければ、ページ右上隅の [Register/Sign In] リンクをクリックします。サインインは、マイクロソフト アカウント (推奨) またはユーザー名/パスワードの資格情報のどちらでも行うことができます。登録したら、[Upload Package] をクリックします。まず、アップロードする Nuget パッケージを指定するメッセージが表示されるので、[Browse] をクリックし、ディスクから RSSFeedLibrary.1.0.xxx.yyy.nupkg の最新バージョンを選択した後、[Upload] をクリックします。

ここで、パッケージのメタデータ情報の入力が求められます。ギャラリーにパッケージをアップロードする前に、パッケージをレビューできます (図 12 参照)。準備ができたら、[Submit] をクリックします。その時点で、API と Roslyn の統合アナライザーの両方を含むパッケージが、NuGet ギャラリーに公開されます。ボタンをクリックしてからパッケージが Visual Studio 2015 の [NuGet パッケージ マネージャー] に表示されて利用可能になるまで、通常 15 ~ 20 分掛かります。

パッケージ公開前の詳細レビュー
図 12 パッケージ公開前の詳細レビュー

パッケージがギャラリーに一覧されたら、オンライン NuGet リポジトリからプロジェクトへパッケージをインストールできるようになります (図 13 参照)。インストールが完了したら、.NET コンパイラ プラットフォームを基盤とするドメイン固有の統合ライブ コード分析を備えたライブラリを利用できるようになります。

オンライン リポジトリに公開され入手可能になった NuGet パッケージ
図 13 オンライン リポジトリに公開され入手可能になった NuGet パッケージ

パッケージの更新

すべての NuGet パッケージと同様、ライブラリを改良して、パッケージの更新バージョンを NuGet にアップロードすることができます。更新パッケージを作成するには、ソリューションをリビルドするだけです。Visual Studio 2015 が自動的にパッケージのバージョン番号を更新します。プロジェクトをリビルドしたら、NuGet パッケージをオンラインに公開する手順を再度実行します。NuGet がパッケージごとに利用可能なバージョンの一覧を処理し、開発者が必要なバージョンを選択できるようにします。

まとめ

.NET コンパイラ プラットフォームを利用する最大のメリットの 1 つは、API 用にドメイン固有のコード分析規則を作成できる点です。今回は、最初にサンプル ライブラリを作成する方法を紹介し、ライブラリの利用者がコードを入力中に問題点を検出する Roslyn アナライザーを作成する方法を説明しました。その後、ライブラリとアナライザーの両方を NuGet パッケージにバンドルする方法を取り上げました。この部分が今回の主要テーマです。その結果、開発者は NuGet パッケージをダウンロードして、Roslyn の統合ライブ分析を備えた API を利用できるようになります。オンラインへの公開前にローカルで Nuget パッケージをテストする方法についても説明しました。この作業は、ライブラリとアナライザーのペアを公開する前に、その動作が適切かどうかを確認する機会になるため、非常に重要です。努力した成果を他の開発者に利用してもらうのは楽しいものです。そこで、最後の部分で、オンライン NuGet リポジトリにパッケージを公開する方法と、Visual Studio から公開したパッケージをインストールする方法も説明しました。ライブラリと共に Roslyn アナライザーを配置すれば、さまざまな開発者のコーディング エクスペリエンスを向上させながら、業務の価値を大きく高めることができます。


Alessandro Del Soleは 2008 年から Microsoft MVP の一員です。彼は年間 MVP を 5 度受賞し、Visual Studio による .NET 開発に関する、書籍、電子ブック、説明ビデオ、記事を手がけてきました。Twitter は、@progalex (英語) からフォローできます。

この記事のレビューに協力してくれた技術スタッフの Srivatsn Narayanan と Manish Vasani に心より感謝いたします。
Srivatsn Narayanan は、8 年前からマイクロソフトでプログラム言語 (IronPython、C#、VB.NET) に取り組んでいます。彼は上記の言語に関するコンパイラおよび言語サービスを担当し、最近では Rosyn アナライザー/フィクサー フレームワーク開発リーダーを務めていました。

Manish Vasani は、マイクロソフトの Visual Studio グループ内のさまざまなチームと共に 8 年間働いた経験を持つ、ソフトウェア エンジニアです。現在の彼は、マネージ言語分析サービス チームに所属し、Roslyn アナライザー API、アナライザー実行インフラストラクチャ、およびそのインフラの Visual Studio への統合を担当しています。