Пользовательский коп за 30 минут

Данная статья является подробной текстовой версией этого видео:

  • Custom cop in 30 min — YouTube

Я расскажу, как с нуля создать пользовательское правило для Ruby lint с помощью RuboCop (он же «пользовательский cop»). В этом уроке я напишу пример cop для сортировки литеральных элементов Hash по их ключам.

Начните с шаблона

Коп, который будет создан в этом руководстве, будет реализован в виде плагина RuboCop. Поскольку RuboCop еще не имеет механизма плагинов, нам придется написать много дополнительного кода, чтобы заставить его работать как плагин.

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

  • https://github.com/r7kamura/rubocop-extension-template

Обзор

Чтобы добавить новый копм, вам нужно выполнить следующие шаги:

  • Добавить тест
    • spec/rubocop/cop/my_extension/hash_literal_order_spec.rb
  • Добавить класс cop
    • lib/rubocop/cop/my_extension/hash_literal_order.rb
  • Загрузить класс cop
    • lib/my_extension.rb
  • Добавьте конфигурацию по умолчанию
    • default.yml

Сам RuboCop и плагины, такие как rubocop-rspec и rubocop-rails, также реализованы аналогичным образом.

Добавить тест

В основном, тесты копирования пишутся на RSpec. Вам не нужно писать RSpec раньше. Просто скопируйте и вставьте существующий код примера, и он будет работать.

Все, что вам нужно знать, это то, что эти 3 метода официально предоставляются RuboCop.

  • expect_offense(code_with_message)
  • expect_no_offenses(code)
  • expect_correction(code)

MyExtension/HashLiteralOrder — это имя копа, который мы пишем.
Каждый коп имеет соответствующий класс, и они определены в пространстве имен ::RuboCop::Cop.
Это, очевидно, не является исключением, даже для сторонних плагинов.
Поэтому минимальный пример нашего тестового кода будет выглядеть следующим образом:

# spec/rubocop/cop/my_extension/hash_literal_order_spec.rb
RSpec.describe RuboCop::Cop::MyExtension::HashLiteralOrder, :config do
  it 'autocorrects offense' do
    expect_offense(<<~TEXT)
      { b: 1, c: 1, a: 1 }
      ^^^^^^^^^^^^^^^^^^^^ Sort Hash literal entries by key.
    TEXT

    expect_correction(<<~RUBY)
      { a: 1, b: 1, c: 1 }
    RUBY
  end
end
Вход в полноэкранный режим Выход из полноэкранного режима

expect_offense принимает в качестве аргумента не только код Ruby, но и строку с сообщением о нарушении. Это немного сложно, но на самом деле полезно.

Добавьте класс полицейского

После того, как вы написали тесты, пришло время написать реализацию.

Я напишу почти полную реализацию полицейского.
Я знаю, что есть много частей, которые вы не знаете, но не паникуйте.

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

# lib/rubocop/cop/my_extension/hash_literal_order.rb
module RuboCop
  module Cop
    module MyExtension
      # By convention, we write fairly detailed documentation for each cop
      # using YARD comments. Even official RuboCop cops do so, and the official
      # documentation site is automatically generated from these comments there.
      # e.g. https://docs.rubocop.org/rubocop/cops_layout.html
      #
      #
      # Sort Hash literal entries by key.
      #
      # @example
      #
      #   # bad
      #   {
      #     b: 1,
      #     a: 1,
      #     c: 1
      #   }
      #
      #   # good
      #   {
      #     a: 1,
      #     b: 1,
      #     c: 1
      #   }
      #
      #
      # As I mentioned previously, if you want to create `MyExtenison/HashLiteralOrder` cop,
      # the class name should be `::RuboCop::Cop::MyExtension::HashLiteralOrder`.
      # All cop classes must inherit from RuboCop::Cop::Base,
      # so here we wrote `< Base` as well.
      class HashLiteralOrder < Base

        # To support `--autocorrect` in this cop,
        # we need to extend this module.
        extend AutoCorrector

        # If you define `MSG` constant in cop class,
        # RuboCop will use it as an offense message for this cop.
        MSG = 'Sort Hash literal entries by key.'

        # RuboCop provides `def_node_matcher` to easily define pattern-matching method,
        # which takes a Symbol method name and a String AST pattern,
        # In this case, it defines `#hash_literal_order` method,
        # which takes an AST node as argument and returns a boolean value.
        #
        # For example, this method returns true for `{ a: 1, b: 2 }`,
        # and returns false for `[1, 2]` and `{ a => b }`.
        #
        # @!method hash_literal?(node)
        # @param [RuboCop::AST::HashNode] node
        # @return [Boolean]
        def_node_matcher :hash_literal?, <<~PATTERN
          (hash
            (pair
              {sym | str}
              _
            )+
          )
        PATTERN

        # This is a callback method that is called when RuboCop detects
        # a Hash node. We use this mechanism to implement the registration
        # and autocorrection of offenses when we find a code pattern that
        # we want to detect.
        #
        # The key method is `add_offense`.
        # By calling this, you can tell RuboCop that you've found an offense.
        #
        # In this case, if the keys of the target Hash node are all Symbols
        # or Strings and not sorted by name, we consider it an offense,
        # then replace the entire Hash node with a new code, which is
        # specified by the Ruby code as a String.
        #
        # @param [RuboCop::AST::HashNode] node
        def on_hash(node)
          return unless hash_literal?(node)

          return if sorted?(node)

          add_offense(node) do |corrector|
            corrector.replace(
              node,
              autocorrect(node)
            )
          end
        end

        private

        # ... some internal implementation ...
        # ... such as `#sorted?(node)`,
        # ... `#autocorrect(node)`, and so on ...
      end
    end
  end
end
Вход в полноэкранный режим Выход из полноэкранного режима

Детали не описываются из-за ограниченности места, поэтому, пожалуйста, читайте оригинальный исходный код здесь:

  • https://github.com/r7kamura/sevencop/blob/main/lib/rubocop/cop/sevencop/hash_literal_order.rb

Сопоставление шаблонов AST

Синтаксис этого шаблона имеет довольно странный стиль и может быть непонятен с первого взгляда. Легкое для понимания объяснение есть в Node Pattern :: RuboCop Docs, поэтому лучше сначала написать его вручную, глядя на это.

(hash
  (pair
    {sym | str}
    _
  )+
)
Вход в полноэкранный режим Выход из полноэкранного режима

В геме парсера есть полезный инструмент CLI под названием ruby-parse, который показывает результаты парсинга в виде AST, и я часто проверяю его при написании подобных Cop:

$ gem install parser
$ echo '{ a: 1, b: 1 }' > example.rb
$ ruby-parse example.rb
(hash
  (pair
    (sym :a)
    (int 1))
  (pair
    (sym :b)
    (int 1)))
Войти в полноэкранный режим Выйти из полноэкранного режима

Загрузить класс cop

Не забудьте загрузить файл класса cop.
Иногда я забываю об этом и путаюсь.

# lib/my_extension.rb
require 'rubocop/cop/my_extension/hash_literal_order'
Войти в полноэкранный режим Выход из полноэкранного режима

Добавить стандартный конфиг

Наконец, не забудьте добавить конфигурацию по умолчанию для добавленного вами копа.

Каждый коп может иметь свои собственные значения конфигурации, и это обычная практика для плагинов RuboCop — включать свои настройки по умолчанию в default.yml.

# default.yml
MyExtension/HashLiteralOrder:
  Description: |
    Sort Hash literal entries by key.
  Enabled: true
  VersionAdded: '0.1'
Вход в полноэкранный режим Выход из полноэкранного режима

Коп завершен! Этот коп работает следующим образом.

$ bundle exec rubocop
Inspecting 1 files
C

Offenses:

example.rb:3:1: C: [Correctable] MyExtension/HashLiteralOrder: Sort Hash literal entries by key.
{ b: 2, a: 1, c: 3 }
^^^^^^^^^^^^^^^^^^^^

1 files inspected, 1 offense detected, 1 offense autocorrectable
Войти в полноэкранный режим Выйти из полноэкранного режима

Тот же самый коп, который мы создали здесь, включен в мой gem под названием sevencop. Вы можете попробовать этот коп в следующем репозитории.

  • https://github.com/r7kamura/sevencop-hash-literal-order-example

Подведение итогов

В этом руководстве я объяснил, как создать пользовательский коп с нуля.

Готовая версия исходников этого копа доступна на GitHub:

  • https://github.com/r7kamura/sevencop/blob/main/lib/rubocop/cop/sevencop/hash_literal_order.rb

Как уже упоминалось в начале статьи, я также предоставляю видео по кодированию в реальном времени для этого урока. Если у вас есть вопросы, не стесняйтесь, задавайте их в комментариях к этой статье или в видео.

  • Пользовательский копм за 30 минут — YouTube

Если вы хотите узнать больше о том, как создавать пользовательские копы, я рекомендую вам прочитать эти страницы:

  • https://github.com/rubocop/rubocop
  • https://github.com/rubocop/rubocop-ast
  • https://github.com/whitequark/parser

Я разместил другие руководства, если вы хотите посмотреть их.

  • Расширение Chrome за 20 минут — Сообщество DEV 👩💻👨💻
  • r7kamura — YouTube

Спасибо, что дочитали до этого момента. Если у вас есть какие-либо вопросы, пожалуйста, задавайте их в комментариях.

Хорошего кодирования!

Оцените статью
devanswers.ru
Добавить комментарий