diff --git a/.rubocop.yml b/.rubocop.yml index 40187f2..324a839 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -50,6 +50,9 @@ Metrics/BlockLength: Metrics/ModuleLength: Enabled: false +Style/YodaCondition: + Enabled: false + Style/CaseEquality: Enabled: false diff --git a/lib/diver_down/trace.rb b/lib/diver_down/trace.rb index 2b081b4..af79989 100644 --- a/lib/diver_down/trace.rb +++ b/lib/diver_down/trace.rb @@ -8,6 +8,7 @@ module Trace require 'diver_down/trace/call_stack' require 'diver_down/trace/module_set' require 'diver_down/trace/redefine_ruby_methods' + require 'diver_down/trace/ignored_method_ids' @trace_events = %i[ call c_call return c_return diff --git a/lib/diver_down/trace/call_stack.rb b/lib/diver_down/trace/call_stack.rb index cc2b0ae..141050f 100644 --- a/lib/diver_down/trace/call_stack.rb +++ b/lib/diver_down/trace/call_stack.rb @@ -8,6 +8,9 @@ module Trace class CallStack class StackEmptyError < RuntimeError; end + # @attr_reader stack [Integer] stack size + attr_reader :stack_size + def initialize @stack_size = 0 @stack = {} diff --git a/lib/diver_down/trace/ignored_method_ids.rb b/lib/diver_down/trace/ignored_method_ids.rb new file mode 100644 index 0000000..499a1ec --- /dev/null +++ b/lib/diver_down/trace/ignored_method_ids.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +module DiverDown + module Trace + class IgnoredMethodIds + def initialize(ignored_methods) + # Ignore all methods in the module + # Hash{ Module => Boolean } + @ignored_modules = {} + + # Ignore all methods in the class + # Hash{ Module => Hash{ Symbol => Boolean } } + @ignored_class_method_id = Hash.new { |h, k| h[k] = {} } + + # Ignore all methods in the instance + # Hash{ Module => Hash{ Symbol => Boolean } } + @ignored_instance_method_id = Hash.new { |h, k| h[k] = {} } + + ignored_methods.each do |ignored_method| + if ignored_method.include?('.') + # instance method + class_name, method_id = ignored_method.split('.') + mod = DiverDown::Helper.constantize(class_name) + @ignored_class_method_id[mod][method_id.to_sym] = true + elsif ignored_method.include?('#') + # class method + class_name, method_id = ignored_method.split('#') + mod = DiverDown::Helper.constantize(class_name) + @ignored_instance_method_id[mod][method_id.to_sym] = true + else + # module + mod = DiverDown::Helper.constantize(ignored_method) + @ignored_modules[mod] = true + end + end + end + + # @param mod [Module] + # @param is_class [Boolean] class is true, instance is false + # @param method_id [Symbol] + # @return [Boolean] + def ignored?(mod, is_class, method_id) + ignored_module?(mod) || ignored_method?(mod, is_class, method_id) + end + + private + + def ignored_module?(mod) + unless @ignored_modules.key?(mod) + dig_superclass(mod) + end + + @ignored_modules.fetch(mod) + end + + def ignored_method?(mod, is_class, method_id) + store = if is_class + # class methods + @ignored_class_method_id + else + # instance methods + @ignored_instance_method_id + end + + begin + dig_superclass_method_id(store, mod, method_id) unless store[mod].key?(method_id) + rescue TypeError => e + # https://github.com/ruby/ruby/blob/f42164e03700469a7000b4f00148a8ca01d75044/object.c#L2232 + return false if e.message == 'uninitialized class' + + raise + end + + store.fetch(mod).fetch(method_id) + end + + def dig_superclass(mod) + unless DiverDown::Helper.class?(mod) + # NOTE: Do not lookup the ancestors if module given because of the complexity of implementation + @ignored_modules[mod] = false + return + end + + stack = [] + current = mod + ignored = nil + + until current.nil? + if @ignored_modules.key?(current) + ignored = @ignored_modules.fetch(current) + break + else + stack.push(current) + current = current.superclass + end + end + + # Convert nil to boolean + ignored = !!ignored + + stack.each do + @ignored_modules[_1] = ignored + end + end + + def dig_superclass_method_id(store, mod, method_id) + unless DiverDown::Helper.class?(mod) + # NOTE: Do not lookup the ancestors if module given because of the complexity of implementation + store[mod][method_id] = false + return + end + + stack = [] + current = mod + ignored = nil + + until current.nil? + if store[current].key?(method_id) + ignored = store[current].fetch(method_id) + break + else + stack.push(current) + current = current.superclass + end + end + + # Convert nil to boolean + ignored = !!ignored + + stack.each do + store[_1][method_id] = ignored + end + end + end + end +end diff --git a/lib/diver_down/trace/tracer.rb b/lib/diver_down/trace/tracer.rb index cb01428..10769e6 100644 --- a/lib/diver_down/trace/tracer.rb +++ b/lib/diver_down/trace/tracer.rb @@ -3,7 +3,11 @@ module DiverDown module Trace class Tracer - StackContext = Data.define(:source, :method_id, :caller_location) + StackContext = Data.define( + :source, + :method_id, + :caller_location + ) # @return [Array] def self.trace_events @@ -17,10 +21,11 @@ class << self # @param module_set [DiverDown::Trace::ModuleSet, Array] # @param target_files [Array, nil] if nil, trace all files + # @param ignored_method_ids [Array] # @param filter_method_id_path [#call, nil] filter method_id.path # @param module_set [DiverDown::Trace::ModuleSet, nil] for optimization # @param module_finder [#call] find module from source - def initialize(module_set: [], target_files: nil, filter_method_id_path: nil, module_finder: nil) + def initialize(module_set: [], target_files: nil, ignored_method_ids: nil, filter_method_id_path: nil, module_finder: nil) if target_files && !target_files.all? { Pathname.new(_1).absolute? } raise ArgumentError, "target_files must be absolute path(#{target_files})" end @@ -33,6 +38,12 @@ def initialize(module_set: [], target_files: nil, filter_method_id_path: nil, mo DiverDown::Trace::ModuleSet.new(modules: module_set) end + @ignored_method_ids = if ignored_method_ids.is_a?(DiverDown::Trace::IgnoredMethodIds) + ignored_method_ids + elsif !ignored_method_ids.nil? + DiverDown::Trace::IgnoredMethodIds.new(ignored_method_ids) + end + @target_file_set = target_files&.to_set @filter_method_id_path = filter_method_id_path @module_finder = module_finder @@ -51,21 +62,27 @@ def trace(title:, definition_group: nil, &) title: ) + @ignored_stack_size = nil + tracer = TracePoint.new(*self.class.trace_events) do |tp| case tp.event when :call, :c_call # puts "#{tp.method_id} #{tp.path}:#{tp.lineno}" mod = DiverDown::Helper.resolve_module(tp.self) source_name = DiverDown::Helper.normalize_module_name(mod) if !mod.nil? && @module_set.include?(mod) + already_ignored = !@ignored_stack_size.nil? # If the current method_id is ignored + current_ignored = !@ignored_method_ids.nil? && @ignored_method_ids.ignored?(mod, DiverDown::Helper.module?(tp.self), tp.method_id) pushed = false - unless source_name.nil? + if !source_name.nil? && !(already_ignored || current_ignored) source = definition.find_or_build_source(source_name) # Determine module name from source module_names = @module_finder&.call(source) source.set_modules(module_names) if module_names + # If the call stack contains a call to a module to be traced + # `@ignored_call_stack` is not nil means the call stack contains a call to a module to be ignored unless call_stack.empty? # Add dependency to called source called_stack_context = call_stack.stack[-1] @@ -88,6 +105,7 @@ def trace(title:, definition_group: nil, &) if caller_location pushed = true + call_stack.push( StackContext.new( source:, @@ -99,7 +117,14 @@ def trace(title:, definition_group: nil, &) end call_stack.push unless pushed + + # If a value is already stored, it means that call stack already determined to be ignored at the shallower call stack size. + # Since stacks deeper than the shallowest stack size are ignored, priority is given to already stored values. + if !already_ignored && current_ignored + @ignored_stack_size = call_stack.stack_size + end when :return, :c_return + @ignored_stack_size = nil if @ignored_stack_size == call_stack.stack_size call_stack.pop end end diff --git a/spec/diver_down/trace/ignored_method_ids_spec.rb b/spec/diver_down/trace/ignored_method_ids_spec.rb new file mode 100644 index 0000000..a057bc6 --- /dev/null +++ b/spec/diver_down/trace/ignored_method_ids_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +RSpec.describe DiverDown::Trace::IgnoredMethodIds do + describe 'InstanceMethods' do + describe '#ignored?' do + it 'returns false if ignored_methods are blank' do + stub_const('A', Class.new) + + ignored_method = described_class.new([]) + + expect(ignored_method.ignored?(A, true, 'new')).to be(false) + end + + it 'returns true if class is matched' do + stub_const('A', Class.new) + stub_const('B', Class.new) + stub_const('C', Class.new(A)) + + ignored_method = described_class.new( + [ + 'A', + ] + ) + + expect(ignored_method.ignored?(A, true, :new)).to be(true) + expect(ignored_method.ignored?(B, true, :new)).to be(false) + expect(ignored_method.ignored?(C, true, :new)).to be(true) + end + + it 'does not lookup the ancestors if module given because of the complexity of implementation' do + stub_const('A', Module.new) + stub_const('B', Module.new) + stub_const('C', Module.new.tap { _1.extend(A) }) + + ignored_method = described_class.new( + [ + 'A.name', + ] + ) + + expect(ignored_method.ignored?(A, true, :name)).to be(true) + expect(ignored_method.ignored?(B, true, :name)).to be(false) + expect(ignored_method.ignored?(C, true, :name)).to be(false) + end + + it 'returns true if class method is matched' do + stub_const('A', Class.new) + stub_const('B', Class.new) + stub_const('C', Class.new(A)) + + ignored_method = described_class.new( + [ + 'A.new', + ] + ) + + expect(ignored_method.ignored?(A, true, :new)).to be(true) + expect(ignored_method.ignored?(B, true, :new)).to be(false) + expect(ignored_method.ignored?(C, true, :new)).to be(true) + end + + it 'returns true if instance method is matched' do + stub_const('A', Class.new) + stub_const('B', Class.new) + stub_const('C', Class.new(A)) + + ignored_method = described_class.new( + [ + 'A#initialize', + ] + ) + + expect(ignored_method.ignored?(A, false, :initialize)).to be(true) + expect(ignored_method.ignored?(B, false, :initialize)).to be(false) + expect(ignored_method.ignored?(C, false, :initialize)).to be(true) + end + end + end +end diff --git a/spec/diver_down/trace/tracer_spec.rb b/spec/diver_down/trace/tracer_spec.rb index f4aa878..51a6ac8 100644 --- a/spec/diver_down/trace/tracer_spec.rb +++ b/spec/diver_down/trace/tracer_spec.rb @@ -17,7 +17,7 @@ describe 'when tracing script' do # @param path [String] # @return [DiverDown::Definition] - def trace_fixture(path, module_set: [], target_files: nil, filter_method_id_path: nil, module_finder: nil, definition_group: nil) + def trace_fixture(path, module_set: [], target_files: nil, ignored_method_ids: [], filter_method_id_path: nil, module_finder: nil, definition_group: nil) # NOTE: Script need to define .run method script = fixture_path(path) load script, AntipollutionModule @@ -26,6 +26,7 @@ def trace_fixture(path, module_set: [], target_files: nil, filter_method_id_path tracer = described_class.new( module_set:, target_files:, + ignored_method_ids:, filter_method_id_path:, module_finder: ) @@ -569,6 +570,49 @@ def fill_default(hash) )) end + it 'traces tracer_ignored_call_stack.rb' do + definition = trace_fixture( + 'tracer_ignored_call_stack.rb', + ignored_method_ids: [ + 'AntipollutionModule::B.class_call', + ], + module_set: [ + 'AntipollutionModule::A', + 'AntipollutionModule::B', + 'AntipollutionModule::C', + 'AntipollutionModule::D', + ], + target_files: [ + fixture_path('tracer_ignored_call_stack.rb'), + ] + ) + + expect(definition.to_h).to match(fill_default( + title: 'title', + sources: [ + { + source_name: 'AntipollutionModule::A', + dependencies: [ + { + source_name: 'AntipollutionModule::D', + method_ids: [ + { + name: 'class_call', + context: 'class', + paths: [ + match(/tracer_ignored_call_stack\.rb:\d+/), + ] + }, + ], + }, + ], + }, { + source_name: 'AntipollutionModule::D', dependencies: [], modules: [], + }, + ] + )) + end + it 'traces tracer_deep_stack.rb' do definition = trace_fixture( 'tracer_deep_stack.rb', diff --git a/spec/fixtures/tracer_ignored_call_stack.rb b/spec/fixtures/tracer_ignored_call_stack.rb new file mode 100644 index 0000000..5d45f80 --- /dev/null +++ b/spec/fixtures/tracer_ignored_call_stack.rb @@ -0,0 +1,30 @@ +def run + A.class_call + # Isolated.class_call + + # A.new.instance_call + # Isolated.new.instance_call +end + +class A + def self.class_call + B.class_call + C.class_call + D.class_call + end +end + +class B + def self.class_call + D.class_call + end +end + +class C < B +end + +class D + def self.class_call + nil + end +end