背景
Time.zone.now
をTime.current
に変更したため、ついにルールとしてRuboCopに追加しました。
環境
❯ ./bin/rails --version
Rails 7.1.1
❯ bundle exec rubocop --version
1.57.2
Custom Copを追加
- lib/custom_cops/ban_time_zone_now.rb
install parser (AST用)
❯ gem install parser
Fetching ast-2.4.2.gem
Fetching parser-3.2.2.4.gem
Successfully installed ast-2.4.2
Successfully installed parser-3.2.2.4
Parsing documentation for ast-2.4.2
Installing ri documentation for ast-2.4.2
Parsing documentation for parser-3.2.2.4
Installing ri documentation for parser-3.2.2.4
Done installing documentation for ast, parser after 26 seconds
2 gems installed
patternを定義する(Time.zone.now)
❯ ruby-parse -e 'Time.zone.now'
(send
(send
(const nil :Time) :zone) :now)
nil
をnil?
にするべきなので、patternは下のようになります。
(send (send (const nil? :Time) :zone) :now)
RESTRICT_ON_SEND
これは特に定義しなくても動作しますが、RuboCopをより効率的に動かすために、定義しておくべきです。
定数 RESTRICT_ON_SEND は最適化のための特別な配列です。この中に含まれるメソッドが呼び出されたときだけ on_send を実行するように制限します。この制限がない場合、すべてのメソッドに対して on_send を呼び出し、パターンマッチの計算を行うために実行時間が増えてしまいます。
注意点
上記を見ると、zone
かnow
のどちらかを入れても良さそうですが、実はzone
を使用すると問題があります。
ソースコードを読めば、これが理解できるはずです。下のnode.method_name
はnow
であるためです。
def operator?(node)
(node.send_type? && RESTRICT_ON_SEND.include?(node.method_name)) || node.operator_keyword?
end
Custom Cop Class
# frozen_string_literal: true
return unless defined?(RuboCop)
module CustomCops
class BanTimeZoneNow < RuboCop::Cop::Base
def_node_matcher :time_zone_now_call?, <<~PATTERN
(send (send (const nil? :Time) :zone) :now)
PATTERN
MSG = 'Do not use Time.zone.now. Use Time.current instead.'
def on_send(node)
# マッチしない場合は、そのままreturnします
return unless time_zone_now_call?(node)
add_offense(node, message: MSG)
end
end
end
Auto correctを追加
- extend
RuboCop::Cop::AutoCorrector
extend RuboCop::Cop::AutoCorrector
- replace pattern
add_offense(node, message: MSG) do |corrector|
corrector.replace(node, 'Time.current')
end
最終的なコード
# frozen_string_literal: true
return unless defined?(RuboCop)
module CustomCops
class BanTimeZoneNow < RuboCop::Cop::Base
extend RuboCop::Cop::AutoCorrector
def_node_matcher :time_zone_now_call?, <<~PATTERN
(send (send (const nil? :Time) :zone) :now)
PATTERN
MSG = 'Do not use Time.zone.now. Use Time.current instead.'
RESTRICT_ON_SEND = [:now].freeze
def on_send(node)
# マッチしない場合は、そのままreturnします
return unless time_zone_now_call?(node)
add_offense(node, message: MSG) do |corrector|
corrector.replace(node, 'Time.current')
end
end
end
end
Custom Copを適用
- .rubocop.yml
require:
- rubocop-rails
- ./lib/custom_cops/ban_time_zone_now.rb
CustomCops/BanTimeZoneNow:
Enabled: true
テスト
❯ rubocop
Inspecting 241 files
................................................................................C................................................................................................................................................................
Offenses:
app/models/program.rb:46:47: C: [Correctable] CustomCops/BanTimeZoneNow: Do not use Time.zone.now. Use Time.current instead.
scope :unexpired, -> { where('end_at >= ?', Time.zone.now) }
^^^^^^^^^^^^^
241 files inspected, 1 offense detected, 1 offense autocorrectable