В предыдущей статье мы анонсировали Dart Code Metrics — инструмент статического анализа кода. Сегодня я расскажу про новые возможности, которые появились в Dart Code Metrics с выходом очередного мажорного обновления. Поговорим про появление команд, поддержку монорепозиториев, улучшения в интеграции с CI/CD, и, конечно же, про новые правила для анализатора. Теперь у инструмента появился сайт с документацией, его можно найти здесь

Команды

Изначально задача Dart Code Metrics заключалась в сборе и удобном представлении метрик по коду, а также предоставлении дополнительных правил для анализатора с более гибкой конфигурацией. Но мы решили не останавливаться на этом, расширили возможности инструмента и добавили команды, которые служат общей цели — помогать поддерживать качество кодовой базы на должном уровне.

Теперь подробнее про одну из первых команд — check-unused-files. Ее задача — искать неиспользуемые дартовые файлы.

$ dart run dart_code_metrics:metrics check-unused-files lib

Полное описание команды:

Check unused *.dart files.

Usage: metrics check-unused-files [arguments] <directories>

-h, --help                                        Print this usage information.

-r, --reporter=<console>                          The format of the output of the analysis.

                                                  [console (default), json]

    --root-folder=<./>                            Root folder.

    --exclude=<{/**.g.dart,/**.template.dart}>    File paths in Glob syntax to be excluded.

                                                  (defaults to "{/**.g.dart,/**.template.dart}")

В какой ситуации может понадобиться поиск неиспользуемых файлов? 

Навскидку приходят как минимум два сценария: 

  1. Если вам нужно провести поэтапный рефакторинг на большой кодовой базе с перемещением файлов, то сложно понять, все ли файлы задействованы корректно.

  2. В Dart очень популярна кодогенерация, создающая дополнительные файлы. Иногда бывает сложно понять, что эти файлы больше не используются.

Как поиск работает под капотом

Поиск неиспользуемых файлов начинается с составления списка файлов, которые находятся внутри переданных директорий и в поддиректориях. После в каждом файле проверяется список import, export и part директив. Если импортируемый или экспортируемый файл принадлежит данному пакету, то он помечается как используемый. Файлы с экспортами или файлы, содержащие main-функцию (например, с тестами), отдельно помечаются как точки входа и как используемые.

После неиспользуемые файлы выводятся в формате:

Unused file: <path_to_file>

...

Unused file: <path_to_file>

Total unused files - N

Пример использования команды

Для примера возьмем проект с такой структурой::

lib/
src/
entry_point.dart
first_file.dart
second_file.dart

И таким контентом файлов:

entry_point.dart

import ‘first_file.dart’

void main() {
    ... // some code
}

first_file.dart

class SomeClass {
    ...
}

second_file.dart

import ‘first_file.dart’

class SomeOtherClass {
    ...
}

Вызовем команду:

$ dart run dart_code_metrics:metrics check-unused-files lib

На первом шаге алгоритм определит, что в проекте содержатся всего три файла, и начнет их обходить. После обхода entry_point.dart файл first_file.dart помечается как используемый, так как он импортируется в entry_point.dart. При этом сам entry_point.dart будет также помечен как используемый, потому что он объявляет функцию main. Далее алгоритм проверит first_file.dart, который не имеет директив, а после перейдет к second_file.dart. Так как на second_file.dart не ссылается ни один из предыдущих файлов, то он в результате будет выведен как неиспользуемый.

Unused file: lib/src/second_file.dart

Total unused files - 1

Мы можем исключить second_file.dart из результатов, передав опцию: --exclude.

$ dart run dart_code_metrics: metrics check-unused-files lib --exclude=”lib/**/second_file.dart”

В таком случае команда выведет:

No unused files found!

Почему мы решили сделать отдельную команду, а не добавили поиск неиспользуемых файлов в существующий вызов и не сделали поддержку для плагина анализатора? Поиск неиспользуемых файлов выглядит как нечастая функция: постоянная проверка — это оверхэд, которого хотелось бы избежать.

Предыдущий вызов анализа кода теперь также доступен в виде команды, и называется analyze. Если раньше вы вызвали cli с помощью 

$ dart run dart_code_mertics:metrics lib

то теперь это будет выглядеть так:

$ dart run dart_code_mertics:metrics analyze lib

В версии 4.0 мы оставили поддержку dart run dart_code_mertics:metrics lib. Она будет работать как минимум до версии 5.0, поэтому на текущий момент никаких изменений на CI не потребуется.

Поддержка монорепозиториев

Начиная с версии 4.1 мы стали лучше поддерживать монорепозитории: теперь контекст анализа определяется корректно для каждого пакета внутри монорепозитория. Это распространяется как на ситуацию, когда в каждом пакете есть отдельный analysis_options.yaml, так и для ситуаций, когда в руте монорепозитория всего один analysis_options.yaml.

Поддержка монорепозиториев позволила поддержать сценарий использований Dart Code Metrics с такими инструментами как melos. Теперь вы можете добавить шаг в melos.yaml, и при его выполнении будет использоваться корректный analysis_options.yaml для каждого пакета монорепозитория. 

Конфигурация melos может выглядеть так:

metrics:
  run: |
    melos exec -c 1 --ignore="*example*" -- \
      flutter pub run dart_code_metrics:metrics analyze lib
  description: |
    Run `dart_code_metrics` in all packages.
     - Note: you can also rely on your IDEs Dart Analysis / Issues window.

Улучшения по интеграции в CI/CD процессы

Мы также работаем над улучшением интеграции в различные CI/CD процессы. В этом релизе добавили GitHub Action: он позволяет не только проще интегрировать Dart Code Metrics, но и содержит более подробный отчет, чем раньше поддерживала cli опция --reporter=github. 

Как может выглядеть конфигурация GitHub Action:

name: Dart Code Metrics

on: [push]

jobs:
  check:
    name: dart-code-metrics-action

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: dart-code-metrics
        uses: dart-code-checker/dart-code-metrics-action@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}

Более подробно про конфигурацию гитхаб экшена вы можете посмотреть в документации

Не осталась без внимания и интеграция с GitLab. Теперь в документации появилось описание того, как можно встроить Dart Code Metrics.

Конфигурация может выглядеть так:

code_quality:
  image: google/dart
  before_script:
    - dart pub global activate dart_code_metrics
  script:
    - dart pub global run dart_code_metrics:metrics lib -r gitlab > code-quality-report.json
  artifacts:
    reports:
      codequality: code-quality-report.json

Новые правила для Flutter

В этом релизе мы сконцентрировались на правилах для Flutter и добавили сразу четыре новых.

Prefer extracting callbacks. Проверяет, что колбеки, используемые для аргументов виджета типа onPressed, вынесены в отдельные методы, а не имплементированы по месту. Основная задача этого правила — сделать код виджетов внутри build метода более простым для восприятия. 

Например, рассмотрим виджет:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return TextButton(
      style: ...,
      onPressed: () {        
        // Some 
        // Huge
        // Callback
      },
      child: ...
    );
  }
}

В крайних случаях код таких колбеков может достигать большую часть кода build метода виджета, что сильно затрудняет восприятие того, из каких частей состоит виджет.

При этом правило не срабатывает для однострочных колбэков — код типа onPressed: () => _handler(...) или onPressed: _handler является корректным с точки зрения правила.

Подробнее про это правило можете прочитать в документации.

Avoid unnecessary setstate. Проверяет, что setState не вызывается синхронно внутри initState, didUpdateWidget и build методах виджета. Такой вызов setState приводит к дополнительным перерисовкам виджета, в которых нет необходимости. 

Рассмотрим пример, в котором правило покажет ошибку:

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  String myString = '';
  
  @override
  void initState() {
    super.initState();
    setState(() => myString = "Hello");
  }
  
  @override
  Widget build(BuildContext context) {
    return ...
  }
}

В этом примере setState(() => myString = "Hello"); приведет к дополнительной перерисовке, поэтому правило подсветит такое использование setState.

Рассмотрим другой пример:

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  String myString = '';
  
  @override
  void initState() {
    super.initState();
    myAsyncMethod();
  }
  
  Future<void> myAsyncMethod() async {
    final data = await service.fetchData();
    setState(() {
      myString = data;
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return ...
  }
}

Вызов setState в данном примере не приведет к срабатыванию правила, так как этот вызов происходит из асинхронного метода.

Подробнее про это правило можете прочитать в документации.

Avoid wrapping in padding. Проверяет, что виджет, обернутый в Padding виджет, сам не имеет настройки отступов, которая может быть использована вместо оборачивания в Padding виджет. 

class CoolWidget extends StatelessWidget {
  ...
  
  Widget build(...) {
    return Padding(
      padding: const EdgeInsets.only(right: 20),
      child: Container(),
    );
  }
}

Избавление от избыточного паддинга позволяет не добавляет дополнительный отступ в build методе виджета, что делает его более читаемым и позволяет сделать дерево виджетов меньше на один уровень.

Подробнее с документацией можете ознакомиться здесь.

Always remove listener. Проверяет, что подписка, добавленная через .addListener(...), удаляется в dispose методе виджета. Неудаление подписки приводит к утечкам памяти, что может оказать значительное влияние на производительность в ситуациях, когда виджет, создающий подписку, инициализируется большое количество раз в рамках жизненного цикла приложения.

class ShinyWidget extends StatefulWidget {
  const ShinyWidget();
  
  @override
  _ShinyWidgetState createState() => _ShinyWidgetState();
}

class _ShinyWidgetState extends State<ShinyWidget> {
  final _scrollController = ScrollController();
  
  const _ShinyWidgetState();
  
  @override
  void initState() {
    super.initState();
    _scrollController.addListener(listener);
  }
  
  void dispose() {
    super.dispose();
  }
  
  void listener() {
    // ...
  }
}

В этом примере правило покажет, что подписка _scrollController.addListener(listener); не была убрана в dispose.

Подробнее с документацией можете ознакомиться здесь.

Note: это правило не обрабатывает дартовые стримы, поэтому оно помечено как экспериментальное. Если у вас есть идеи о том, как его можно  улучшить, напишите нам!

Мы будем рады любому фидбэку и вашим идеям по новым правилам, метрикам или командам! Нас всегда можно найти в чате комьюнити в телеграмме или оставить нам issue на GitHub. Мы также будем рады новым контрибьюторам. Если вам интересно погрузиться в мир статического анализа, но вы не знаете с чего начать, то мы с радостью расскажем о том, как инструмент устроен изнутри и поможем имплементировать ваше первое правило.

Мы будем благодарны, если вы заполните небольшой опрос. Это поможет нам понять, в каком направлении стоит развивать проект и какие фичи добавить в следующих релизах.

Комментарии (4)


  1. nikita_dol
    16.08.2021 18:59
    +1

    Файлы с экспортами или файлы, содержащие main-функцию (например, с тестами), отдельно помечаются как точки входа и как используемые.

    Ещё, что либо помеченное этой аннотацией, будет считаться точкой входа

    @pragma('vm:entry-point')


    1. incendial Автор
      17.08.2021 14:06

      Интересно, не знал про это, спасибо! А можете привести пример из реального кода приложения на Flutter, где бы такие аннотации могли бы использоваться?


      1. nikita_dol
        17.08.2021 14:14

        У меня есть приложение, у которого 2 активити: главная и нотификация (например, для будильника или звонков). Поэтому было решено сделать так, что бы она стартовала из другой функции. Но так как она не main , то компилятор её вырезает. Поэтому пометка этой аннотацией говорила ему, что это не нужно вырезать


        1. incendial Автор
          18.08.2021 14:01

          Спасибо! Добавим поддержку для этой аннотации.