Пишем простой анализатор кода на Roslyn

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

    Не так давно я сходил на конференцию 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 подсказал страницу в интернете с похожим описанием.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

      +4
      Вот, это очень здорово, когда с семинаров и конференций выносят полезное для себя =)
        0
        Скажите, какую версию VS вы использовали? Как раз вчера хотел посмотреть эти анализаторы, но не нашёл там таких проектов. Версия VS Community 2015 RC. SDK тоже вроде бы поставил.
          0
          Такая же ситуация, поставил Community, таких проектов не было. Поставил пакетом, но все равно не завелся.
            0
            У меня MSVS 2015 RC. Отдельного SDK я не ставил.
            Попробуйте скопировать один из репозиториев. Ведь все эти анализаторы ставятся через Nuget.
            Довольно подозрительно, что пунктов меню нет в некоторых версиях…

            Скриншот About

              +2
              Оказывается в студии в расширениях надо поставить ".NET Compiler Platform SDK Templates"
              После этого проект начинает открываться и в списке проекты тоже появляются.
              Взято отсюда: joshvarty.wordpress.com/2015/04/30/learn-roslyn-now-part-10-introduction-to-analyzers
                0
                Да, видимо, я как-то при установке поставил. Добавлю в пост, спасибо!
                  +1
                  Огромное спасибо за ссылку на блог Josh Varty. По Roslyn пока не так много удобоваримой и up-to-date справочной информации.
              0
              Смутила перегрузка SupportedDiagnostics. Так и нужно, чтоб каждый раз создавался ImmutableArray?
                0
                Спасибо!

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

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

              Самое читаемое