Created
July 10, 2023 10:03
-
-
Save sinsoku/a0fc686ab477e45969c9d145d3ba0917 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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