Октябрь 2015

Том 30, номер 10

.NET Compiler Platform - Компиляция и развертывание библиотек в NuGet совместно с Roslyn-анализатором кода

Алессандро Дель Дель | Октябрь 2015

Продукты и технологии:

Microsoft .NET Compiler Platform, Visual Studio 2015, NuGet

В статье рассматриваются:

  • как компилировать и развертывать библиотеки;
  • как создать специфичный для членов библиотеки Roslyn-анализатор, который обнаруживает проблемы в коде при его наборе;
  • как поместить библиотеку и анализатор в один NuGet-пакет;

Исходный код можно скачать по ссылке

Microsoft .NET Compiler Platform (также называемая кодовой базой «Roslyn») предлагает компиляторы C# и Visual Basic с открытым исходным кодом, предоставляющие, помимо всего прочего, богатые API анализа кода, которые можно использовать для создания правил анализа в реальном времени (live analysis rules), интегрируемые в редактор кода Visual Studio 2015. Благодаря .NET Compiler Platform можно писать пользовательские, специфичные для предметной области анализаторы кода и средства рефакторинга, чтобы Visual Studio обнаруживал проблемы в коде по мере его ввода, выводил предупреждения и сообщения об ошибках. Крупное преимущество .NET Compiler Platform в том, что она позволяет связывать анализаторы кода с вашими API. Например, если вы создаете библиотеки или повторно применяемые пользовательские элементы управления, то можете поставлять с ними анализаторы, чтобы облегчить программистам их работу.

В этой статье я объясню, как связывать библиотеки и анализаторы в NuGet-пакеты для онлайнового развертывания, и покажу, как предоставлять интегрированный в Roslyn анализ кода для пользовательских API. Это требует хотя бы базового знания концепций .NET Compiler Platform и принципов написания анализаторов кода. Эти темы уже обсуждались в «MSDN Magazine» в статьях Алекса Тернера (Alex Turner) «C# and Visual Basic: Use Roslyn to Write a Live Code Analyzer for Your API» (msdn.microsoft.com/magazine/dn879356) и «C#—Adding a Code Fix to Your Roslyn Analyzer» (msdn.microsoft.com/magazine/dn904670), которые я настоятельно рекомендую прочитать, прежде чем продолжить дальше.

Крупное преимущество .NET Compiler Platform в том, что она позволяет связывать анализаторы кода с вашими API.

Подготовка библиотеки-примера к симуляции пользовательских API

Первым делом нужна библиотека класса, которая симулирует пользовательский API. Библиотека-пример для этой статьи предоставляет простой открытый метод, который извлекает общую информацию из RSS-канала, возвращая набор элементов каналов (feed items). В Visual Studio 2015 создайте новый проект Portable Class Library с именем FeedLibrary на C# или Visual Basic и убедитесь, что он ориентирован на Windows 8.1, Windows Phone 8.1 и Microsoft .NET Framework 4.5.1. В этом случае библиотека также может использовать преимущества шаблона Async/Await без дополнительных требований.

Переименуйте сгенерированный файл Class1.vb или Class1.cs в FeedItem.vb/.cs. C#-код для этого класса показан на рис. 1, а код на Visual Basic — на рис. 2.

Рис. 1. Реализация класса на C# для извлечения общих элементов из RSS-канала

using System.Net.Http;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace FeedLibrary
{
  // Представляет отдельный элемент контента в RSS-канале
  public class FeedItem
  {
    // Свойства, представляющие информацию,
    // общую для любого RSS-канала
    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; }
    // Возвращает набор объектов FeedItem из RSS-канала
    public static async Task<IEnumerable<FeedItem>> ParseFeedAsync(
      string feedUrl)
    {
      var client = new HttpClient();
      // Скачиваем контент канала как строку
      var result = await client.GetStringAsync(new Uri(feedUrl));
      // Если результата нет, генерируем исключение
      if (result == null)
      {
        throw new InvalidOperationException(
          "The specified URL returned a null result");
      }
      else
      {
        // LINQ to XML: преобразуем возвращенную строку
        // в объект XDocument 
        var doc = XDocument.Parse(result);
        var dc = XNamespace.Get("http://purl.org/dc/elements/1.1/");
        // Выполняем LINQ-запрос к XML-документу
        // и возвращаем набор объектов FeedItem
        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. Реализация класса на Visual Basic для извлечения общих элементов из RSS-канала

Imports System.Net.Http
Imports <xmlns:dc="http://purl.org/dc/elements/1.1/">
' Представляет отдельный элемент контента в RSS-канале
Public Class FeedItem
  ' Свойства, представляющие информацию,
  ' общую для любого RSS-канала
  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
  ' Возвращает набор объектов FeedItem из RSS-канала
  Public Shared Async Function ParseFeedAsync(feedUrl As String) As _
    Task(Of IEnumerable(Of FeedItem))
    Dim client As New HttpClient
    ' Скачиваем контент канала как строку
    Dim result = Await client.GetStringAsync(New Uri(feedUrl, UriKind.Absolute))
    ' Если результата нет, генерируем исключение
    If result Is Nothing Then
       Throw New InvalidOperationException(
         "The specified URL returned a null result")
    Else
      ' LINQ to XML: преобразуем возвращенную строку
      ' в объект XDocument
      Dim document = XDocument.Parse(result)
      ' Выполняем LINQ-запрос к XML-документу
      ' и возвращаем набор объектов FeedItem
      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

Этот код очень прост: он скачивает сводный контент (syndicated content) из указанного по URL RSS-канала, создает экземпляр класса FeedItem для каждого элемента канала и возвращает новый набор элементов. Чтобы задействовать библиотеку, вызовите статический метод ParseFeedAsyncAsync для C#:

// Заменяем аргумент допустимым URL
var items = await FeedItem.ParseFeedAsyncAsync("http://sampleurl.com/rss");

А для Visual Basic то же самое выглядит так:

' Заменяем аргумент допустимым URL
Dim items = Await FeedItem.ParseFeedAsyncAsync("http://sampleurl.com/rss")

Этот вызов возвращает IEnumerable<FeedItem>, который можно потом использовать в соответствии с вашими потребностями. Выберите конфигурацию Release и скомпилируйте проект; в этот момент Visual Studio 2015 сгенерирует библиотеку FeedLibrary.dll, которая будет задействована позже.

Написание Roslyn-анализатора

Следующий шаг — создание Roslyn-анализатора, который поддерживает специфичные для предметной области правила анализа в реальном времени для пользовательских API. Этот анализатор будет обнаруживать, правильна ли форма URL, передаваемого как аргумент в метод ParseFeedAsyncAsync, используя метод Uri.IsWellFormedUriString; если она не правильна, анализатор будет выводить предупреждение при его наборе. Конечно, существует масса способов распознавания недопустимости URL, но я использую этот простоты ради. Кроме того, по тем же причинам анализатор будет выдавать только предупреждения, не предлагая никаких исправлений кода (code fixes). Реализацию их поддержки я оставляю вам в качестве упражнения. Итак, придерживайтесь следующей схемы.

  1. В Solution Explorer щелкните правой кнопкой мыши имя решения и выберите Add | New Project.
  2. В узле Extensibility списка шаблонов проектов укажите Analyzer with Code Fix (NuGet + VSIX). Заметьте, что узел Extensibility по умолчанию не появляется. Сначала вам нужно скачать .NET Compiler Platform SDK для получения шаблонов проектов Analyzer with Code Fix. Найдите Analyzers в диалоге New Project и вы увидите, как скачать этот SDK.
  3. Назовите новый анализатор FeedLibraryAnalyzer и щелкните OK.
  4. Когда новый проект готов, удалите файл CodeFixProvider.cs (или .vb).

В файле DiagnosticAnalyzer.cs (или .vb) надо прежде всего предоставить строки, идентифицирующие анализатор при кодировании. Чтобы не усложнять реализацию анализатора, я использую в текущем примере обычные строки вместо объекта LocalizableString и файлов ресурсов, предполагая, что анализатор не требуется локализовать. Перепишите DiagnosticId, Title, Message, Description и Category для C# следующим образом:

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"

Не изменяйте уровень диагностики (по умолчанию — Warning) — это правильный выбор для данного примера. Теперь сосредоточимся на логике анализа. Анализатор должен проверять, вызывает ли код метод ParseFeedAsync. Если да, тогда анализатор проверяет правильность синтаксиса URL. С помощью Syntax Visualizer можно увидеть (рис. 3), как вызов метода ParseFeedAsync представляется InvocationExpression, сопоставленным с объектом типа InvocationExpressionSyntax.

Syntax Visualizer помогает найти правильное представление синтаксического узла
Рис. 3. Syntax Visualizer помогает найти правильное представление синтаксического узла

Поэтому анализатор концентрируется только на объектах типа InvocationExpressionSyntax; найдя таковой, он преобразует сопоставленное выражение в объект типа MemberAccessExpressionSyntax, который содержит информацию о вызове метода. Если преобразование проходит успешно, анализатор проверяет, является ли метод именно ParseFeedAsync, затем извлекает первый аргумент и выполняет анализ его значения в реальном времени. Это осуществляется новым методом, AnalyzeMethod, который работает на уровне SyntaxNode и представлен на рис. 4 для C# и на рис. 5 для Visual Basic.

Рис. 4. Обнаружение проблем в аргументе ParseFeedAsync на C#

private static void AnalyzeMethodInvocation(
  SyntaxNodeAnalysisContext context)
{
  // Преобразуем текущий синтаксический узел
  // в InvocationExpressionSyntax,
  // который представляет вызов метода
  var invocationExpr =
    (InvocationExpressionSyntax)context.Node;

  // Преобразуем связанное выражение
  // в MemberAccessExpressionSyntax, который представляет
  // информацию о методе. Если выражение не является
  // MemberAccessExpressionSyntax - возврат.
  if (!(invocationExpr.Expression is
    MemberAccessExpressionSyntax))
  {
    return;
  }
  var memberAccessExpr = (MemberAccessExpressionSyntax)
    invocationExpr.Expression;

  // Если имя метода не ParseFeedAsync - возврат
  if (memberAccessExpr?.Name.ToString() !=
    "ParseFeedAsync") { return; }

  // Если имя метода - ParseFeedAsync, проверяем информацию
  // о символе и смотрим, подходит ли тип возврата
  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;
  }

  // Проверяем, есть ли в вызове метода
  // требуемый номер аргумента
  var argumentList = invocationExpr.ArgumentList;
  if (argumentList?.Arguments.Count != 1) {
      return; }

  // Преобразуем выражение для первого аргумента метода
  // в LiteralExpressionSyntax. Если null - возврат
  var urlLiteral = (LiteralExpressionSyntax)
    invocationExpr.ArgumentList.Arguments[0].Expression;
  if (urlLiteral == null) { return; }

  // Преобразуем реальное значение аргумента метода в строку.
  // Если null - возврат.
  var urlLiteralOpt = context.SemanticModel.GetConstantValue(
    urlLiteral);

  var urlValue = (string)urlLiteralOpt.Value;
  if (urlValue == null) { return; }

  // Если URL синтаксически неправилен, создаем диагностику
  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. Обнаружение проблем в аргументе ParseFeedAsync на Visual Basic

Private Sub Shared AnalyzeMethodInvocation(
  context As SyntaxNodeAnalysisContext)
  ' Преобразуем текущий синтаксический узел
  ' в InvocationExpressionSyntax,
  ' который представляет вызов метода
  Dim invocationExpr = CType(context.Node,
    InvocationExpressionSyntax)

  ' Преобразуем связанное выражение
  ' в MemberAccessExpressionSyntax, который представляет
  ' информацию о методе. Если выражение не является
  ' MemberAccessExpressionSyntax - возврат.
  If TypeOf invocationExpr.Expression IsNot
    MemberAccessExpressionSyntax Then Return
  Dim memberAccessExpr = DirectCast(invocationExpr.Expression,
    MemberAccessExpressionSyntax)

  ' Если имя метода не ParseFeedAsync - возврат
  If memberAccessExpr?.Name.ToString <>
    "ParseFeedAsync" Then Return

  ' Если имя метода - ParseFeedAsync, проверяем информацию
  ' о символе и смотрим, подходит ли тип возврата
  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

  ' Проверяем, есть ли в вызове метода
  ' требуемый номер аргумента
  Dim argumentList = invocationExpr.ArgumentList
  If argumentList?.Arguments.Count <> 1 Then Return

  ' Преобразуем выражение для первого аргумента метода
  ' в LiteralExpressionSyntax. Если null - возврат
  Dim urlLiteral = DirectCast(invocationExpr.ArgumentList.
    Arguments(0).GetExpression, LiteralExpressionSyntax)
  If urlLiteral Is Nothing Then Return

  ' Преобразуем реальное значение аргумента метода в строку.
  ' Если null - возврат.
  Dim urlLiteralOpt = context.SemanticModel.GetConstantValue(
    urlLiteral)

  Dim urlValue = DirectCast(urlLiteralOpt.Value, String)
  If urlValue Is Nothing Then Return

  ' Если URL синтаксически неправилен, создаем диагностику
  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, как показано на рис. 6 для C# и на рис. 7 для Visual Basic.

Рис. 6. Редактирование метода Initialize на C#

public override void Initialize(AnalysisContext context)
{
  // Регистрируем действие, когда начинается компиляция
  context.RegisterCompilationStartAction((
    CompilationStartAnalysisContext ctx) =>
  {
    // Смотрим, есть ли метаданные типа в контексте компиляции
    var myLibraryType = ctx.Compilation.
      GetTypeByMetadataName("FeedLibrary.FeedItem");

    // Если нет - возврат
    if (myLibraryType == null)
        return;

    // Регистрируем действие применительно
    // к InvocationExpression
    ctx.RegisterSyntaxNodeAction(AnalyzeMethodInvocation,
      SyntaxKind.InvocationExpression);
  });
}

Рис. 7. Редактирование метода Initialize на Visual Basic

Public Overrides Sub Initialize(context As AnalysisContext)
  ' Регистрируем действие, когда начинается компиляция
  context.RegisterCompilationStartAction
    (Sub(ctx As CompilationStartAnalysisContext)
      ' Смотрим, есть ли метаданные типа в контексте компиляции
      Dim myLibraryType = ctx.Compilation.
        GetTypeByMetadataName("FeedLibrary.FeedItem")
        ' Если нет - возврат
        ' (нет ссылки на библиотеку)
        If myLibraryType Is Nothing Then Return
        ' Регистрируем действие применительно
        ' к InvocationExpression
        ctx.RegisterSyntaxNodeAction(
          AddressOf AnalyzeMethodInvocation,
          SyntaxKind.InvocationExpression)
     End Sub)
End Sub

Заметьте, что код сначала проверяет, есть ли ссылка на библиотеку в проекте, вызывая метод Compilation.GetTypeMetadataName, чьим аргументом является имя типа, который должен существовать в текущем контексте. Это необходимо, чтобы убедиться в том, что ссылка была добавлена. Если этот вызов возвращает null, значит, тип не существует и, следовательно, ссылки на библиотеку нет. В таком случае нет нужды регистрировать действие анализа кода, что ускоряет работу анализатора. Если теперь нажать клавишу F5 для проверки анализатора в экземпляре Experimental среды Visual Studio 2015 и создать новый проект со ссылкой на библиотеку FeedLibrary, вы сможете увидеть, что он корректно выдает предупреждение всякий раз, когда URL недопустим (рис. 8).

Анализатор выдает предупреждение, если URL синтаксически неправилен
Рис. 8. Анализатор выдает предупреждение, если URL синтаксически неправилен

На данный момент мы создали API и соответствующие правила анализа кода, специфичные для предметной области. Теперь пора посмотреть, как поместить их вместе в один NuGet-пакет.

Если вы хотите поделиться какой-либо библиотекой и интегрированным анализом Roslyn, то должны добавить эту библиотеку в NuGet-пакет, который Visual Studio 2015 сгенерирует при компиляции проекта.

Создание NuGet-пакета, включающего API и анализаторы

Правила MSBuild для шаблона проекта Analyzer with Code Fix автоматизируют генерацию NuGet-пакета, включающий скомпилированный анализатор, которым можно делиться с другими разработчиками, публикуя его в репозитарии NuGet. На практике каждый раз, когда вы запускаете отладку анализатора нажатием клавиши F5 или когда вы компилируете проект анализатора, Visual Studio 2015 заново компилирует DLL-файл анализатора (FeedLibraryAnalyzer.dll в данном примере) и распространяемый NuGet-пакет, содержащий этот анализатор.

В процессе компиляции также генерируется VSIX-пакет, который можно опубликовать в Visual Studio Gallery и который также используется для отладки анализатора в экземпляре Experimental среды Visual Studio 2015, но эта тематика выходит за рамки данной статьи.

Если вы хотите поделиться какой-либо библиотекой и интегрированным анализом Roslyn, то должны добавить эту библиотеку в NuGet-пакет, который Visual Studio 2015 сгенерирует при компиляции проекта. Прежде чем делать это, вы должны получше понять, как создается NuGet-пакет для анализатора. На самом деле NuGet-пакет — это архив .zip с расширением .nupkg. Ввиду этого вы можете легко увидеть содержимое и структуру NuGet-пакета с помощью любого архиватора, понимающего формат ZIP, такого как средства сжатия папок в Windows Explorer, WinZip или WinRar. Ниже дано краткое описание самых важных элементов в NuGet-пакете, предназначенном для развертывания анализаторов.

  • Файл .nuspec Содержит метаданные пакета и включает информацию, необходимую для публикации, например имя пакета, версию, описание, автора, URL лицензии и т. д. Файл .nuspec помещается в NuGet-пакет на основе файла Diagnostic.nuspec, который вы видите в Solution Explorer в проекте анализатора. Вскоре мы отредактируем Diagnostic.nuspec в Visual Studio 2015.
  • Папка tools Содержит скрипты Windows PowerShell, используемые Visual Studio для установки (Install.ps1) и удаления (Uninstall.ps1) анализатора для данного проекта.
  • Папка analyzers Содержит файлы .dll анализатора, организованные в конкретные подпапки. Нейтральные библиотеки анализатора (т. е. ориентированные на все языки) находятся в подпапке dotnet. Анализаторы, рассчитанные на C#, размещаются в подпапке dotnet\cs, тогда как анализаторы для Visual Basic — в подпапке dotnet\vb. Стоит отметить, что dotnet представляет NuGet-профиль для .NET Core и поддерживает такие типы проектов, как Universal Windows Apps и ASP.NET 5.

В NuGet-пакет можно поместить ряд дополнительных элементов, но здесь я рассматриваю типичный пакет, генерируемый для Roslyn-анализатора, поэтому обсуждаются лишь обязательные элементы.

Любые библиотеки, на которые могут быть ссылки из проекта Visual Studio, должны быть организованы в папку lib. Поскольку библиотеки могут быть ориентированы на разные платформы, например на разные версии .NET Framework, Windows Runtime, разные версии Windows Phone, или даже могут быть портируемым подмножеством (включая библиотеки Xamarin), папка lib должна содержать по одной подпапке на каждую платформу, а каждая подпапка — копию развертываемой библиотеки. Имя каждой подпапки должно совпадать с именем так называемого профиля, представляющего конкретную платформу. Скажем, если у вас есть библиотека для .NET Framework 4.5.1 и Windows 8.1, у вас была следующая структура, где net451 — имя профиля для .NET Framework 4.5.1 и netcore451 — имя профиля для Windows Runtime в Windows 8.1:

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

Стоит отметить профиль uap10.0, который ориентирован на Universal Windows Platform (UWP) для создания приложений Windows 10. Полный список поддерживаемых профилей доступен в документации NuGet. Библиотека-пример, созданная ранее, является портируемой и ориентированной на .NET Framework 4.5.1, Windows 8.1 и Windows Phone 8.1. Имя профиля для мишени такого рода — portable-net451+netcore451+wpa81, и оно должно использоваться в качестве имени подпапки, которая будет содержать библиотеку в NuGet-пакете. Вы не должны создавать подпапку и копировать библиотеку вручную; вам нужно лишь отредактировать метаданные NuGet-пакета (файл Diagnostic.nuspec) в Visual Studio. На рис. 9 показаны обновленные метаданные с правильной информацией для публикации (идентификатор, заголовок, автор, описание, лицензия и т. д.) и новый элемент file в узле files, который указывает исходный файл и целевую подпапку.

Рис. 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 будет искать библиотеку FeedLibrary.dll в папке lib, которую вы должны создать в текущем каталоге. Последний является папкой, которая содержит скомпилированный анализатор, обычно папку Release или Debug — в зависимости от выбранной конфигурации сборки. Для текущего примера вам понадобится создать папку lib в папке Release, затем скопировать FeedLibrary.dll (сгенерированную при компиляции библиотеки-примера еще в самом начале) в папку lib. Сделав это, вы можете просто перекомпилировать свое решение, и Visual Studio сгенерирует обновленный NuGet-пакет, содержащий как библиотеку, так и Roslyn-анализатор.

Стоит отметить, что, когда вы перекомпилируете проект, Visual Studio 2015 автоматически обновляет сборку и номера версии NuGet-пакета независимо от цифр, указанных в теге version. Обновленное содержимое NuGet-пакета можно легко просмотреть, открыв его новую версию в любом архиваторе, поддерживающем формат ZIP. Помните: всякий раз, когда вы изменяете значение элемента id, как на рис. 9, Visual Studio 2015 генерирует NuGet-пакет с другим именем на основе нового id. В данном случае изменение значения id на RSSFeedLibrary дает NuGet-пакет с именем RSSFeedLibrary.1.0.xxx.yyy.NuPkg, где xxx — номер версии сборки, а yyy — номер ревизии; все эти номера автоматически предоставляются на этапе сборки. К этому моменту вы достигли первой цели: упаковки пользовательских API с интегрированным анализом Roslyn в один NuGet-пакет.

В качестве альтернативы вы могли бы создать (и опубликовать) два раздельных пакета (один для анализатора, а другой для библиотеки) и третий, пустой NuGet-пакет, который объединял бы два пакета, разрешая их как зависимости. При таком подходе вы можете решить сохранить меньший размер установки, используя только API без анализатора, хотя вам потребуется знание соглашений NuGet, чтобы вручную создать пакет с нуля. Перед публикацией только что сгенерированного пакета в онлайновом репозитарии NuGet следует протестировать его на локальном компьютере.

Локальное тестирование NuGet-пакета

Visual Studio 2015 позволяет извлекать NuGet-пакеты из локальных репозитариев. Это очень полезно, особенно в ситуациях, где нужно кешировать часто используемые пакеты или где может потребоваться работа в автономном режиме. Заметьте, что это хорошее решение, когда количество пакетов в локальной папке невелико; если бы у вас были сотни пакетов, ими было бы гораздо труднее управлять. ЧТобы протестировать ранее сгенерированный NuGet-пакет, создайте на диске новую папку с именем LocalPackages, а затем скопируйте в нее последнюю версию файла RSSFeedLibrary.1.0.xxx.yyy.nupkg. Следующий шаг — разрешить Visual Studio 2015 извлекать пакеты из указанной локальной папки, как показано на рис. 10; для этого выберите Tools | Options | NuGet Package Manager | Package Sources и в окне Available package sources щелкните кнопку Add (зеленый символ добавления). К этому моменту введите Local packages в поле Name и C:\LocalPackages в поле Source. Наконец, щелкните Update для обновления списка источников пакетов.

Добавление локального репозитария как источника NuGet-пакетов
Рис. 10. Добавление локального репозитария как источника NuGet-пакетов

Теперь, когда у вас есть локальный репозитарий NuGet, создайте в Visual Studio 2015 новое консольное приложение для проверки пакета библиотеки с Roslyn-анализатором. Сохраните проект, затем выберите Project | Manage NuGet Packages. Когда появится окно NuGet Package Manager, в поле с раскрывающимся списком выберите Local packages source. В этот момент диспетчер пакетов покажет список пакетов, доступных в указанном репозитарии (в данном случае — только пакет-пример).

Щелкните Install. Как и в случае NuGet-пакетов в сети, Visual Studio выведет сводную информацию для выбранного пакета и попросит принять лицензионное соглашение. По окончании установки вы сможете увидеть в Solution Explorer как библиотеку, так и Roslyn-анализатор (рис. 11).

Библиотека и Roslyn-анализатор были установлены через NuGet-пакет
Рис. 11. Библиотека и Roslyn-анализатор были установлены через NuGet-пакет

Если вы просто напишете какой-то код, который использует метод FeedItem.ParseFeedAsync для передачи недопустимого URL в качестве аргумента, механизм анализа в реальном времени выдаст сообщение с предупреждением, как и ожидалось (см. рис. 8 для сравнения).

Устанавливая анализатор (независимо от того, кем он был создан — вами или другими разработчиками), вы можете в деталях просматривать каждое правило в Solution Explorer, раскрыв References, Analyzers, а затем имя анализатора. В данном случае можно раскрыть имя FeedLibraryAnalyzer и увидеть правило анализа RSS URL, как показано на рис. 11. Когда вы щелкаете правило, окно Properties выводит подробную информацию, такую как исходный и действующий уровни предупреждений, если это разрешено по умолчанию, и полное описание правила. Кроме того, можно использовать Ruleset Editor для просмотра всех правил, применимых к проекту, просмотра или изменения уровня серьезности правила (rule’s severity), а также для включения или отключения анализаторов и правил. Чтобы открыть Ruleset Editor, дважды щелкните Properties в Solution Explorer, затем в окне Properties проекта выберите вкладку Code Analysis и щелкните Open, оставив без изменений набор правил по умолчанию.

Как видно на рис. 11, отключать/включать правило можно сбросом/установкой флажка рядом с кодом правила; вы можете изменить уровень серьезности по умолчанию, щелкнув черную стрелку, указывающую вниз, с правой стороны текущего уровня серьезности (то же самое можно сделать, щелкнув правой кнопкой мыши правило в Solution Explorer и выбрав Set Rule Set Severity из контекстного меню). Успешно завершив локальные тесты, можно переходить к публикации NuGet-пакета в сети.

Онлайновое тестирование пакета

В NuGet разработчики ожидают найти высококачественные пакеты профессионального уровня. По этой причине, прежде чем публиковать пакет в онлайновой галерее NuGet, вы должны проверить свою работу с помощью онлайнового сервиса, который позволяет создавать закрытые репозитарии и каналы NuGet и выпускать ваш пакет в официальный репозитарий NuGet после того, как он становится стабильным. Хороший выбор предлагается MyGet (myget.org), онлайновым сервисом, позволяющим создавать личные и корпоративные NuGet-каналы, а также каналы VSIX, npm и Bower. MyGet предлагает бесплатный вариант с наиболее распространенными средствами для публикации и использования NuGet-пакетов. Хотя этот вариант не позволяет создавать закрытые каналы (для этого нужен платный тариф), он является отличным выбором для проверки того, работают ли ваши пакеты ожидаемым образом из онлайнового репозитария. После регистрации вы получите возможность создать канал NuGet. Например, мой открытый канал на MyGet доступен по адресу myget.org/F/aledelsole/api/v2. Объяснение того, как работать с MyGet, выходит за рамки этой статьи, но в документации полностью описано, как сконфигурировать собственный канал. Создав канал и опубликовав пакет, вы просто разрешаете Visual Studio 2015 извлекать NuGet-пакеты из канала MyGet. Для этого следуйте схеме, описанной в предыдущем разделе, и возьмите за образец рис. 10, предоставив URL своего канала MyGet. Чтобы скачать и протестировать пакет в Visual Studio, вам понадобится действовать по той же схеме, описанной в предыдущем разделе, но, очевидно, выбрать в окне NuGet Package Manager в качестве источника канал MyGet.

Публикация пакета в онлайновой галерее NuGet

Чтобы публиковать пакеты в онлайновом репозитарии NuGet, нужно открыть nuget.org и войти под своей учетной записью. Если у вас еще нет учетной записи, щелкните гиперссылку Register/Sign In в правом верхнем углу страницы. Вы можете входить либо по Microsoft Account (рекомендуется), либо по имени пользователя и паролю. После регистрации щелкните Upload Package. Первое, что у вас запросят, — указать NuGet-пакет, который вы хотите закачать, поэтому щелкните Browse, выберите последнюю версию RSSFeedLibrary.1.0.xxx.yyy.nupkg с диска, а затем щелкните Upload.

Теперь вам предложат передать информацию о метаданных пакета. Здесь у вас есть возможность просмотреть детали пакета до публикации в галерее (рис. 12). Как только будете готовы, щелкните Submit. В этот момент пакет, содержащий ваши API и интегрированный Roslyn-анализатор, будет опубликован в галерее NuGet. Заметьте, что после этого до появления пакета в окне NuGet Package Manager в Visual Studio 2015 обычно проходит 15–20 минут.

Просмотр деталей пакета до публикации
Рис. 12. Просмотр деталей пакета до публикации

После того как пакет оказывается в списке галереи, вы можете установить его в свой проект из онлайновой галереи NuGet, как показано на рис. 13. Установив пакет, вы будете использовать библиотеку, как пояснялось в предыдущих разделах, вместе с интегрированным анализатором кода, специфичным для данной предметной области, в реальном времени на платформе .NET Compiler Platform.

NuGet-пакет в онлайновом репозитарии является общедоступнымy
Рис. 13. NuGet-пакет в онлайновом репозитарии является общедоступным

Обновления пакета

Как и в случае любых других NuGet-пакетов, вы можете совершенствовать свои библиотеки и передавать обновленную версию пакета в NuGet. Чтобы создать обновленный пакет, просто скомпилируйте решение заново, и Visual Studio 2015 автоматически обновит номер версии пакета. Затем повторно соберите проект и повторите операции, описанные ранее, для онлайновой публикации NuGet-пакета. NuGet будет обрабатывать список доступных версий для каждого пакета и давать возможность разработчикам выбирать нужную им версию.

Заключение

Одно из важнейших преимуществ, предлагаемых .NET Compiler Platform, заключается в том, что вы можете создавать правила анализа кода, специфичные для предметной области ваших API. В этой статье я сначала показал, как создать библиотеку-пример, потом создать Roslyn-анализатор, который обнаруживает проблемы в коде при его наборе, специфичные для членов библиотеки. Затем я продемонстрировал, как поместить библиотеку и анализатор в один NuGet-пакет, что было основной целью этой статьи. Благодаря этому разработчики, скачавшие данный NuGet-пакет, получат API с интегрированным Roslyn-анализом в реальном времени. Далее я перешел к обсуждению того, как локально проверить NuGet-пакет, прежде чем публиковать его в сети. Этот этап очень важен, потому что позволяет удостовериться в корректной работе пары «библиотека-анализатор» до того, как сделать ее общедоступной. Но самое приятное, когда результатом вашей работы могут пользоваться другие разработчики, поэтому последний раздел этой статьи я посвятил тому, как опубликовать пакет в онлайновом репозитарии NuGet и как его потом устанавливать в свои проекты из Visual Studio. Развертывание Roslyn-анализаторов совместно с библиотеками определенно повышает ценность вашей работы, обеспечивая другим программистам более качественные условия для кодирования с использованием ваших API.


Алессандро Дель Соуле (Alessandro Del Sole) — был Microsoft MVP с 2008 года. Награждался званием MVP of the Year пять раз. Автор многих книг, электронных книг, обучающих видеороликов и статей по .NET-разработке в Visual Studio. Следите за его заметками в Twitter (@progalex).

Выражаю благодарность за рецензирование статьи экспертам Сриватсну Нарайенену (Srivatsn Narayanan) и Манишу Васани (Manish Vasani).