Skip to content

Commit

Permalink
Support ignored_method_ids to ignore specific method_id
Browse files Browse the repository at this point in the history
  • Loading branch information
alpaca-tc committed Apr 1, 2024
1 parent 84e58f8 commit 420c62d
Show file tree
Hide file tree
Showing 8 changed files with 325 additions and 4 deletions.
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ Metrics/BlockLength:
Metrics/ModuleLength:
Enabled: false

Style/YodaCondition:
Enabled: false

Style/CaseEquality:
Enabled: false

Expand Down
1 change: 1 addition & 0 deletions lib/diver_down/trace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions lib/diver_down/trace/call_stack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down
136 changes: 136 additions & 0 deletions lib/diver_down/trace/ignored_method_ids.rb
Original file line number Diff line number Diff line change
@@ -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
31 changes: 28 additions & 3 deletions lib/diver_down/trace/tracer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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<Symbol>]
def self.trace_events
Expand All @@ -17,10 +21,11 @@ class << self

# @param module_set [DiverDown::Trace::ModuleSet, Array<Module, String>]
# @param target_files [Array<String>, nil] if nil, trace all files
# @param ignored_method_ids [Array<String>]
# @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
Expand All @@ -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
Expand All @@ -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]
Expand All @@ -88,6 +105,7 @@ def trace(title:, definition_group: nil, &)

if caller_location
pushed = true

call_stack.push(
StackContext.new(
source:,
Expand All @@ -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
Expand Down
79 changes: 79 additions & 0 deletions spec/diver_down/trace/ignored_method_ids_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 420c62d

Please sign in to comment.