-
-
Save alpaca-tc/cb1976d433066ca069b6193b7e2fa2f5 to your computer and use it in GitHub Desktop.
実際のスクリプトから少し削ってる。そのままだと動かないかも :gomenne:
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
# ./bin/rails r script/query_methods_dumper.rb | |
dumper = MethodDumper.new do | |
run_rspec | |
end | |
%i(where joins includes preload eager_load references left_joins left_outer_joins find_by find_or_create_by find_or_initialize_by create_or_find_by).each do |method_name| | |
dumper.add_method_trace("ActiveRecord::QueryMethods##{method_name}") | |
dumper.add_method_trace("ActiveRecord::QueryMethods##{method_name}!") | |
end | |
%i(exists? find_by).each do |method_name| | |
dumper.add_method_trace("ActiveRecord::FinderMethods##{method_name}") | |
end | |
%i(new create_or_find_by find_or_create_by find_or_initialize_by).each do |method_name| | |
dumper.add_method_trace("ActiveRecord::Relation##{method_name}") | |
end | |
%i(build create).each do |method_name| | |
dumper.add_method_trace("ActiveRecord::Associations::CollectionProxy##{method_name}") | |
dumper.add_method_trace("ActiveRecord::Associations::CollectionProxy##{method_name}!") | |
end | |
%i(build create attributes_for build_stubbed null).each do |method_name| | |
dumper.add_method_trace("FactoryBot::Syntax::Methods##{method_name}") | |
dumper.add_method_trace("FactoryBot::Syntax::Methods##{method_name}_list") | |
dumper.add_method_trace("FactoryBot::Syntax::Methods##{method_name}_pair") | |
end | |
dumper.add_method_trace("ActiveRecord::QueryMethods::WhereChain#not") | |
dumper.add_method_trace("ActiveRecord::QueryMethods::WhereChain#associated") | |
dumper.add_method_trace("ActiveRecord::QueryMethods::WhereChain#missing") | |
dumper.add_method_trace("ActiveRecord::Inheritance::ClassMethods.new") | |
dumper.add_method_trace("ActiveRecord::Persistence::ClassMethods.create") | |
ActiveRecord::TableMetadata.class_attribute(:enabled_reflections_collector, default: false) | |
ActiveRecord::TableMetadata.class_attribute(:reflections, default: []) | |
ActiveRecord::TableMetadata.singleton_class.prepend(Module.new do | |
def collect_reflections | |
self.enabled_reflections_collector = true | |
self.reflections = [] | |
yield | |
reflections | |
ensure | |
self.enabled_reflections_collector = false | |
end | |
end) | |
ActiveRecord::TableMetadata.prepend(Module.new do | |
def associated_table(table_name) | |
if self.class.enabled_reflections_collector | |
reflection = klass._reflect_on_association(table_name) || klass._reflect_on_association(table_name.singularize) | |
self.class.reflections << [klass.name, table_name.to_sym] if reflection | |
end | |
super | |
end | |
end) | |
class QueryMethodAnalyzer | |
BACKTRACE_CLEANER = ActiveSupport::BacktraceCleaner.new.tap do |cleaner| | |
cleaner.add_silencer { !(_1.start_with?(Rails.root.join("app").to_s) || _1.start_with?(Rails.root.join("spec").to_s)) } | |
cleaner.add_silencer { _1.include?("spec/rails_helper.rb") } | |
end | |
# User.where.not のwhere句や、User.none を無視する | |
IGNORED_METHODS = %i(where none).freeze | |
ALIAS_METHODS = { | |
"ActiveRecord::QueryMethods#left_joins" => "ActiveRecord::QueryMethods#left_outer_joins", | |
"ActiveRecord::Relation#new" => "ActiveRecord::Relation#build", | |
}.tap { _1.merge!(_1.invert) }.freeze | |
attr_reader :trace_point | |
attr_reader :klass | |
attr_reader :receiver_and_method_name | |
def initialize(trace_point, klass, receiver_and_method_name) | |
@trace_point = trace_point | |
@klass = klass | |
@receiver_and_method_name = receiver_and_method_name | |
end | |
def parameters_code_location | |
case node.type | |
when :CALL | |
_nd_recv, _mid, nd_args = node.children | |
{ | |
path: caller_location.path, | |
first_lineno: nd_args.first_lineno, | |
first_column: nd_args.first_column - 1, | |
last_lineno: node.last_lineno, | |
last_column: node.last_column, | |
} | |
when :FCALL | |
_method_name, nd_args = node.children | |
{ | |
path: caller_location.path, | |
first_lineno: nd_args.first_lineno, | |
first_column: nd_args.first_column - 1, | |
last_lineno: node.last_lineno, | |
last_column: node.last_column, | |
} | |
else | |
raise NotImplementedError, "not implemented yet #{node.type}" | |
end | |
end | |
def model | |
if @klass < ActiveRecord::Associations::CollectionProxy | |
@klass.to_s.remove("::ActiveRecord_Associations_CollectionProxy").safe_constantize | |
elsif @klass < ActiveRecord::AssociationRelation | |
@klass.to_s.remove("::ActiveRecord_AssociationRelation").safe_constantize | |
elsif @klass < ActiveRecord::Relation | |
@klass.to_s.remove("::ActiveRecord_Relation").safe_constantize | |
elsif @klass < ActiveRecord::Base | |
@klass | |
elsif @trace_point.self.kind_of?(ActiveRecord::QueryMethods::WhereChain) | |
@trace_point.self.instance_variable_get(:@scope).klass | |
elsif @receiver_and_method_name.start_with?("FactoryBot::Syntax::Methods#") | |
name, * = extract_attributes_for_factory_bot | |
klass = FactoryBot.factories[name].build_class | |
klass if klass < ActiveRecord::Base | |
else | |
raise "not supported #{@klass}" | |
end | |
end | |
def extract_reflections | |
case @receiver_and_method_name.remove(/!$/) | |
when "ActiveRecord::FinderMethods#exists?" | |
conditions = extract_variables_from_trace_point(:conditions)[0] | |
if conditions == :none | |
[] | |
else | |
collect_reflections_from_where_clause(trace_point.self.unscoped, conditions) | |
end | |
when "ActiveRecord::Persistence::ClassMethods.create", | |
"ActiveRecord::Relation#new", | |
"ActiveRecord::Inheritance::ClassMethods.new", | |
"ActiveRecord::Associations::CollectionProxy#build", | |
"ActiveRecord::Associations::CollectionProxy#create" | |
attributes = extract_variables_from_trace_point(:attributes)[0] | |
collect_reflections_from_where_clause(trace_point.self.unscoped, attributes) | |
when "ActiveRecord::QueryMethods#find_or_create_by", | |
"ActiveRecord::QueryMethods#find_or_initialize_by", | |
"ActiveRecord::QueryMethods#create_or_find_by", | |
"ActiveRecord::Relation#find_or_initialize_by", | |
"ActiveRecord::Relation#find_or_create_by", | |
"ActiveRecord::Relation#create_or_find_by" | |
attributes = extract_variables_from_trace_point(:attributes)[0] | |
collect_reflections_from_where_clause(trace_point.self, attributes) | |
when "ActiveRecord::QueryMethods#find_by", | |
"ActiveRecord::FinderMethods#find_by" | |
arg, args = extract_variables_from_trace_point(:arg, :args) | |
collect_reflections_from_where_clause(trace_point.self, arg, *args) | |
when "ActiveRecord::QueryMethods#where" | |
args = extract_variables_from_trace_point(:args)[0] | |
collect_reflections_from_where_clause(trace_point.self, *args) | |
when "ActiveRecord::QueryMethods::WhereChain#not" | |
opts, rest, receiver = extract_variables_from_trace_point(:opts, :rest, :@scope) | |
collect_reflections_from_where_clause(receiver, opts, *rest) | |
when "ActiveRecord::QueryMethods#references" | |
table_names = extract_variables_from_trace_point(:table_names)[0] | |
collect_reflections_from_joins_clause(trace_point.self, *table_names) | |
when "ActiveRecord::QueryMethods#joins", | |
"ActiveRecord::QueryMethods#includes", | |
"ActiveRecord::QueryMethods#preload", | |
"ActiveRecord::QueryMethods#eager_load", | |
"ActiveRecord::QueryMethods#left_outer_joins" | |
args = extract_variables_from_trace_point(:args)[0] | |
collect_reflections_from_joins_clause(trace_point.self, *args) | |
when "ActiveRecord::QueryMethods::WhereChain#associated", | |
"ActiveRecord::QueryMethods::WhereChain#missing" | |
args, receiver = extract_variables_from_trace_point(:associations, :@scope) | |
collect_reflections_from_joins_clause(receiver, *args) | |
when "FactoryBot::Syntax::Methods#build", | |
"FactoryBot::Syntax::Methods#create", | |
"FactoryBot::Syntax::Methods#build_list", | |
"FactoryBot::Syntax::Methods#create_list", | |
"FactoryBot::Syntax::Methods#attributes_for" | |
name, *traits = extract_attributes_for_factory_bot | |
attributes = traits.extract_options! | |
klass = FactoryBot.factories[name].build_class | |
collect_reflections_from_where_clause(klass.unscoped, attributes) | |
else | |
raise NotImplementedError, "not implemented yet #{@receiver_and_method_name}" | |
end | |
end | |
private | |
def node | |
@node ||= RubyVM::AbstractSyntaxTree.of(caller_location, keep_script_lines: true) | |
end | |
def extract_attributes_for_factory_bot | |
case @receiver_and_method_name | |
when "FactoryBot::Syntax::Methods#build", | |
"FactoryBot::Syntax::Methods#create", | |
"FactoryBot::Syntax::Methods#build_list", | |
"FactoryBot::Syntax::Methods#create_list", | |
"FactoryBot::Syntax::Methods#attributes_for" | |
name, traits_and_overrides = extract_variables_from_trace_point(:name, :traits_and_overrides) | |
[name, *traits_and_overrides] | |
else | |
raise NotImplementedError, "not implemented yet factory attributes #{@receiver_and_method_name}" | |
end | |
end | |
def extract_variables_from_trace_point(*names) | |
names.map do |name| | |
if name.to_s.start_with?("@") | |
trace_point.self.instance_variable_get(name) | |
else | |
trace_point.binding.local_variable_get(name) | |
end | |
end | |
end | |
def flat_arguments(args, collector = []) | |
case args | |
when Array | |
args.each { flat_arguments(_1, collector) } | |
when Hash | |
args.each do |key, value| | |
collector << key | |
flat_arguments(value, collector) | |
end | |
when Symbol, String | |
collector << args | |
else | |
raise NotImplementedError, "not implemented yet #{args.class}" | |
end | |
collector | |
end | |
def collect_reflections_from_joins_clause(receiver, *args) | |
associations = args | |
candidates = flat_arguments(args) | |
join_dependencies = [] | |
join_dependencies.unshift( | |
receiver.construct_join_dependency( | |
receiver.send(:select_association_list, associations, join_dependencies), nil | |
), | |
) | |
join_dependencies.flat_map(&:reflections).uniq.filter_map do |reflection| | |
[reflection.active_record.name, reflection.name] if candidates.include?(reflection.name) | |
end | |
end | |
def collect_reflections_from_where_clause(receiver, *args) | |
if args.empty? || (args.length == 1 && args.first.blank?) | |
[] | |
else | |
opts, *rest = *args | |
ActiveRecord::TableMetadata.collect_reflections do | |
receiver.send(:build_where_clause, opts, rest) | |
end | |
end | |
end | |
def caller_location | |
@caller_location ||= begin | |
backtrace = BACKTRACE_CLEANER.clean(@trace_point.binding.send(:caller)) | |
called_from = backtrace[0].to_s | |
@trace_point.send(:caller_locations).find { _1.to_s == called_from } | |
end | |
end | |
end | |
CSV.open("tmp/dumps/query_methods.csv", "w") do |csv| | |
headers = %i(model klass method_name path first_lineno first_column last_lineno last_column reflections) | |
csv << headers | |
dumper.run do |row| | |
trace_point = row[:trace_point] | |
analyzer = QueryMethodAnalyzer.new(trace_point, row[:klass], row[:method_name]) | |
next if analyzer.model.nil? | |
reflections = analyzer.extract_reflections | |
next if reflections.empty? | |
code_location = analyzer.parameters_code_location | |
row = { | |
model: analyzer.model, | |
klass: analyzer.klass, | |
method_name: row[:method_name], | |
reflections: reflections.to_json, | |
**code_location.to_h, | |
} | |
pp(row) | |
csv << headers.map { row.fetch(_1) } | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment