tp_tree 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TPTree
4
+ # MethodFilter handles filtering and excluding method calls based on various criteria
5
+ class MethodFilter
6
+ def initialize(filter: nil, exclude: nil)
7
+ @filter_matchers = build_matchers(filter) if filter
8
+ @exclude_matchers = build_matchers(exclude) if exclude
9
+ end
10
+
11
+ def should_include?(method_name, defined_class, tp)
12
+ # If we have filters, method must match at least one filter
13
+ if @filter_matchers && !@filter_matchers.empty?
14
+ return false unless matches_any?(@filter_matchers, method_name, defined_class, tp)
15
+ end
16
+
17
+ # If we have excludes, method must not match any exclude
18
+ if @exclude_matchers && !@exclude_matchers.empty?
19
+ return false if matches_any?(@exclude_matchers, method_name, defined_class, tp)
20
+ end
21
+
22
+ true
23
+ end
24
+
25
+ private
26
+
27
+ def build_matchers(criteria)
28
+ case criteria
29
+ when Array
30
+ criteria.map { |item| build_single_matcher(item) }
31
+ else
32
+ [build_single_matcher(criteria)]
33
+ end
34
+ end
35
+
36
+ def build_single_matcher(criterion)
37
+ case criterion
38
+ when String
39
+ ->(method_name, defined_class, tp) { method_name.to_s == criterion }
40
+ when Regexp
41
+ ->(method_name, defined_class, tp) { criterion.match?(method_name.to_s) }
42
+ when Proc
43
+ criterion
44
+ else
45
+ raise ArgumentError, "Filter/exclude criteria must be String, Regexp, Array, or Proc"
46
+ end
47
+ end
48
+
49
+ def matches_any?(matchers, method_name, defined_class, tp)
50
+ matchers.any? { |matcher| matcher.call(method_name, defined_class, tp) }
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../formatters/ansi_formatter'
4
+
5
+ module TPTree
6
+ module Presenters
7
+ # TreeNodePresenter handles the formatting and display logic for TreeNode objects
8
+ class TreeNodePresenter
9
+ def initialize(tree_node, formatter: nil)
10
+ @tree_node = tree_node
11
+ @formatter = formatter || Formatters::AnsiFormatter.new
12
+ end
13
+
14
+ def to_s
15
+ prefix = build_prefix
16
+ color = @formatter.color_for_depth(@tree_node.depth)
17
+ colored_method_name = @formatter.colorize(@tree_node.method_name, color)
18
+ timing_info = @formatter.format_timing(@tree_node.duration)
19
+
20
+ case @tree_node.event
21
+ when :call
22
+ "#{prefix}#{colored_method_name}(#{@formatter.format_parameters(@tree_node.parameters)})#{timing_info}"
23
+ when :return
24
+ "#{prefix}#{@formatter.format_return_value(@tree_node.return_value)}#{timing_info}"
25
+ when :call_return
26
+ "#{prefix}#{colored_method_name}(#{@formatter.format_parameters(@tree_node.parameters)}) → #{@formatter.format_return_value(@tree_node.return_value)}#{timing_info}"
27
+ end
28
+ end
29
+
30
+ def to_parts
31
+ prefix_parts = build_prefix_parts
32
+ color = @formatter.color_for_depth(@tree_node.depth)
33
+ colored_method_name = @formatter.colorize(@tree_node.method_name, color)
34
+ timing_info = @formatter.format_timing(@tree_node.duration)
35
+
36
+ content = case @tree_node.event
37
+ when :call
38
+ "#{colored_method_name}(#{@formatter.format_parameters(@tree_node.parameters)})#{timing_info}"
39
+ when :return
40
+ "#{@formatter.format_return_value(@tree_node.return_value)}#{timing_info}"
41
+ when :call_return
42
+ "#{colored_method_name}(#{@formatter.format_parameters(@tree_node.parameters)}) → #{@formatter.format_return_value(@tree_node.return_value)}#{timing_info}"
43
+ end
44
+ [prefix_parts, content]
45
+ end
46
+
47
+ private
48
+
49
+ def build_prefix_parts
50
+ parts = []
51
+ color = @formatter.color_for_depth(@tree_node.depth)
52
+
53
+ if @tree_node.event == :return
54
+ (0...@tree_node.depth).each do |level|
55
+ parts << ['│ ', @formatter.color_for_depth(level)]
56
+ end
57
+ parts << ['└→ ', color]
58
+ return parts
59
+ end
60
+
61
+ return [] if @tree_node.depth.zero?
62
+
63
+ (0...@tree_node.depth).each do |level|
64
+ parts << ['│ ', @formatter.color_for_depth(level)]
65
+ end
66
+ parts
67
+ end
68
+
69
+ def build_prefix
70
+ build_prefix_parts.map do |text, color|
71
+ @formatter.colorize(text, color)
72
+ end.join
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'tree_node'
4
+ require_relative 'call_stack'
5
+
6
+ module TPTree
7
+ # TreeBuilder uses TracePoint to build a tree of method calls.
8
+ class TreeBuilder
9
+ attr_reader :events
10
+
11
+ def initialize(method_filter: nil, &block)
12
+ @call_stack = CallStack.new
13
+ @method_filter = method_filter
14
+ @block = block
15
+ end
16
+
17
+ def events
18
+ @call_stack.events
19
+ end
20
+
21
+ def build
22
+ tp = TracePoint.trace(:call, :return) do |tp|
23
+ next if tp.callee_id == :disable && tp.defined_class == TracePoint
24
+
25
+ case tp.event
26
+ when :call
27
+ handle_call(tp)
28
+ when :return
29
+ handle_return(tp)
30
+ end
31
+ end
32
+
33
+ @block.call
34
+ tp.disable
35
+
36
+ events
37
+ end
38
+
39
+ private
40
+
41
+ def handle_call(tp)
42
+ # Apply filtering if a method filter is configured
43
+ if @method_filter && !@method_filter.should_include?(tp.callee_id, tp.defined_class, tp)
44
+ return
45
+ end
46
+
47
+ param_values = extract_parameters(tp)
48
+ call_time = Time.now
49
+
50
+ @call_stack.start_call(
51
+ tp.callee_id,
52
+ param_values,
53
+ tp.defined_class,
54
+ tp.path,
55
+ tp.lineno,
56
+ call_time
57
+ )
58
+ end
59
+
60
+ def handle_return(tp)
61
+ return_time = Time.now
62
+ call_info = @call_stack.finish_call(tp.callee_id, tp.return_value, return_time)
63
+
64
+ # If call_info is nil, the method was filtered out during call
65
+ return unless call_info
66
+
67
+ if call_info[:has_children]
68
+ # Create separate call and return events
69
+ call_node = TreeNode.new(
70
+ :call,
71
+ call_info[:method_name],
72
+ call_info[:parameters],
73
+ nil,
74
+ call_info[:depth],
75
+ call_info[:defined_class],
76
+ call_info[:path],
77
+ call_info[:lineno],
78
+ call_info[:start_time],
79
+ return_time
80
+ )
81
+
82
+ return_node = TreeNode.new(
83
+ :return,
84
+ tp.callee_id,
85
+ nil,
86
+ tp.return_value,
87
+ @call_stack.current_depth,
88
+ tp.defined_class,
89
+ tp.path,
90
+ tp.lineno,
91
+ call_info[:start_time],
92
+ return_time
93
+ )
94
+
95
+ # Replace placeholder and add return event
96
+ @call_stack.set_event_at_index(call_info[:event_index], call_node)
97
+ @call_stack.add_event(return_node)
98
+ else
99
+ # Create single call_return event
100
+ call_return_node = TreeNode.new(
101
+ :call_return,
102
+ call_info[:method_name],
103
+ call_info[:parameters],
104
+ tp.return_value,
105
+ call_info[:depth],
106
+ call_info[:defined_class],
107
+ call_info[:path],
108
+ call_info[:lineno],
109
+ call_info[:start_time],
110
+ return_time
111
+ )
112
+
113
+ # Replace placeholder
114
+ @call_stack.set_event_at_index(call_info[:event_index], call_return_node)
115
+ end
116
+ end
117
+
118
+ def extract_parameters(tp)
119
+ return tp.parameters.map { |type, name| [type, name, nil] } unless tp.binding
120
+
121
+ tp.parameters.map do |param_type, param_name|
122
+ begin
123
+ value = case param_type
124
+ when :req, :opt, :keyreq, :key
125
+ tp.binding.local_variable_get(param_name)
126
+ else
127
+ nil
128
+ end
129
+ [param_type, param_name, value]
130
+ rescue NameError
131
+ [param_type, param_name, nil]
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'formatter'
4
+ require_relative 'presenters/tree_node_presenter'
5
+
6
+ module TPTree
7
+ # TreeNode represents a single event in the method call tree
8
+ class TreeNode
9
+ include Formatter
10
+
11
+ attr_reader :event, :method_name, :parameters, :return_value, :depth, :defined_class, :path, :lineno, :start_time, :end_time
12
+
13
+ def initialize(event, method_name, parameters = nil, return_value = nil, depth = 0, defined_class = nil, path = nil, lineno = nil, start_time = nil, end_time = nil)
14
+ @event = event
15
+ @method_name = method_name
16
+ @parameters = parameters
17
+ @return_value = return_value
18
+ @depth = depth
19
+ @defined_class = defined_class
20
+ @path = path
21
+ @lineno = lineno
22
+ @start_time = start_time
23
+ @end_time = end_time
24
+ end
25
+
26
+ def duration
27
+ return nil unless @start_time && @end_time
28
+ @end_time - @start_time
29
+ end
30
+
31
+ def to_hash
32
+ {
33
+ event: @event,
34
+ method_name: @method_name,
35
+ parameters: serialize_parameters(@parameters),
36
+ return_value: serialize_value(@return_value),
37
+ depth: @depth,
38
+ defined_class: @defined_class&.to_s,
39
+ path: @path,
40
+ lineno: @lineno,
41
+ start_time: @start_time&.to_f,
42
+ end_time: @end_time&.to_f,
43
+ duration: duration
44
+ }
45
+ end
46
+
47
+ def to_s(formatter: self)
48
+ # Create appropriate formatter for presenter
49
+ presenter_formatter = if formatter.respond_to?(:formatter)
50
+ formatter.formatter
51
+ elsif formatter.respond_to?(:colorize)
52
+ formatter
53
+ else
54
+ # Fallback to default ANSI formatter
55
+ Formatters::AnsiFormatter.new
56
+ end
57
+
58
+ presenter = Presenters::TreeNodePresenter.new(self, formatter: presenter_formatter)
59
+ presenter.to_s
60
+ end
61
+
62
+ def to_parts(formatter: self)
63
+ # Create appropriate formatter for presenter
64
+ presenter_formatter = if formatter.respond_to?(:formatter)
65
+ formatter.formatter
66
+ elsif formatter.respond_to?(:colorize)
67
+ formatter
68
+ else
69
+ # Fallback to default ANSI formatter
70
+ Formatters::AnsiFormatter.new
71
+ end
72
+
73
+ presenter = Presenters::TreeNodePresenter.new(self, formatter: presenter_formatter)
74
+ presenter.to_parts
75
+ end
76
+
77
+ private
78
+
79
+ def serialize_parameters(parameters)
80
+ return nil unless parameters
81
+ parameters.map { |param_type, param_name, param_value|
82
+ {
83
+ type: param_type,
84
+ name: param_name,
85
+ value: serialize_value(param_value)
86
+ }
87
+ }
88
+ end
89
+
90
+ def serialize_value(value)
91
+ case value
92
+ when String, Symbol, NilClass, TrueClass, FalseClass, Numeric
93
+ value
94
+ when Array
95
+ value.map { |v| serialize_value(v) }
96
+ when Hash
97
+ value.transform_values { |v| serialize_value(v) }
98
+ when Proc
99
+ 'Proc'
100
+ else
101
+ value.inspect
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TPTree
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'formatters/xml_formatter'
4
+
5
+ module TPTree
6
+ # XMLFormatter provides methods for colorizing and formatting output using XML tags.
7
+ # This module acts as a compatibility layer for the old XMLFormatter module.
8
+ module XMLFormatter
9
+ def self.included(base)
10
+ base.extend(FormatterMethods)
11
+ base.include(FormatterMethods)
12
+ end
13
+
14
+ module FormatterMethods
15
+ def formatter
16
+ @xml_formatter ||= Formatters::XmlFormatter.new
17
+ end
18
+
19
+ def colorize(text, color)
20
+ formatter.colorize(text, color)
21
+ end
22
+
23
+ def format_timing(duration)
24
+ formatter.format_timing(duration)
25
+ end
26
+
27
+ def format_parameters(parameters)
28
+ formatter.format_parameters(parameters)
29
+ end
30
+
31
+ def format_value(value)
32
+ formatter.format_value(value)
33
+ end
34
+
35
+ def format_return_value(return_value)
36
+ formatter.format_return_value(return_value)
37
+ end
38
+
39
+ def color_for_depth(depth)
40
+ formatter.color_for_depth(depth)
41
+ end
42
+ end
43
+
44
+ # Expose constants for backward compatibility
45
+ DEPTH_COLORS = Formatters::BaseFormatter::DEPTH_COLORS
46
+ end
47
+ end
data/lib/tp_tree.rb ADDED
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tp_tree/version"
4
+ require_relative "tp_tree/tree_builder"
5
+ require_relative "tp_tree/method_filter"
6
+
7
+ module TPTree
8
+ class <<self
9
+ # catch sets up a TracePoint to monitor method calls and returns,
10
+ # printing them in chronological order with proper tree indentation.
11
+ #
12
+ # @param interactive [Boolean] whether to show interactive viewer
13
+ # @param write_to [String] file path to write JSON output to
14
+ # @param filter [String, Regexp, Array, Proc] only include methods matching these criteria
15
+ # @param exclude [String, Regexp, Array, Proc] exclude methods matching these criteria
16
+ def catch(interactive: false, write_to: nil, filter: nil, exclude: nil, &block)
17
+ filter_obj = MethodFilter.new(filter: filter, exclude: exclude) if filter || exclude
18
+ events = TreeBuilder.new(method_filter: filter_obj, &block).build
19
+
20
+ if interactive
21
+ require_relative "tp_tree/interactive_viewer"
22
+ InteractiveViewer.new(events).show
23
+ elsif write_to
24
+ require 'json'
25
+ require 'time'
26
+ json_data = {
27
+ version: TPTree::VERSION,
28
+ timestamp: Time.now.strftime('%Y-%m-%dT%H:%M:%S%z'),
29
+ events: events.map(&:to_hash)
30
+ }
31
+ File.write(write_to, JSON.pretty_generate(json_data))
32
+ puts "Trace data written to: #{write_to}"
33
+ else
34
+ events.each { |event| puts event }
35
+ end
36
+ end
37
+ end
38
+ end
data/sig/tp_tree.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module TpTree
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tp_tree
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dmytro Koval
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: TPTree provides a simple but powerful tracer for method calls, using
13
+ TracePoint.
14
+ email:
15
+ - contact@dkoval.pro
16
+ executables:
17
+ - tp_tree
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - ".rspec"
22
+ - ".rubocop.yml"
23
+ - CHANGELOG.md
24
+ - CODE_OF_CONDUCT.md
25
+ - LICENSE.txt
26
+ - README.md
27
+ - Rakefile
28
+ - examples/interactive_timing_demo.rb
29
+ - examples/semi_empty_nodes_demo.rb
30
+ - examples/timing_demo.rb
31
+ - exe/tp_tree
32
+ - lib/tp_tree.rb
33
+ - lib/tp_tree/call_stack.rb
34
+ - lib/tp_tree/formatter.rb
35
+ - lib/tp_tree/formatters/ansi_formatter.rb
36
+ - lib/tp_tree/formatters/base_formatter.rb
37
+ - lib/tp_tree/formatters/xml_formatter.rb
38
+ - lib/tp_tree/interactive_viewer.rb
39
+ - lib/tp_tree/method_filter.rb
40
+ - lib/tp_tree/presenters/tree_node_presenter.rb
41
+ - lib/tp_tree/tree_builder.rb
42
+ - lib/tp_tree/tree_node.rb
43
+ - lib/tp_tree/version.rb
44
+ - lib/tp_tree/xml_formatter.rb
45
+ - sig/tp_tree.rbs
46
+ homepage: https://github.com/dmk/tp_tree
47
+ licenses:
48
+ - MIT
49
+ metadata:
50
+ allowed_push_host: https://rubygems.org
51
+ homepage_uri: https://github.com/dmk/tp_tree
52
+ source_code_uri: https://github.com/dmk/tp_tree
53
+ changelog_uri: https://github.com/dmk/tp_tree/blob/main/CHANGELOG.md
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 3.1.0
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubygems_version: 3.6.9
69
+ specification_version: 4
70
+ summary: A simple tracer for method calls.
71
+ test_files: []