Skip to content

Instantly share code, notes, and snippets.

@sinsoku
Created July 10, 2023 10:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sinsoku/a0fc686ab477e45969c9d145d3ba0917 to your computer and use it in GitHub Desktop.
Save sinsoku/a0fc686ab477e45969c9d145d3ba0917 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
# frozen_string_literal: true
# Rails v7.0.5以降におけるcreate_associationメソッドの影響範囲を調べるスクリプト
#
# ## 参考ページ
# * Rails 7.0.5以降におけるcreate_associationメソッドの挙動変更についてまとめ
# https://blog.willnet.in/entry/2023/07/04/113321
# * parser gemでRubyプログラムのバグを探す
# https://nacl-ltd.github.io/2021/07/02/ruby-ast.html
require 'parser/current'
# has_one に dependent オプションを使用したときに生成される create_association
# のメソッド名を収集するクラス
class HasOneProcessor
include AST::Processor::Mixin
DEPENDENT_VALUE = %i[destroy destroy_async delete nullify].freeze
def initialize
@has_one_methods = []
end
attr_reader :has_one_methods
def on_send(node)
children = node.children
method_name = children[1]
return if method_name != :has_one
# has_one の引数を取得
args = children[2..]
name = args[0].children[0]
options = args.size == 2 ? args[1] : args[2]
return unless dependent?(options)
@has_one_methods += [:"create_#{name}", :"create_#{name}!"]
end
def handler_missing(node)
node.children.each do |child|
process(child) if child.is_a?(AST::Node)
end
end
private
def dependent?(node)
return false if node.nil?
node.children.any? do |pair|
key, value = pair.children
key.children[0] == :dependent && DEPENDENT_VALUE.include?(value.children[0])
end
end
end
# 指定のメソッドを呼び出しているかを調査するためのクラス
class CallMethodNamesProcessor
include AST::Processor::Mixin
def initialize(method_names)
@method_names = method_names
@called = []
end
attr_reader :called
def on_send(node)
children = node.children
method_name = children[1]
return unless @method_names.include?(method_name)
@called << method_name
end
def handler_missing(node)
node.children.each do |child|
process(child) if child.is_a?(AST::Node)
end
end
end
files = Dir['app/**/*.rb'] + Dir['packs/**/*.rb']
# has_one で生成されるメソッド一覧を作成
method_names = []
files.each do |f|
expr = Parser::CurrentRuby.parse(File.read(f))
processor = HasOneProcessor.new
processor.process(expr)
next if processor.has_one_methods.empty?
method_names += processor.has_one_methods
end
method_names.uniq!
# has_oneで生成されたメソッドの呼び出し箇所を調査
files.each do |f|
content = File.read(f)
expr = Parser::CurrentRuby.parse(content)
processor = CallMethodNamesProcessor.new(method_names)
processor.process(expr)
# has_oneのメソッドの呼び出しがなければ次ファイルに進む
next if processor.called.empty?
# 呼び出しがあった場合、ファイル名・行番号・呼び出し箇所を出力
content.each_line.with_index(1) do |line, no|
processor.called.each do |called_method|
next if line.match?(/^\s+?#/)
next if line.match?(/^\s+def/)
next unless line.include?(called_method.to_s)
colored = line.strip.gsub(called_method.to_s, "\e[31m#{called_method}\e[m")
puts "#{f}:#{no}\t#{colored}"
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment