Deprewriter in Ruby
RubyでDeprewriterのような動的プログラム修正を検討する
実行時コード書き換え
Deprecation in Rubyと動的コード置換を組み合わせるイメージ
静的解析による置換ならSynvertやRuboCopでいいが動的にやるアプローチを模索したい
DSLのsyntaxは参考になるかも
PoC2: 2024-12
approach
Gem::Deprecateのようなsyntaxでdeprecationを宣言する
同時にreplacementを指定できる
実行時にcallerを特定して書き換える
note
https://notebooklm.google.com/notebook/f1dd480b-4210-4390-b647-9c8a0dc7b2c0?original_referer=https:%2F%2Fwww.google.com%23&pli=1
PoC1: 2024-08
以下のようなコードを実装
code:ruby
require 'prism'
class DeprecationCorrector
class << self
def setup
return unless ENV'DEPRECATION_CORRECT'
RSpec.configure do |config|
corrector = DeprecationCorrector.new
config.before(:suite) do
ActiveSupport::Notifications.subscribe('deprecation.corrector') do |_name, _start, _finish, _id, payload|
corrector.add_deprecation(
caller_location: payload:caller_location,
method_name: payload:method_name,
replacement: payload:replacement
)
end
end
config.after(:suite) do
corrector.correct_all
end
end
end
end
class CallNodeVisitor < Prism::Visitor
attr_reader :start_column, :end_column
def initialize(method_name, line)
@method_name = method_name
@line = line
@start_column = nil
super()
end
def visit_call_node(node)
if node.name == @method_name && node.location.start_line == @line
@start_column = node.message_loc.start_column
@end_column = node.message_loc.end_column
end
super
end
end
class Deprecation
attr_reader :caller_location, :method_name, :replacement
def initialize(caller_location:, method_name:, replacement:)
@caller_location = caller_location
@method_name = method_name
@replacement = replacement
end
def correct
content = File.read(@caller_location.path).each_line.to_a
line = content@caller_location.lineno - 1
content@caller_location.lineno - 1 = line...start_column + @replacement + lineend_column..
# File.write(@caller_location.path, content.join)
puts " - Replaced #{@method_name} with #{@replacement} at #{@caller_location.path}:#{@caller_location.lineno}"
end
def eql?(other)
self.class == other.class &&
@method_name == other.method_name &&
@caller_location.to_s == other.caller_location.to_s
end
def hash
self.class, @method_name, @caller_location.to_s.hash
end
def start_column = visitor.start_column
def end_column = visitor.end_column
def visitor
@visitor ||= begin
parsed = Prism.parse_file(@caller_location.path)
visitor = CallNodeVisitor.new(@method_name, @caller_location.lineno)
parsed.value.statements.accept(visitor)
visitor
end
end
end
def initialize
@deprecations = Set.new
end
def add_deprecation(caller_location:, method_name:, replacement:)
@deprecations << Deprecation.new(caller_location:, method_name:, replacement:)
end
def correct_all
puts "\nStart correction:\n"
@deprecations.each(&:correct)
end
end
RSpec実行のセットアップでDeprecationCorrector.setupを呼ぶ
deprecateにしたいメソッドの1行目にActiveSupport::Notifications.instrument('deprecation.corrector')を仕込む
実行されるたびにDeprecationCorrectorに記録される
Thread::Backtrace::Location、deprecated method名、置換したいmethod名を与える
https://docs.ruby-lang.org/ja/latest/class/Thread=3a=3aBacktrace=3a=3aLocation.html
RSpec実行後にまとめて置換する
同じ行に同名のローカル変数が現れることもあるので、列番号も必要
Prismでparseすると取れる
アイデア
ワンショットのスクリプトなのでもっと気軽でいいのでは
gem install deprewriterしてdeprewriter bin/rspecしたら修正されている、みたいな
参考
RSpecのテストコードを実行時に書き換えて実行速度を改善した話