Привет, Хабр!

Не так давно я сходил на конференцию CLRium от sidristij, где увидел довольно простой и удобный способ для анализа исходного кода C# в MSVS 2015.

Задача взята из проекта, в котором я участвую: каждый аргумент со ссылочным типом должен иметь аттрибут NotNull или CanBeNull (которые потом использует ReSharper). В реальности, конечно, в самом проекты атрибуты являются только частью проверок, однако это не мешает им быть обязательными. Уже есть тесты, которые проверяют сборку и падают, если методы или конструкторы не содержат требуемых атрибутов, однако разработчики все равно довольно часто забывают их проставить, что приводит к падениям билдов, обновлению кода и т.д. Вот если бы Visual Studio вместе с ReSharper выдавали бы предупреждения, что код не совсем хороший, то можно было бы сэкономить время и нервы…

И, на самом деле, вместе с новой студией это становится возможным! Более того, сделать свои проверки нереально просто.

Исходный код можно посмотреть тут.

Итак, для начала у Вас должна быть скачана Microsoft Visual Studio 2015. Советую внимательно выбирать язык, так как я не посмотрел и скачал русскую версию, с весьма нестандартным переводом.
Далее, необходимо скачать расширение для MSVS ".NET Compiler Platform SDK Templates" (см. детали здесь, спасибо Ordos)
Далее создаем проект для для нашего анализатора кода:



В результате, студия создаст три проекта: рабочий проект, тесты и специальный проект для vsix расширения. Первый проект необходим для реализации нашего анализатора + для создания Code Fix'ов, то есть подсказок в студии с предложением исправить. Есть два способа распространения пакета: через vsix расширения и через nuget. Первый позволяет установить проверки для Visual Studio на текущем компьютере, не затрагивая проекты. Второй способ позволяет проверять код во время разработки (причем, на любом компьютере, nuget пакет докачается), а также во время сборки (даже если Visual Studio не установлена), он работает и в предыдущих выпусках Visual Studio. Для создания nuget пакета достаточно просто nuspec файла и пары скриптов, однако для vsix расширения создается дополнительный проект (который здесь приведен только для примера).

Также интересен проект с тестами: мы можем тестировать и отлаживать наш анализатор без отдельного запуска Visual Studio! Мы просто создаем C# файл, загружаем его в компилятор, а он возвращает список Warning/Error!

Изначально Visual Studio создает шаблонный анализатор, который требует, чтобы все типы имели именования в UPPERCASE. И тесты заточены на него.

Чтобы поменять поведение, проделаем следующие изменения с классом наследником DiagnosticAnalyzer:

1. Изменим DiagnosticId на свой. Это ключ нашего warning'а (с типом String). Его увидит программист в списке ошибок, на него среагирует CodeFix, его будет использовать атрибут SuppressMessage. Чтобы случайно ни с кем не пересечься, лучше всего выбрать название подлиннее. Я выбрал его как <имя nuget пакета>_<внутренний id>: NullCheckAnalyzer_MethodContainsNulls
2. Затем лучше всего поменять все описания на свои: см. методы Title, MessageFormat, Description, Category.
3. Далее в методе Initialize поменяем аргументы функции RegisterSymbolAction: мы будем реагировать не на типы, а на методы. Кстати говоря, судя по моим изысканиям и исходникам Roslyn, часть значений SymbolKind вообще не поддерживается (то есть, например, на параметры мы реагировать не можем).
3. Меняем немного метод AnalyzeSymbol:
— На вход нам придет лексема для проверки
— На каждую ошибку необходимо добавить в контекст информацию о ней. То есть, для одного метода можно найти сколько угодно ошибок, причем с разными Id.

Получается следующий код:
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class NullCheckAnalyzer : DiagnosticAnalyzer
{
    public const string ParameterIsNullId = "NullCheckAnalyzer_MethodContainsNulls";

    // You can change these strings in the Resources.resx file. If you do not want your analyzer to be localize-able, you can use regular strings for Title and MessageFormat.
    internal static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.AnalyzerTitle), Resources.ResourceManager, typeof(Resources));
    internal static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.AnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources));
    internal static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.AnalyzerDescription), Resources.ResourceManager, typeof(Resources));
    internal const string Category = "Naming";

    internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor(ParameterIsNullId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);

    private static readonly ImmutableArray<DiagnosticDescriptor> supportedDiagnostics = ImmutableArray.Create(Rule);

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => supportedDiagnostics;

    public override void Initialize(AnalysisContext context)
    {
        context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Method);
    }

    private static void AnalyzeSymbol(SymbolAnalysisContext context)
    {
        var methodSymbol = context.Symbol as IMethodSymbol;

        if (ReferenceEquals(null, methodSymbol) || methodSymbol.DeclaredAccessibility == Accessibility.Private)
        {
            return;
        }

        foreach (var parameter in ParametersGetter.GetParametersToFix(methodSymbol))
        {
            var type = methodSymbol.ContainingType;

            // For all such symbols, produce a diagnostic.
            var diagnostic = Diagnostic.Create(Rule, parameter.Locations[0], methodSymbol.Name, type.Name, parameter.Name);

            context.ReportDiagnostic(diagnostic);
        }
    }
}

Все, теперь наш маленький анализатор уже заставит Visual Studio сыпать ошибками. Для проверки запустим тесты. Microsoft заботливо создала целых два: проверка того, что пустой файл корректен, и проверка того, что диагностика + исправления работают правильно. Первый завершается правильно, а второй с ошибкой, так как мы так и не сделали Code Fix.

Я попытался быстро сделать Code Fix и понял, что даже такое простое исправление уже содержит нюансы, которые не так просто решить:
— Из какого namespace добавлять NotNull атрибут? Из Resharper.*? А если есть несколько вариантов: свои атрибуты и пакет от Resharper?
— Как дописывать using: внутри namespace, или же сверху файла? Не будет ли коллизий? Возможно, лучше зарегистрировать alias?
— Если нет ссылки на сборку с атрибутами, то её надо добавить, однако по каким правилам? Взять первую попавшуюся, или попробовать загрузить с сайта nuget? Или с корпоративного nuget репозитория?

Попробовав несколько исправлений, я понял, что:
1. Они работают. Roslyn действительно добавляет атрибуты, компилирует результат.
2. Алгоритм довольно простой: надо найти необходимый *Syntax элемент, потом с помощью SyntaxFactory создать правильный и вызвать ReplaceNode.
3. Правильный Code Fix не настолько прост, как кажется на первый взгляд. И вместо того, чтобы предлагать проблемное решение, лучше попросить программиста сделать исправление самостоятельно.

Для того чтобы протестировать анализатор, достаточно просто установить Nuget пакет (т.е. ввести команду в Package Manager Console: Install-Package NullCheckAnalyzer). Однако, скорее всего, тот пакет, который Вы собрали, не заработает, так как изначально PowerShell скрипты содержат ошибку: в путь в dll с анализатором зачем-то добавляется подпапка «C#». Поэтому эти строчки лучше поменять так, как сделано тут. После этих действий nuget пакет готов, его можно выгружать на nuget.org, а потом добавить в проект.

И вот как оно выглядит в Microsoft Visual Studio 2015:


В итоге, на выходе мы получаем расширение:
1. Которое подключается и обновляется через Nuget
2. Проверяет код в процессе написание и компиляции (в т.ч. без MSVS)
3. Пишется настолько просто и быстро, что ревью среднего pull-request'а в компании займет больше времени

И напоследок парочка советов:
1. Как Вы видите, Microsoft отдала предпочтение неизменяемым типам. А потому большинство конструкций Code Fix можно создать заранее, а потом просто давать ссылки.
2. В процессе тестирования проверять можно легко только один файл, а потому лучше специально предусмотреть варианты с partial-классами и прочими многофайловыми конструкциями
3. Пока нет Release версии Roslyn, а потому API может незначительно поменяться. Уже сейчас некоторые ответы на Stackoverflow содержат советы по устаревшему API.
4.
Ordos подсказал страницу в интернете с похожим описанием.

Комментарии (9)


  1. sidristij
    01.06.2015 06:55
    +4

    Вот, это очень здорово, когда с семинаров и конференций выносят полезное для себя =)


  1. Ordos
    01.06.2015 08:21

    Скажите, какую версию VS вы использовали? Как раз вчера хотел посмотреть эти анализаторы, но не нашёл там таких проектов. Версия VS Community 2015 RC. SDK тоже вроде бы поставил.


    1. Sequd
      01.06.2015 09:41

      Такая же ситуация, поставил Community, таких проектов не было. Поставил пакетом, но все равно не завелся.


    1. imanushin Автор
      01.06.2015 10:41

      У меня MSVS 2015 RC. Отдельного SDK я не ставил.
      Попробуйте скопировать один из репозиториев. Ведь все эти анализаторы ставятся через Nuget.
      Довольно подозрительно, что пунктов меню нет в некоторых версиях…

      Скриншот About


      1. Ordos
        01.06.2015 16:40
        +2

        Оказывается в студии в расширениях надо поставить ".NET Compiler Platform SDK Templates"
        После этого проект начинает открываться и в списке проекты тоже появляются.
        Взято отсюда: joshvarty.wordpress.com/2015/04/30/learn-roslyn-now-part-10-introduction-to-analyzers


        1. imanushin Автор
          01.06.2015 18:06

          Да, видимо, я как-то при установке поставил. Добавлю в пост, спасибо!


        1. DeeKey
          01.06.2015 19:52
          +1

          Огромное спасибо за ссылку на блог Josh Varty. По Roslyn пока не так много удобоваримой и up-to-date справочной информации.


  1. DjoNIK
    01.06.2015 08:46

    Смутила перегрузка SupportedDiagnostics. Так и нужно, чтоб каждый раз создавался ImmutableArray?


    1. imanushin Автор
      01.06.2015 10:44

      Спасибо!

      Это я просто не менял шаблонный код. На деле создание Rule можно кешировать.