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

Привет, хабр! Не так давно я сходил на конференцию CLRium от sidristij, где увидел довольно простой и удобный способ для анализа исходного кода C# в MSVS 2015.Задача взята из проекта, в котором я участвую: каждый аргумент со ссылочным типом должен иметь аттрибут NotNull или CanBeNull (которые потом использует ReSharper). В реальности, конечно, в самом проекты аттрибуты являются только частью проверок, однако это не мешает им быть обязательными. Уже есть тесты, которые проверяют сборку и падают, если методы или конструкторы не содержат требуемых аттрибутов, однако разработчики все равно довольно часто забывают их проставить, что приводит к падениям билдов, обновлению кода и т.д. Вот если бы Visual Studio вместе с ReSharper выдавали бы предупреждения, что код не совсем хороший, то можно было бы сэкономить время и нервы…И, на самом деле, вместе с новой студией это становится возможным! Более того, сделать свои проверки нереально просто.

Исходный код можно посмотреть тут.Итак, для начала у Вас должна быть скачана Microsoft Visual Studio 2015. Советую внимательно выбирать язык, так как я не посмотрел и скачал русскую версию, с весьма нестандартным переводом.Далее создаем проект для для нашего анализатора кода: 69b454b319bc4a1ca3e52caa5458d35e

В результате, студия создаст три проекта: рабочий проект, тесты и специальный проект для 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_MethodContainsNulls2. Затем лучше всего поменять все описания на свои: см. методы 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);

public override ImmutableArray SupportedDiagnostics { get { return ImmutableArray.Create (Rule); } }

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: 848a117c15dc4daa940a3e694281d3f0

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

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

© Habrahabr.ru