why_chain 0.1.0 → 0.2.1

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: 57709eb19a0190d1c1cde75dc2cb9b4a657edfd5b4a57cda237a7665b9b027f4
4
+ data.tar.gz: 76a6b127db4b8acaa90f4e7047e2d64d4d12ff5e8553c3faf48d8b20a6b23141
5
5
  SHA512:
6
- metadata.gz: ea34de73851a224c13f468019c23cc9379edc1ac85b75e0975dc3419a67dc3a3a47bca4e19d12591f3e1335a54ad788e0bb0feafcd1838bc4522c965010efbae
7
- data.tar.gz: 0cd9ad56a88c26c350c9f9d35918c2eef28223d0a751b31a584292c9051e44cdde069f12eb1307850f064fee0f39b7a0c34b82c37c1d3cc2ab226adf9890c019
6
+ metadata.gz: b1b2fd4c412353c75e0fc7b2645fae16bd76587e91b312785125e48487b5a7304c12ff72f4569955471fb9c657c3db474ac3190163860c1d9d166ad70bbcc78a
7
+ data.tar.gz: fce81cd5ebf55df0fe92a8ddcb6fb1667f836ca806cb323625447fe77e22c2ab4dd1ce3a260981f1cb7b0787b1a4f74e7b92680f84d42fe41da0800dd891de51
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.1] - 2026-05-11
4
+
5
+ - Refactor internals by extracting shared method-definition predicate into `MethodDefinition`.
6
+ - Remove duplicated visibility checks from `Tracer` and `MethodLocator` without behavior changes.
7
+
8
+ ## [0.2.0] - 2026-05-11
9
+
10
+ - Add `WhyChain.explain(object, method_name)` for human-readable dispatch output.
11
+ - Extend `DispatchTrace` with `source_location` and ordered `steps` data.
12
+ - Introduce `DispatchStep` value object for typed dispatch step representation.
13
+ - Add focused specs for explain output and super-path step tracing.
14
+
3
15
  ## [0.1.0] - 2026-05-10
4
16
 
5
17
  - 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.1)
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
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WhyChain
4
+ # Shared predicate for direct method definitions in a lookup node.
5
+ module MethodDefinition
6
+ module_function
7
+
8
+ def defined_directly?(mod, method_name)
9
+ mod.method_defined?(method_name, false) ||
10
+ mod.private_method_defined?(method_name, false) ||
11
+ mod.protected_method_defined?(method_name, false)
12
+ end
13
+ end
14
+ end
@@ -15,16 +15,8 @@ module WhyChain
15
15
  return nil unless owner_index
16
16
 
17
17
  @lookup_chain[(owner_index + 1)..].find do |mod|
18
- defines_instance_method?(mod)
18
+ MethodDefinition.defined_directly?(mod, @method_name)
19
19
  end
20
20
  end
21
-
22
- private
23
-
24
- def defines_instance_method?(mod)
25
- mod.method_defined?(@method_name, false) ||
26
- mod.private_method_defined?(@method_name, false) ||
27
- mod.protected_method_defined?(@method_name, false)
28
- end
29
21
  end
30
22
  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,26 @@ 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 MethodDefinition.defined_directly?(mod, @method_name)
42
+
43
+ DispatchStep.new(
44
+ owner: mod,
45
+ source_location: mod.instance_method(@method_name).source_location
46
+ )
47
+ end
48
+ end
32
49
  end
33
50
  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.1"
5
5
  end
data/lib/why_chain.rb CHANGED
@@ -3,7 +3,10 @@
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"
7
+ require_relative "why_chain/method_definition"
6
8
  require_relative "why_chain/method_locator"
9
+ require_relative "why_chain/explainer"
7
10
 
8
11
  # Entry point for WhyChain runtime dispatch introspection.
9
12
  module WhyChain
@@ -12,4 +15,8 @@ module WhyChain
12
15
  def self.trace(object, method_name)
13
16
  Tracer.new(object, method_name).trace
14
17
  end
18
+
19
+ def self.explain(object, method_name)
20
+ Explainer.new(trace(object, method_name), method_name).to_s
21
+ end
15
22
  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.1
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,10 @@ 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
33
+ - lib/why_chain/method_definition.rb
31
34
  - lib/why_chain/method_locator.rb
32
35
  - lib/why_chain/tracer.rb
33
36
  - lib/why_chain/version.rb