why_chain 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc9a06ec23c45c166f5f1ee5ef100e2a611b4a221800bf6f5b947a026c357e8a
4
- data.tar.gz: 542eca20fb80f277e8bdfac100af52f01e781ebe7f4c3a1621f2bf3cadb4e370
3
+ metadata.gz: a6b36f3e181fd925c701bfc22b05348a39044ca3158e42133091c123aca5dcb4
4
+ data.tar.gz: c0e15d0ac8df970c25f4ea071a1965e13c3d57a77a0152b46440fa52585f3338
5
5
  SHA512:
6
- metadata.gz: ea34de73851a224c13f468019c23cc9379edc1ac85b75e0975dc3419a67dc3a3a47bca4e19d12591f3e1335a54ad788e0bb0feafcd1838bc4522c965010efbae
7
- data.tar.gz: 0cd9ad56a88c26c350c9f9d35918c2eef28223d0a751b31a584292c9051e44cdde069f12eb1307850f064fee0f39b7a0c34b82c37c1d3cc2ab226adf9890c019
6
+ metadata.gz: 1c14db65f40546e42e636ca4a4863007b837683144a1f5c087090d30db29967cce9f208368d77eb25962626f3780e240f06889a8ddd3c3d5fac5ec2aaeadf26f
7
+ data.tar.gz: 0675ed498b8c3407b40aebc7a809eda293ff9bcd5233b4ac2968569c610e914106c15a798f5bb7d8689c30d724e37d48345cb7c2ccdf9d6c2897a7d1cde0f2c2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2026-05-11
4
+
5
+ - Add `WhyChain.explain(object, method_name)` for human-readable dispatch output.
6
+ - Extend `DispatchTrace` with `source_location` and ordered `steps` data.
7
+ - Introduce `DispatchStep` value object for typed dispatch step representation.
8
+ - Add focused specs for explain output and super-path step tracing.
9
+
3
10
  ## [0.1.0] - 2026-05-10
4
11
 
5
12
  - Initial release
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- why_chain (0.1.0)
4
+ why_chain (0.2.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -30,12 +30,16 @@ bundle install
30
30
  ```ruby
31
31
  trace = WhyChain.trace(object, :method_name)
32
32
  pp trace.to_h
33
+
34
+ puts WhyChain.explain(object, :method_name)
33
35
  ```
34
36
 
35
37
  `trace` is a `WhyChain::DispatchTrace` object with readers:
36
38
  - `lookup_chain`
37
39
  - `owner`
38
40
  - `next_super_owner`
41
+ - `source_location`
42
+ - `steps` (`WhyChain::DispatchStep` objects)
39
43
 
40
44
  As hash:
41
45
 
@@ -44,10 +48,31 @@ trace.to_h
44
48
  # {
45
49
  lookup_chain: [...],
46
50
  owner: SomeClassOrModule,
47
- next_super_owner: AnotherClassOrModule
51
+ next_super_owner: AnotherClassOrModule,
52
+ source_location: ["file.rb", 12],
53
+ steps: [
54
+ { owner: SomeClassOrModule, source_location: ["file.rb", 12] }
55
+ ]
48
56
  # }
49
57
  ```
50
58
 
59
+ `WhyChain.explain` returns a human-readable dispatch explanation:
60
+
61
+ ```ruby
62
+ puts WhyChain.explain(B.new, :foo)
63
+ # Ruby dispatch explanation for :foo
64
+ #
65
+ # 1. P#foo
66
+ # defined at:
67
+ # /path/to/file.rb:12
68
+ #
69
+ # calls super ->
70
+ #
71
+ # 2. A#foo
72
+ # defined at:
73
+ # /path/to/file.rb:7
74
+ ```
75
+
51
76
  ## Usage examples
52
77
 
53
78
  ### 1) `prepend` vs `include` in runtime lookup
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WhyChain
4
+ # Immutable value object for a single dispatch step.
5
+ class DispatchStep
6
+ attr_reader :owner, :source_location
7
+
8
+ def initialize(owner:, source_location:)
9
+ @owner = owner
10
+ @source_location = source_location
11
+ end
12
+
13
+ def to_h
14
+ {
15
+ owner: owner,
16
+ source_location: source_location
17
+ }
18
+ end
19
+ end
20
+ end
@@ -3,24 +3,38 @@
3
3
  module WhyChain
4
4
  # Immutable value object for traced dispatch data.
5
5
  class DispatchTrace
6
- attr_reader :lookup_chain, :owner, :next_super_owner
6
+ attr_reader :lookup_chain, :owner, :next_super_owner, :source_location, :steps
7
7
 
8
8
  def initialize(
9
9
  lookup_chain:,
10
10
  owner:,
11
- next_super_owner:
11
+ next_super_owner:,
12
+ source_location: nil,
13
+ steps: []
12
14
  )
13
15
  @lookup_chain = lookup_chain
14
16
  @owner = owner
15
17
  @next_super_owner = next_super_owner
18
+ @source_location = source_location
19
+ @steps = steps.map { |step| coerce_step(step) }
16
20
  end
17
21
 
18
22
  def to_h
19
23
  {
20
24
  lookup_chain: lookup_chain,
21
25
  owner: owner,
22
- next_super_owner: next_super_owner
26
+ next_super_owner: next_super_owner,
27
+ source_location: source_location,
28
+ steps: steps.map(&:to_h)
23
29
  }
24
30
  end
31
+
32
+ private
33
+
34
+ def coerce_step(step)
35
+ return step if step.is_a?(DispatchStep)
36
+
37
+ DispatchStep.new(**step)
38
+ end
25
39
  end
26
40
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WhyChain
4
+ # Formats a human-friendly runtime dispatch explanation.
5
+ class Explainer
6
+ def initialize(trace, method_name)
7
+ @trace = trace
8
+ @method_name = method_name
9
+ end
10
+
11
+ def to_s
12
+ lines = ["Ruby dispatch explanation for :#{@method_name}", ""]
13
+
14
+ explanation_steps.each_with_index do |step, index|
15
+ lines.concat(step_lines(step, index))
16
+ end
17
+
18
+ lines.join("\n")
19
+ end
20
+
21
+ private
22
+
23
+ def explanation_steps
24
+ return @trace.steps unless @trace.steps.empty?
25
+
26
+ [DispatchStep.new(owner: @trace.owner, source_location: @trace.source_location)]
27
+ end
28
+
29
+ def format_source_location(source_location)
30
+ source_location ? source_location.join(":") : "<native>"
31
+ end
32
+
33
+ def step_lines(step, index)
34
+ lines = [
35
+ "#{index + 1}. #{step.owner}##{@method_name}",
36
+ " defined at:",
37
+ " #{format_source_location(step.source_location)}"
38
+ ]
39
+
40
+ return lines unless index < explanation_steps.length - 1
41
+
42
+ lines + ["", " calls super ->", ""]
43
+ end
44
+ end
45
+ end
@@ -12,7 +12,9 @@ module WhyChain
12
12
  DispatchTrace.new(
13
13
  lookup_chain: lookup_chain,
14
14
  owner: owner,
15
- next_super_owner: next_super_owner
15
+ next_super_owner: next_super_owner,
16
+ source_location: method_object.source_location,
17
+ steps: steps
16
18
  )
17
19
  end
18
20
 
@@ -23,11 +25,32 @@ module WhyChain
23
25
  end
24
26
 
25
27
  def owner
26
- @owner ||= @object.method(@method_name).owner
28
+ @owner ||= method_object.owner
29
+ end
30
+
31
+ def method_object
32
+ @method_object ||= @object.method(@method_name)
27
33
  end
28
34
 
29
35
  def next_super_owner
30
36
  MethodLocator.new(lookup_chain, owner, @method_name).next_super_owner
31
37
  end
38
+
39
+ def steps
40
+ lookup_chain.filter_map do |mod|
41
+ next unless defines_instance_method?(mod)
42
+
43
+ DispatchStep.new(
44
+ owner: mod,
45
+ source_location: mod.instance_method(@method_name).source_location
46
+ )
47
+ end
48
+ end
49
+
50
+ def defines_instance_method?(mod)
51
+ mod.method_defined?(@method_name, false) ||
52
+ mod.private_method_defined?(@method_name, false) ||
53
+ mod.protected_method_defined?(@method_name, false)
54
+ end
32
55
  end
33
56
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WhyChain
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/why_chain.rb CHANGED
@@ -3,7 +3,9 @@
3
3
  require_relative "why_chain/version"
4
4
  require_relative "why_chain/tracer"
5
5
  require_relative "why_chain/dispatch_trace"
6
+ require_relative "why_chain/dispatch_step"
6
7
  require_relative "why_chain/method_locator"
8
+ require_relative "why_chain/explainer"
7
9
 
8
10
  # Entry point for WhyChain runtime dispatch introspection.
9
11
  module WhyChain
@@ -12,4 +14,8 @@ module WhyChain
12
14
  def self.trace(object, method_name)
13
15
  Tracer.new(object, method_name).trace
14
16
  end
17
+
18
+ def self.explain(object, method_name)
19
+ Explainer.new(trace(object, method_name), method_name).to_s
20
+ end
15
21
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: why_chain
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - alessio salati
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-10 00:00:00.000000000 Z
11
+ date: 2026-05-11 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: WhyChain is a tiny educational gem to inspect lookup chain, method owner,
14
14
  and next super target in Ruby.
@@ -27,7 +27,9 @@ files:
27
27
  - README.md
28
28
  - Rakefile
29
29
  - lib/why_chain.rb
30
+ - lib/why_chain/dispatch_step.rb
30
31
  - lib/why_chain/dispatch_trace.rb
32
+ - lib/why_chain/explainer.rb
31
33
  - lib/why_chain/method_locator.rb
32
34
  - lib/why_chain/tracer.rb
33
35
  - lib/why_chain/version.rb