Введение

Привет всем, меня зовут Коношенко Владислав, и я Flutter-разработчик из Surf. Сегодня я хотел рассказать и показать  с помощью каких инструментов можно делегировать компьютеру рутинную работу по анализу и рефакторингу кода в кратчайшие сроки. Уверяю вас, что уже после первого применения этих знаний, вы избавитесь от тонны повторяющихся замечаний на ревью кода. Также посмотрим, как расширять возможности анализатора кода и делиться своими наработками с комьюнити. Вдохновил меня на участие в данном проекте мой тимлид Дмитрий Круцких. Собственно, благодаря ему, вы и читаете эту статью.

Что такое Dart Code Metrics

Думаю, многие начинающие разработчики (и не только начинающие) хотели бы иметь наставника, который регулярно делает  code review и указывает на неточности и ошибки в написании кода. Но, к сожалению, мир не идеален, и даже самые опытные разработчики могут ошибаться. Да, и к тому же, поставив себя на их место ,можно почувствовать всю боль этой монотонной работы. Представьте такого ревьювера, который не имеет усталости, у него нет плохого настроения и , что самое главное, он доступен 24/7. Утопия!? Нет, это всем вам знакомый статический анализатор кода. В этот статье речь пойдет о Dart Code Metrics(DCM). Этот инструмент позволяет заблаговременно предупредить о возможных ошибках, а также исправить уже существующие проблемы. Это незаменимый инструмент, который начнет приносить пользу проекту с первых минут его подключения. Все правила можно гибко сконфигурировать под требования проекта, также при возникновении проблем всегда может помочь активное комьюнити в телеграм.

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

Мои первые шаги в Open Source

Проект является полностью Open Source. Это значит, что исходный код его полностью открыт для пользователей, а, следовательно, его можно доработать под свои задачи. А эти решения уже  потом могут помочь другим.

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

Развиваем dart-code-metrics

Создаем новую Issue

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

Разберемся с созданием новой issue. Это можно сделать в разделе Issue на github. У DCM есть 4 базовые issue:

  • Bug report

  • New rule proposal

  • Question

  • Rule change proposal

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

Немного о культуре создания issue. При создании старайтесь предоставить максимальный набор информации, даже если вам кажется, что информация избыточна, то для другого специалиста она может быть действительно важна. Держите в голове мысль о том, что сэкономленные вами 10 минут при создании Issue могут потратить несколько часов другого человека. Ну, и самое главное: при четко сформулированных требованиях вероятность того, что ваш баг или предложение рассмотрят быстрее -  намного выше. Вот пример хорошего сообщения.

Делаем первый PR

Первое, что мы должны сделать как порядочные контрибьюторы -  это почитать информацию, которую нам оставили предыдущие разработчики. Обычно она лежит в корне репозитория в файле CONTRIBUTING.md. В этом документе мы получаем базовую информацию для понимания следующих работ с пакетом:

  1. Создание pull request(PR)

  2. Как создавать свое правило.

  3. Как запустить проект в IDE.

Вот мы познакомились с базовыми правилами.  Дальше  я расскажу, с чего можно начать. Если у вас есть конкретные предложения, то Вы можете попробовать реализовать их самостоятельно, либо оставить свои предложения в виде Issue. Или Вы можете найти подходящую задачу во вкладке Issue. Изучая этот список, можно найти что-то интересное для себя. В поиске могут помочь механизмы github по названию label (аналог тегов или меток).  Обратите внимание на очень хороший label для начинающих - good first issue. 

После того, как вы выбрали задачку по своим силам, будет правилом хорошего тона -  сообщить в issue о том, что беретесь за нее, чтобы избежать работы нескольких человек над одним и тем же. Но при этом, если по каким-то причинам перестаете этим заниматься, то отобразите  это в  issue: с какими проблемами столкнулись , какие варианты решений пробовали. Перед началом работы обязательно проверьте отсутствие PR на это Issue. Когда все это проверили, то делаете  fork и далее по инструкции из first_contribute.

Пример написания правила

Почему это нужно

Историю моего PR можно посмотреть здесь. Прежде чем погрузить вас в написание кода, хочу сразу выделить преимущества контрибьютинг dart-code-metrics:

  1. Это возможность получить code review от других специалистов. (долгое время работал в компании, где занимался проектами самостоятельно и такого опыта действительно не хватало).

  2. Подобная работа позволяет расширить свой кругозор и понимание того, как устроен язык.

  3. Возможность автоматизировать проверку каких-то базовых вещей в коде.

  4. Приводит стиль кода к единому виду, что позволяет быстрее ориентироваться в чужом коде.

  5. Один раз потратив время на написание правил, вы решаете не только свою проблему, но и помогаете найти такие проблемы другим пользователя DCM.

Пишем код

Основный функционал DCM - это правила для статического анализа. Для примера мы возьмем мой реальный PR с правилом, которое проверяет соответствие названий файла и содержимого в нем класса.

Для написания правила нам нужен класс самого правила, который будет наследником Rule и класс визитор, который является наследником одного из базовых классов(BreadthFirstVisitor, DelegatingAstVisitor, RecursiveAstVisitor, SimpleAstVisitor, TimedAstVisitor, ThrowingAstVisitor, UnifyingAstVisitor). Их можно выбирать в зависимости от того, какие ноды вы хотите проверять. Выбрать подходящий базовый класс можно изучив список абстрактных методов данных классов.

В visitor нам нужно найти все места, в которых объявляем класс. При этом, если в файле более одного класса, то мы должны брать первый публичный.

Последовательность:

  1. Выбираем тип визитора

  2. Переопределить нужные нам абстрактные методы для поиска нодов.

  3. Собираем все необходимые нам ноды в массив.

class _Visitor extends RecursiveAstVisitor<void> {
  final _declarations = <SimpleIdentifier>[];

  Iterable<SimpleIdentifier> get declaration =>
      _declarations..sort(_compareByPrivateType);

  @override
  void visitClassDeclaration(ClassDeclaration node) {
    super.visitClassDeclaration(node);

    _declarations.add(node.name);
  }

  int _compareByPrivateType(SimpleIdentifier a, SimpleIdentifier b) {
    final isAPrivate = Identifier.isPrivateName(a.name);
    final isBPrivate = Identifier.isPrivateName(b.name);
    if (!isAPrivate && isBPrivate) {
      return -1;
    } else if (isAPrivate && !isBPrivate) {
      return 1;
    }

    return a.offset.compareTo(b.offset);
  }
}

Следующий шаг - это написание самого правила. В этом классе мы задаем идентификатор правила, ошибку которого будем выводить в консоль. Также тут пишем код, который сравнивает имя файла и имя класса. И собственно, главная часть  - это переопределение метода check в классе правила. В нем мы создаем экземпляр класса Visitor, созданный нами на предыдущем шаге, и обрабатываем полученные из него ноды. Если имя файла не соответствует имени класса, то создаем Issue и заполняем массив. Issues хранят информацию о том, где расположен код, какой тип ошибки, сообщение об ошибке, а иногда даже могут содержать hotFix.

Последовательность:

  1. Задаем имя, идентификатор правила.

  2. Переопределить метод check

  3. Получаем список нод из Visitor

  4. Пишем код, который проверяет ошибки

  5. Проверяем на ошибки ноды.

  6. Заполняем массив ошибок, если такие имеются

class PreferMatchFileName extends Rule {
  static const String ruleId = 'prefer-match-file-name';
  static const _notMatchNameFailure =
      'File name does not match with first class name';
  static final _onlySymbolsRegex = RegExp('[^a-zA-Z0-9]');

  PreferMatchFileName([Map<String, Object> config = const {}])
      : super(
          id: ruleId,
          documentation: const RuleDocumentation(
            name: 'Prefer match file name',
            brief: 'Warns when file name does not match class name.',
          ),
          severity: readSeverity(config, Severity.warning),
          excludes: readExcludes(config),
        );

  bool _hasMatchName(String path, String className) {
    final classNameFormatted =
        className.replaceAll(_onlySymbolsRegex, '').toLowerCase();

    return classNameFormatted ==
        basenameWithoutExtension(path)
            .replaceAll(_onlySymbolsRegex, '')
            .toLowerCase();
  }

  @override
  Iterable<Issue> check(InternalResolvedUnitResult source) {
    final visitor = _Visitor();
    source.unit.visitChildren(visitor);

    final _issue = <Issue>[];

    if (visitor.declaration.isNotEmpty &&
        !_hasMatchName(source.path, visitor.declaration.first.name)) {
      final issue = createIssue(
        rule: this,
        location: nodeLocation(
          node: visitor.declaration.first,
          source: source,
          withCommentOrMetadata: true,
        ),
        message: _notMatchNameFailure,
      );

      _issue.add(issue);
    }

    return _issue;
  }
}

Покрываем правило тестами

Для того, чтобы протестировать правило, создадим файл с тестом и рядом директорию с examples. В эту папку нужно положить файлы для теста. Мы проверяем инициализацию правила. Далее проверим файл без ошибок на отсутствие ложноположительных срабатываний. И самое главное -  проверка файла с неправильным именем класса на то, что линтер действительно обнаружил ошибку.

void main() {
  group('PreferMatchFileName', () {
    test('initialization', () async {
      final unit = await RuleTestHelper.resolveFromFile(_withSingleClass);
      final issues = PreferMatchFileName().check(unit);

      RuleTestHelper.verifyInitialization(
        issues: issues,
        ruleId: 'prefer-match-file-name',
        severity: Severity.style,
      );
    });

    test('reports no issues', () async {
      final unit = await RuleTestHelper.resolveFromFile(_withSingleClass);
      final issues = PreferMatchFileName().check(unit);

      RuleTestHelper.verifyNoIssues(issues);
    });

    test('reports about found issues for incorrect class name', () async {
      final unit = await RuleTestHelper.resolveFromFile(_withIssue);
      final issues = PreferMatchFileName().check(unit);

      RuleTestHelper.verifyIssues(
        issues: issues,
        startOffsets: [6],
        startLines: [1],
        startColumns: [7],
        endOffsets: [13],
        messages: ['File name does not match with first class name'],
        locationTexts: ['Example'],
      );
    });

    test('reports no issues for statefull widget class', () async {
      final unit = await RuleTestHelper.resolveFromFile(_withStateFullWidget);
      final issues = PreferMatchFileName().check(unit);

      RuleTestHelper.verifyNoIssues(issues);
    });

    test('reports no issues for empty file', () async {
      final unit = await RuleTestHelper.resolveFromFile(_emptyFile);
      final issues = PreferMatchFileName().check(unit);

      RuleTestHelper.verifyNoIssues(issues);
    });

    test('reports no issues for file with only private class', () async {
      final unit = await RuleTestHelper.resolveFromFile(_privateClass);
      final issues = PreferMatchFileName().check(unit);

      RuleTestHelper.verifyNoIssues(issues);
    });

    test('reports no issues for file multiples file', () async {
      final unit = await RuleTestHelper.resolveFromFile(_multiClass);
      final issues = PreferMatchFileName().check(unit);

      RuleTestHelper.verifyNoIssues(issues);
    });
  });
}
Отбражение предупреждения во вкладке Dart Analysis
Отбражение предупреждения во вкладке Dart Analysis

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

Заключение

Мы рассмотрели инструмент для анализа кода и поговорили о том, насколько полезно его использование. Я постарался поделиться своими знаниями  в создании нового правила. Это был мой первый contribute, для меня это был очень интересный опыт. Как оказалось, вносить изменения в open source совсем несложно. Вот так, потратив пару часов, мы автоматизировали такую проверку для своего проекта, а также для всех тех, кто захочет подключить такое правило к своим проектам, используя Dart Code Metrics.

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