Привет! Меня зовут Дима, я frontend-разработчик в компании Wrike. В этой статье я расскажу про то, как написать плагин для анализа кода на Dart. Текст будет полезен тем, кому не хватает текущей функциональности дартового анализатора по статическому анализу или если вам просто захочется попробовать написать простой анализатор самостоятельно.
Согласно документации, плагин — это код, который коммуницирует с analysis-сервером и дополнительно анализирует код. Он запускается в той же самой VM, что и сервер, но в отдельном изоляте. Интеграция с существующими IDE лежит полностью на стороне сервера, что позволяет не думать об этом при разработке плагина.
Сервер предоставляет данные, которые с помощью драйвера анализатора (отвечает за сбор информации об анализируемых файлах) преобразуются в AST. Плагину предоставляется AST, с которым можно работать: дополнительно подсвечивать ошибки в коде или собирать статистику.
Возможности плагина и пошаговая схема создания
С помощью плагина можно показывать ошибки и способы их исправления, делать подсветку синтаксиса, навигацию и автодополнение. Например, при выделении блока кода можно добавить assist, который обернет этот блок во что-то или отформатирует его.
Подробно возможности плагина описаны в спецификации.
Краткая пошаговая схема создания плагина выглядит так:
-
Создаете пакет с зависимостями на анализатор и плагин.
-
Создаете класс, который унаследован от ServerPlugin.
-
Имплементируете базовые геттеры и метод на инициализацию от сервера.
-
Добавляете функцию «стартер».
-
Создаете отдельный подпакет, который расположен в tools/analyzerplugin/.
-
Для использования плагина в клиенте указываете зависимость на пакет.
-
Добавляете название плагина в analysis_options, в блок плагинов.
Реализация плагина
Простой плагин выглядит примерно так:
class CustomPlugin extends ServerPlugin {
CustomPlugin(ResourceProvider provider): super(provider);
@override
List<String> get fileGlobsToAnalyze => const ['*.dart'];
@override
String get name => 'My custom plugin';
@override
String get version => '1.0.0';
@override
AnalysisDriverGeneric createAnalysisDriver(ContextRoot contextRoot) {
// implementation
}
}
Три геттера — название плагина, используемая версия API сервера (должна совпадать с одной из существующих версий, например, 1.0.0-alpha.0), с которым работает плагин, и паттерн на то, какие файлы анализировать.
В методе инициализации нужно создать дартовый драйвер:
@override
AnalysisDriverGeneric createAnalysisDriver(plugin.ContextRoot contextRoot) {
final root = ContextRoot(contextRoot.root, contextRoot.exclude,
pathContext: resourceProvider.pathContext)
..optionsFilePath = contextRoot.optionsFile;
final contextBuilder = ContextBuilder(resourceProvider, sdkManager, null)
..analysisDriverScheduler = analysisDriverScheduler
..byteStore = byteStore
..performanceLog = performanceLog
..fileContentOverlay = fileContentOverlay;
final dartDriver = contextBuilder.buildDriver(root);
dartDriver.results.listen((analysisResult) {
_processResult(dartDriver, analysisResult);
});
return dartDriver;
}
Только этот драйвер идет в пакете с анализатором по умолчанию. Но API позволяет создать драйвер для любого языка. Единственный минус — для этого потребуется достаточно усилий: инициализация драйвера и других классов, конфигурация и так далее.
При создании драйвера необходимо подписаться на результат обработки файлов. Для каждого файла драйвер будет пушить событие о результате.
Структура результата выглядит так:
abstract class ResolveResult implements AnalysisResultWithErrors {
/// The content of the file that was scanned, parsed and resolved.
String get content;
/// The element representing the library containing the compilation [unit].
LibraryElement get libraryElement;
/// The type provider used when resolving the compilation [unit].
TypeProvider get typeProvider;
/// The type system used when resolving the compilation [unit].
TypeSystem get typeSystem;
/// The fully resolved compilation unit for the [content].
CompilationUnit get unit;
}
По моему опыту, для анализа кода полезен Compilation unit. С ним удобно работать, потому что он содержит AST-дерево дартового файла. Еще в этом фрагменте кода есть другая информацию про библиотеки, систему типов и так далее.
Для обхода дерева используется набор готовых визиторов:
-
RecursiveAstVisitor
-
GeneralizingAstVisitor
-
SimpleAstVisitor
-
ThrowingAstVisitor
-
TimedAstVisitor
-
UnifyingAstVisitor
RecursiveAstVisitor рекурсивно обходит все узлы AST. Например, при обходе узла [Block], визитор пройдет и все дочерние узлы.
GeneralizingAstVisitor рекурсивно обходит все узлы AST (аналогично RecursiveAstVisitor), но для всех узлов будут вызваны не только методы для обхода данного типа узла, но и для обхода базового класса для этого типа.
SimpleAstVisitor не делает ничего при обходе узлов AST. Подходит для случаев, когда не требуется рекурсивный обход.
ThrowingAstVisitor выбрасывает исключение при обходе узла AST, метод обхода которого не был переопределен в наследнике.
TimedAstVisitor позволяет замерять время обхода AST.
UnifyingAstVisitor рекурсивно обходит все узлы AST (аналогично RecursiveAstVisitor), но для всех узлов дополнительно вызывает общий метод visitNode.
Простая имплементация рекурсивного визитора может выглядеть так:
String checkCompilationUnit(CompilationUnit unit) {
final visitor = _Visitor();
unit.visitChildren(visitor);
return visitor.result;
}
class _Visitor extends RecursiveAstVisitor<void> {
String result = ‘’;
@override
void visitMethodInvocation(MethodInvocation node) {
super.visitMethodInvocation(node);
// implementation
}
}
Для обхода юнита нужно вызвать метод visitChildren. Если визитор переопределяет какой-либо метод обхода узла AST, то мы попадем в этот метод при передаче визитора в visitChildren. И дальше можно выполнять любые манипуляции с кодом.
Для обнаружения и инициализации плагина необходимо реализовать функцию Starter и вызвать ее в специальной директории — tools/analyzerplugin/bin/plugin.dart.
void start(Iterable<String> _, SendPort sendPort) {
ServerPluginStarter(CustomPlugin(PhysicalResourceProvider.INSTANCE))
.start(sendPort);
Это может быть отдельный пакет или подпакет, но такое расположение строго прописано в документации: это именно то место, где должен находиться инициализатор плагина.
Плагин легко конфигурировать: драйвер предоставляет доступ ко всему контенту analysis_options.yaml. Контент можно парсить и забирать из него нужные данные. Оптимальный способ — парсить файл с конфигурацией при создании драйвера.
Пример того, как мы конфигурируем плагин в своем проекте Dart code metrics:
dart_code_metrics:
anti-patterns:
- long-method
- long-parameter-list
metrics:
cyclomatic-complexity: 20
number-of-arguments: 4
metrics-exclude:
- test/**
rules:
- binary-expression-operand-order
- double-literal-format
- newline-before-return
- no-boolean-literal-compare
- no-equal-then-else
- prefer-conditional-expressions
- prefer-trailing-comma-for-collection
Мы используем листы и скалярные значения yaml.
Тестирование
При тестировании покрыть взаимодействие между сервером и плагином сложно, но остальной код отлично покрывается юнитами. Для тестирования можно использовать знакомые пакеты (test, mokito) и дополнительные функции, которые позволяют преобразовать строки или контент файла в AST.
Первая строка преобразуется в строку AST, вторая — в контент-файл, а третья исправляет все импорты и предоставляет информацию об используемых типах.
const _content = '''
Object function(Object param) {
return null;
}
''';
void main() {
test('should return correct result', () {
final sourceUrl = Uri.parse('/example.dart');
final parseResult = parseString(
content: _content,
featureSet: FeatureSet.fromEnableFlags([]),
throwIfDiagnostics: false);
final result = checkCompilationUnit(parseResult.unit);
expect(
result,
isNotEmpty,
);
});
}
Дебаг
С дебагом все непросто. Есть три способа.
Первый — использовать логи. Да, возможно, это не самый эффективный способ, но они действительно помогают. При работе над нашим проектом был случай, когда именно логи помогли понять, почему уже открытые файлы не обрабатывались плагином при редактировании.
Логи очень «шумные», там генерируется много всего. Но они могут помочь отловить некоторые ошибки.
Второй способ — посмотреть диагностику. Ее можно открыть с помощью команды Dart: Open Analyzer Diagnostics.
Третий способ — использовать Observatory и датовую VM. Но в этой статье я не буду подробно его рассматривать, потому что есть плагин для дартового биндинга к реакту. В его документации подробно и понятно описано, как дебажить Observatory.
Проблемы, с которыми можно столкнуться
Основная проблема при создании плагина — отсутствие примеров. Поэтому очень сложно разобраться, что идет не так, и найти быстрое решение. А еще достаточно сложно работать с документацией, потому что в основном она представляет собой комментарии в коде.
Нигде явно не указано, что есть возможность не только анализировать код на Dart, но и имплементировать драйвера под другие языки и пробовать их анализировать. Так, есть пример с анализом HTML для DartAngular-плагина.
Полезные ссылки
Dart code metrics — это наш опенсорсный проект по статическому анализу дартового кода. Может быть интересен тем, кто хочет попробовать себя в написании статического анализа или просто познакомиться поближе с плагинами. Представляет собой набор дополнительных правил для анализатора, а также позволяет собирать метрики по коду.
Built value — пример плагина, в котором используется дартовый драйвер.
DartAngular plugin — плагин, в котором есть пример с анализом HTML.
Over react — плагин для дартового биндинга к реакту, в котором есть полезные примеры дебага.
Автор: Dmitry Zhifarsky