Dart Code Metrics — это инструмент статического анализа кода, который позволяет собирать метрики по коду и предоставляет дополнительные правила для анализатора. Основная задача — помогать разработчикам следить за качеством кода и улучшать его. В этой статье мы хотим поделиться возможностями инструмента с сообществом. Он помог нам в Wrike решить часть проблем на фронтенде, и, надеемся, поможет и вам.
Инструмент можно запускать из командной строки, подключать в виде плагина к Dart Analysis Server, а также в виде библиотеки. Запуск из командной строки позволяет легко интегрировать инструмент в процесс CI/CD, при этом результат анализа можно получить в одном из форматов: Сonsole, HTML, JSON, CodeClimate, GitHub. Подключение в виде плагина к Analysis Server позволяет получать оперативную обратную связь непосредственно в IDE.
Зачем мы решили создать такой инструмент? В Wrike уже написано примерно 2.5 миллиона строк кода на Dart. При таком размере кодовой базы невольно задумываешься о том, какова цена поддержки такого объема кода, как понять, что пора проводить рефакторинг, и с каких мест его стоит начать.
Вместе с Dart SDK нам поставляется Analyzer, у которого есть встроенный линтер с богатым набором правил. В официальном pub вы также можете встретить рекомендованные наборы правил: pedantic, effective_dart и т.д. Это помогает нам избежать глупых ошибок и поддерживать кодовую базу в той стилистике, которую предлагают авторы языка. Но мы столкнулись с тем, что нам не хватало аналитической информации по коду, а дальше завертелось.
Метрики
Из книг и статей про оценку и дизайн программного кода (например, отсюда) мы поняли, что первый шаг, который нужно сделать, — реализовать сбор метрик с кода.
Сейчас анализатор собирает такие метрики:
Cyclomatic Complexity
Lines of Executable Code
Lines of Code
Number of Parameters
Number of Methods
Maximum Nesting
Weight of Class
Для каждой метрики мы задали базовое пороговое значение, после которого рекомендуется провести рефакторинг кода. При этом пороговые значения могут быть легко предопределены и переданы инструменту при вызове из консоли или при конфигурации плагина для анализатора через analysis_options.yaml, подробнее про это можно почитать здесь.
Следующий шаг — опираясь на данные нескольких метрик, инструмент находит антипаттерны в кодовой базе. На текущий момент реализовано всего два антипаттерна — long-method и long-parameter-list. Мы думаем о том, чтобы расширить этот список.
Если с метриками типа Number of Parameters или Number of Methods достаточно легко разобраться, то как же считается, например, Cyclomatic Complexity?
Цикломатическая сложность части программного кода — это количество линейно независимых маршрутов через программный код. Например, если исходный код не содержит никаких точек ветвления или циклов, то сложность равна единице, поскольку есть только единственный маршрут через код. Если код имеет единственный оператор if, содержащий простое условие, то существует два пути через код: первый — если условие оператора if имеет значение true, второй — если false. Подробнее про цикломатическую сложность можно почитать в статье на Википедии.
Большое количество независимых маршрутов в теле функции оказывает влияние на читаемость и поддержку кода, поэтому в таких случаях предпочтительнее разбивать функцию на несколько.
Для визуализации собранных метрик инструмент предоставляет различные форматы отчетов, подробнее о них я расскажу в части «Отчеты».
Правила
Изначально инструмент собирал только метрики, но мы решили пойти дальше и добавить линтинг. В других экосистемах есть полезные правила: например, проверка неиспользуемых аргументов, порядок членов класса и другие. Во встроенном дартовом линтере их нет, поэтому мы сделали свои.
Почему мы решили добавить линтинг в отдельный пакет, а не сделать PR во встроенный анализатор? Мы решили иначе подойти к конфигурации правил и сделать ее более расширенной, например, правило на проверку порядка членов класса принимает конфигурацию в виде упорядоченного списка членов класса, по которому происходит проверка.
Текущий список правил выглядит так:
Общие
avoid-unused-parameters
binary-expression-operand-order
double-literal-format
member-ordering
member-ordering-extended
newline-before-return
no-boolean-literal-compare
no-empty-block
no-equal-arguments
no-equal-then-else
no-magic-number
no-object-declaration
prefer-conditional-expressions
prefer-trailing-comma
Специально для использования с библиотекой Intl
prefer-intl-name
provide-correct-intl-args
Специально под Dart Angular
avoid-preserve-whitespace-false
component-annotation-arguments-ordering
prefer-on-push-cd-strategy
Актуальный список вы всегда можете посмотреть в документации.
Речь идет не только про стилистические правила, но и про те, которые подсвечивают потенциальные ошибки: например, no-equal-then-else, no-equal-arguments и другие.
Часть наших правил основаны на проблемах, с которыми мы сталкиваемся на ревью и которые хотим покрывать автоматически, чтобы иметь возможность сосредотачиваться на самом главном. Другая часть появилась в процессе изучения списка правил таких инструментов, как PVS-Studio, TSLint, ESLint (большое спасибо им за вдохновение).
Рассмотрим подробнее некоторые из них.
Avoid unused parameters. Проверяет наличие неиспользуемых параметров у функций или методов. Наличие неиспользуемого параметра может говорить как о том, что параметр перестал быть нужен в ходе рефакторинга, так и том, что где-то в теле функции или метода используется какая-то переменная вместо него. Если в первой ситуации правило помогает убрать неиспользуемый код, то во второй — указывает на возможную ошибку.
Простой пример:
String method(String value) => “”;
Здесь параметр value не используется, и для него анализатор выведет сообщение “Parameter is unused”.
Учитывая то, что в Dart можно наследоваться от любого класса и параметр может быть необходим в наследниках, в базовом классе он может быть переименован в _ . Тогда анализатор его пропустит.
Например:
String method(String _) => “”;
Prefer trailing comma. Проверяет последнюю запятую для аргументов, параметров, перечислений и коллекций при условии, что они занимают несколько строк.
Например:
void function(
String firstArgument, String secondArgument, String thirdArgument) {
return;
}
Для такой функции правило предложит добавить в конец запятую, чтобы после форматирования она превратилась в:
void function(
String firstArgument,
String secondArgument,
String thirdArgument,
) {
return;
}
Если параметры изначально помещались на одной строке, то анализатор не будет считать это ошибкой:
void function(String arg1, String arg2, String arg3) {
return;
}
Для правила также можно указать параметр break-on, который включает дополнительную проверку для указанного количества элементов. Например, без параметра break-on пример выше считается корректным. Но если сконфигурировать правило на break-on: 2, то анализатор покажет ошибку для этой функции и предложит добавить запятую.
No equal arguments. Проверяет передачу одного и того же аргумента более одного раза при создании инстанса класса или вызове метода/функции.
Предположим, что существует некоторый класс User и отдельная функция, создающая этого юзера:
class User {
final String firstName;
final String lastName;
const User(this.firstName, this.lastName);
}
User createUser(String lastName) {
String firstName = getFirstName();
return User(
firstName,
firstName,
);
}
Оба поля класса User являются строкам. Легко не заметить, что при его создании передается одна и та же переменная. В таких случаях правило покажет, что переменная firstName передается более одного раза, и это может быть ошибкой.
Member ordering extended. Проверяет порядок членов класса. Это правило получило постфикс extended: у нас уже было правило Member ordering, но оно было не настолько гибким.
Правило принимает конфигурацию порядка по достаточно гибкому шаблону. Он позволяет указывать не только тип члена класса (поле, метод, конструктор и др.), но и такие ключевые слова, как late, const, final или, например, признак nullable.
Конфигурация может быть задана, например, так:
- public-late-final-fields
- private-late-final-fields
- public-nullable-fields
- private-nullable-fields
- named-constructors
- factory-constructors
- getters
- setters
- public-static-methods
- private-static-methods
- protected-methods
- etc.
Или упрощена до:
- fields
- methods
- setters
- getters (или просто **getters-setters** если нет необходимости разделять)
- constructors
Подробнее про возможности правила можно почитать в документации.
Дополнительно правило может требовать сортировку по алфавиту. Для этого в его конфигурацию нужно передать `alphabetize: true`.
Отчеты
Для удобной визуализации собранных метрик и результатов линтинга инструмент предоставляет отчеты в одном из следующих форматов:
Console
HTML
JSON
CodeClimate
GitHub
Консольный формат является форматом по-умолчанию, формат может быть передан консольной утилите с помощью флага --reporter.
Например, при выполнении команды:
$ dart pub run dart_code_metrics:metrics lib
# or for a Flutter package
$ flutter pub run dart_code_metrics:metrics lib
При выполнении команды на кодовой базе Dart Code Metrics мы получили такой отчет в консоль:
Если захотим выбрать другой тип репортера (например, HTML), то нужно выполнить следующую команду:
$ dart pub run dart_code_metrics:metrics lib --reporter=html
# or for a Flutter package
$ flutter pub run dart_code_metrics:metrics lib --reporter=html
Сгенерируется отчет в папке metrics. Результирующую папку также можно передать с помощью флага --output-directory или -o:
В отчете можно посмотреть каждый файл отдельно:
Чтобы посмотреть подробную информацию по метрикам, нужно навести курсор на значки слева от кода:
Если вы используете GitHub Workflows и хотите получить репорт сразу в созданных PR, необходимо добавить в пайплайн новый шаг:
jobs:
your_job_name:
...
steps:
...
- name: Run Code Metrics
run: dart run bin/metrics.dart --reporter=github lib
Это позволит получить отчеты в таком формате:
Подробную информацию про все типы метрик и способы конфигурации можно посмотреть в документации.
Как подключить
Если вы хотите попробовать пакет у себя, то вот краткий гайд о том, как его подключить в виде плагина для дартового анализатора.
Шаг 1: установите пакет как dev-зависимость.
$ dart pub add --dev dart_code_metrics
# or for a Flutter package
$ flutter pub add --dev dart_code_metrics
ИЛИ
Добавьте пакет вручную в pubspec.yaml.
Важно: если ваш пакет еще не переведен на null safety, используйте версию 2.4.1.
dev_dependencies:
dart_code_metrics: ^3.0.0
Запустите команду установки зависимостей.
$ dart pub get
# or for a Flutter package
$ flutter pub get
Шаг 2: добавьте конфигурацию в analysis_options.yaml.
analyzer:
plugins:
- dart_code_metrics
dart_code_metrics:
anti-patterns:
- long-method
- long-parameter-list
metrics:
cyclomatic-complexity: 20
lines-of-executable-code: 50
number-of-parameters: 4
maximum-nesting-level: 5
metrics-exclude:
- test/**
rules:
- newline-before-return
- no-boolean-literal-compare
- no-empty-block
- prefer-trailing-comma
- prefer-conditional-expressions
- no-equal-then-else
В этой конфигурации указаны антипаттерны long-method и long-parameter-list, метрики с их пороговыми значениями и правила. Весь доступный список метрик можно посмотреть здесь, а список правил — здесь.
Шаг 3: перезагрузите IDE, чтобы анализатор смог обнаружить плагин.
Если вы хотите использовать пакет как CLI, то с документацией по использованию можно ознакомиться здесь.
Заключение
Учитывая популярность Flutter, мы думаем над тем, какие метрики и правила могли бы помочь разработчикам на этом фреймворке. При этом мы открыты к предложениями: если у вас есть идеи правил или метрик, которые могли бы быть полезны разработчикам, пишущим на Dart или Flutter, смело пишите нам или оставляйте issue на GitHub. Мы будем рады добавить их и сделать жизнь разработчиков еще лучше.
Наши планы на будущее всегда можно увидеть в публичном репозитории проекта в списке issue или в проектах, там же вы можете оставить обратную связь по инструменту.
ookami_kb
Здесь не понял. Встроенный анализатор ведь тоже это умеет, и настройку уровня, и падение. В чем разница?
incendial Автор
Действительно, написано некорректно и поведение по строгости аналогично встроенному анализатору. Основной посыл был в другом подходе к конфигурации правил.
Статью поправлю, спасибо, что обратили внимание!