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 или в проектах, там же вы можете оставить обратную связь по инструменту.