Данная статья является подробной текстовой версией этого видео:
- 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
Спасибо, что дочитали до этого момента. Если у вас есть какие-либо вопросы, пожалуйста, задавайте их в комментариях.
Хорошего кодирования!