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 +4 -4
- data/CHANGELOG.md +7 -0
- data/Gemfile.lock +1 -1
- data/README.md +26 -1
- data/lib/why_chain/dispatch_step.rb +20 -0
- data/lib/why_chain/dispatch_trace.rb +17 -3
- data/lib/why_chain/explainer.rb +45 -0
- data/lib/why_chain/tracer.rb +25 -2
- data/lib/why_chain/version.rb +1 -1
- data/lib/why_chain.rb +6 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a6b36f3e181fd925c701bfc22b05348a39044ca3158e42133091c123aca5dcb4
|
|
4
|
+
data.tar.gz: c0e15d0ac8df970c25f4ea071a1965e13c3d57a77a0152b46440fa52585f3338
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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
|
data/lib/why_chain/tracer.rb
CHANGED
|
@@ -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 ||=
|
|
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
|
data/lib/why_chain/version.rb
CHANGED
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.
|
|
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-
|
|
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
|