Skip to content

Commit

Permalink
Provide a way to analyze without passing block
Browse files Browse the repository at this point in the history
  • Loading branch information
alpaca-tc committed Apr 11, 2024
1 parent 29c249e commit cfecafc
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 96 deletions.
1 change: 1 addition & 0 deletions lib/diver_down.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module DiverDown
class Error < StandardError; end

DELIMITER = ','
LIB_DIR = __dir__

require 'diver_down/definition'
require 'diver_down/helper'
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 @@ -5,6 +5,7 @@
module DiverDown
module Trace
require 'diver_down/trace/tracer'
require 'diver_down/trace/tracer/session'
require 'diver_down/trace/call_stack'
require 'diver_down/trace/module_set'
require 'diver_down/trace/redefine_ruby_methods'
Expand Down
105 changes: 24 additions & 81 deletions lib/diver_down/trace/tracer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,91 +62,34 @@ def initialize(module_set: {}, target_files: nil, ignored_method_ids: nil, filte
# @param definition_group [String, nil]
#
# @return [DiverDown::Definition]
def trace(title:, definition_group: nil, &)
call_stack = DiverDown::Trace::CallStack.new
definition = DiverDown::Definition.new(
definition_group:,
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

if !source_name.nil? && !(already_ignored || current_ignored)
source = definition.find_or_build_source(source_name)

# 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]
called_source = called_stack_context.source
dependency = called_source.find_or_build_dependency(source_name)

# `dependency.nil?` means source_name equals to called_source.source.
# self-references are not tracked because it is not "dependency".
if dependency
context = DiverDown::Helper.module?(tp.self) ? 'class' : 'instance'
method_id = dependency.find_or_build_method_id(name: tp.method_id, context:)
method_id_path = "#{called_stack_context.caller_location.path}:#{called_stack_context.caller_location.lineno}"
method_id_path = @filter_method_id_path.call(method_id_path) if @filter_method_id_path
method_id.add_path(method_id_path)
end
end

# `caller_location` is nil if it is filtered by target_files
caller_location = find_neast_caller_location

if caller_location
pushed = true

call_stack.push(
StackContext.new(
source:,
method_id: tp.method_id,
caller_location:
)
)
end
end

call_stack.push unless pushed
def trace(title: SecureRandom.uuid, definition_group: nil, &)
session = new_session(title:, definition_group:)
session.start

# 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

tracer.enable(&)
yield

definition
session.stop
session.definition
ensure
# Ensure to stop the session
session&.stop
end

private

def find_neast_caller_location
return caller_locations(2, 2)[0] if @target_file_set.nil?

Thread.each_caller_location do
return _1 if @target_file_set.include?(_1.path)
end

nil
# @param title [String]
# @param definition_group [String, nil]
#
# @return [TracePoint]
def new_session(title: SecureRandom.uuid, definition_group: nil)
DiverDown::Trace::Tracer::Session.new(
module_set: @module_set,
ignored_method_ids: @ignored_method_ids,
target_file_set: @target_file_set,
filter_method_id_path: @filter_method_id_path,
definition: DiverDown::Definition.new(
title:,
definition_group:
)
)
end
end
end
Expand Down
121 changes: 121 additions & 0 deletions lib/diver_down/trace/tracer/session.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# frozen_string_literal: true

module DiverDown
module Trace
class Tracer
class Session
class NotStarted < StandardError; end

attr_reader :definition

# @param [DiverDown::Trace::ModuleSet, nil] module_set
# @param [DiverDown::Trace::IgnoredMethodIds, nil] ignored_method_ids
# @param [Set<String>, nil] target_file_set
# @param [#call, nil] filter_method_id_path
def initialize(module_set: DiverDown::Trace::ModuleSet.new, ignored_method_ids: nil, target_file_set: nil, filter_method_id_path: nil, definition: DiverDown::Definition.new)
@module_set = module_set
@ignored_method_ids = ignored_method_ids
@target_file_set = target_file_set
@filter_method_id_path = filter_method_id_path
@definition = definition
@trace_point = build_trace_point
end

# @return [void]
def start
@trace_point.enable
end

# @return [void]
def stop
@trace_point.disable
end

private

def build_trace_point
call_stack = DiverDown::Trace::CallStack.new
ignored_stack_size = nil

TracePoint.new(*DiverDown::Trace::Tracer.trace_events) do |tp|
# Skip the trace of the library itself
next if tp.path&.start_with?(DiverDown::LIB_DIR)
next if TracePoint == tp.defined_class

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

if !source_name.nil? && !(already_ignored || current_ignored)
source = @definition.find_or_build_source(source_name)

# 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]
called_source = called_stack_context.source
dependency = called_source.find_or_build_dependency(source_name)

# `dependency.nil?` means source_name equals to called_source.source.
# self-references are not tracked because it is not "dependency".
if dependency
context = DiverDown::Helper.module?(tp.self) ? 'class' : 'instance'
method_id = dependency.find_or_build_method_id(name: tp.method_id, context:)
method_id_path = "#{called_stack_context.caller_location.path}:#{called_stack_context.caller_location.lineno}"
method_id_path = @filter_method_id_path.call(method_id_path) if @filter_method_id_path
method_id.add_path(method_id_path)
end
end

# `caller_location` is nil if it is filtered by target_files
caller_location = find_neast_caller_location

if caller_location
pushed = true

call_stack.push(
StackContext.new(
source:,
method_id: tp.method_id,
caller_location:
)
)
end
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
rescue StandardError
tp.disable
raise
end
end

def find_neast_caller_location
return caller_locations(2, 2)[0] if @target_file_set.nil?

Thread.each_caller_location do
return _1 if @target_file_set.include?(_1.path)
end

nil
end
end
end
end
end
88 changes: 73 additions & 15 deletions spec/diver_down/trace/tracer_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
# frozen_string_literal: true

RSpec.describe DiverDown::Trace::Tracer do
# fill default values
def fill_default(hash)
hash[:title] ||= ''
hash[:definition_group] ||= nil
hash[:sources] ||= []
hash[:sources].each do |source|
source[:dependencies] ||= []

source[:dependencies].each do |dependency|
dependency[:method_ids] ||= []
end
end
hash
end

describe '#initialize' do
describe 'with relative path target_files' do
it 'raises ArgumentError' do
Expand All @@ -23,6 +38,64 @@
end
end

describe '#new_session' do
it 'provides session for tracing ruby code without block' do
klass_a = Class.new do
def self.call_b
B.call
end
end

klass_b = Class.new do
def self.call
nil
end
end

stub_const('A', klass_a)
stub_const('B', klass_b)

tracer = DiverDown::Trace::Tracer.new

definition_1 = tracer.trace(title: 'title') do
A.call_b
end

session = tracer.new_session(title: 'title')
session.start
A.call_b
session.stop
definition_2 = session.definition

expect(definition_1.to_h).to match(fill_default(
title: 'title',
sources: [
{
source_name: 'A',
dependencies: [
{
source_name: 'B',
method_ids: [
{
context: 'class',
name: 'call',
paths: [
include(__FILE__),
],
},
],
},
],
}, {
source_name: 'B',
},
]
))

expect(definition_1).to eq(definition_2)
end
end

describe '#trace' do
describe 'when tracing script' do
# @param path [String]
Expand All @@ -48,21 +121,6 @@ def trace_fixture(path, module_set: {}, target_files: nil, ignored_method_ids: [
end
end

# fill default values
def fill_default(hash)
hash[:title] ||= ''
hash[:definition_group] ||= nil
hash[:sources] ||= []
hash[:sources].each do |source|
source[:dependencies] ||= []

source[:dependencies].each do |dependency|
dependency[:method_ids] ||= []
end
end
hash
end

before do
stub_const('AntipollutionModule', Module.new)
stub_const('AntipollutionKlass', Class.new)
Expand Down

0 comments on commit cfecafc

Please sign in to comment.