why_chain 0.2.1 → 0.3.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 +6 -0
- data/Gemfile.lock +1 -1
- data/lib/why_chain/dispatch_step.rb +5 -3
- data/lib/why_chain/explainer/colorizer.rb +51 -0
- data/lib/why_chain/explainer/compact_formatter.rb +35 -0
- data/lib/why_chain/explainer/teaching_formatter.rb +39 -0
- data/lib/why_chain/explainer.rb +15 -23
- data/lib/why_chain/tracer.rb +14 -1
- data/lib/why_chain/version.rb +1 -1
- data/lib/why_chain.rb +8 -2
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2fa488f47789be0531b412a2c9407deda76c1cf5760f1a13510f313d852dbe5a
|
|
4
|
+
data.tar.gz: d464be25491fb7111b91869d340bb3f8ab8c003878be50d80ff8f2deaf765719
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bc4a485648f1dc97335cb4bec48909903156f7c464f3ac637c1945ea6ec33ad0c2ea4b668e15e5054f5651075e5ce51d17b2844c1ad9f0e9df121f03a8faac95
|
|
7
|
+
data.tar.gz: 9f2b823a8e304dfc90ca9d704e320d9b98b090ac706841c9300cff024805ef568569c861526e94e872f5a89f0c0a131c9548e05ad24be27696f1e04f59f495c1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-05-15
|
|
4
|
+
|
|
5
|
+
- Add style/color options to `WhyChain.explain` with `:teaching` and compact debug format.
|
|
6
|
+
- Introduce compact step labels for dispatch kind: `singleton`, `prepend`, `include`, and `class`.
|
|
7
|
+
- Refactor explainer internals into dedicated formatter and colorizer classes for maintainability.
|
|
8
|
+
|
|
3
9
|
## [0.2.1] - 2026-05-11
|
|
4
10
|
|
|
5
11
|
- Refactor internals by extracting shared method-definition predicate into `MethodDefinition`.
|
data/Gemfile.lock
CHANGED
|
@@ -3,17 +3,19 @@
|
|
|
3
3
|
module WhyChain
|
|
4
4
|
# Immutable value object for a single dispatch step.
|
|
5
5
|
class DispatchStep
|
|
6
|
-
attr_reader :owner, :source_location
|
|
6
|
+
attr_reader :owner, :source_location, :kind
|
|
7
7
|
|
|
8
|
-
def initialize(owner:, source_location:)
|
|
8
|
+
def initialize(owner:, source_location:, kind: nil)
|
|
9
9
|
@owner = owner
|
|
10
10
|
@source_location = source_location
|
|
11
|
+
@kind = kind
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def to_h
|
|
14
15
|
{
|
|
15
16
|
owner: owner,
|
|
16
|
-
source_location: source_location
|
|
17
|
+
source_location: source_location,
|
|
18
|
+
kind: kind
|
|
17
19
|
}
|
|
18
20
|
end
|
|
19
21
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WhyChain
|
|
4
|
+
class Explainer
|
|
5
|
+
# Handles ANSI color output and source location formatting.
|
|
6
|
+
class Colorizer
|
|
7
|
+
ANSI_CODES = {
|
|
8
|
+
title: "1;36",
|
|
9
|
+
owner: "32",
|
|
10
|
+
kind: "35",
|
|
11
|
+
label: "2;37",
|
|
12
|
+
location: "2;37",
|
|
13
|
+
native: "33",
|
|
14
|
+
arrow: "36"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def initialize(color_mode)
|
|
18
|
+
@use_color = resolve_color(color_mode)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def colorize(text, tone)
|
|
22
|
+
return text unless @use_color
|
|
23
|
+
|
|
24
|
+
code = ANSI_CODES.fetch(tone, "0")
|
|
25
|
+
"\e[#{code}m#{text}\e[0m"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def format_source_location(source_location, color: true)
|
|
29
|
+
formatted = source_location ? source_location.join(":") : "<native>"
|
|
30
|
+
return formatted unless color
|
|
31
|
+
|
|
32
|
+
source_location ? colorize(formatted, :location) : colorize(formatted, :native)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def resolve_color(color)
|
|
38
|
+
case color
|
|
39
|
+
when true
|
|
40
|
+
true
|
|
41
|
+
when false
|
|
42
|
+
false
|
|
43
|
+
when :auto
|
|
44
|
+
$stdout.tty? && ENV["CI"] != "true"
|
|
45
|
+
else
|
|
46
|
+
raise ArgumentError, "Unknown color mode: #{color.inspect}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WhyChain
|
|
4
|
+
class Explainer
|
|
5
|
+
# Compact debug-friendly explanation format.
|
|
6
|
+
class CompactFormatter
|
|
7
|
+
def initialize(method_name, steps, graph, colorizer)
|
|
8
|
+
@method_name = method_name
|
|
9
|
+
@steps = steps
|
|
10
|
+
@graph = graph
|
|
11
|
+
@colorizer = colorizer
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render
|
|
15
|
+
lines = [@colorizer.colorize("WhyChain :#{@method_name}", :title)]
|
|
16
|
+
|
|
17
|
+
@steps.each_with_index do |step, index|
|
|
18
|
+
lines << step_line(step, index)
|
|
19
|
+
lines << @colorizer.colorize(" -> super", :arrow) if @graph && index < @steps.length - 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
lines.join("\n")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def step_line(step, index)
|
|
28
|
+
kind = @colorizer.colorize("[#{step.kind || :unknown}]", :kind)
|
|
29
|
+
label = @colorizer.colorize("#{index + 1}) #{kind} #{step.owner}##{@method_name}", :owner)
|
|
30
|
+
location = @colorizer.format_source_location(step.source_location, color: false)
|
|
31
|
+
"#{label} #{@colorizer.colorize(location, :location)}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WhyChain
|
|
4
|
+
class Explainer
|
|
5
|
+
# Verbose didactic explanation format.
|
|
6
|
+
class TeachingFormatter
|
|
7
|
+
def initialize(method_name, steps, graph, colorizer)
|
|
8
|
+
@method_name = method_name
|
|
9
|
+
@steps = steps
|
|
10
|
+
@graph = graph
|
|
11
|
+
@colorizer = colorizer
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render
|
|
15
|
+
lines = [@colorizer.colorize("Ruby dispatch explanation for :#{@method_name}", :title), ""]
|
|
16
|
+
|
|
17
|
+
@steps.each_with_index do |step, index|
|
|
18
|
+
lines.concat(step_lines(step, index))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
lines.join("\n")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def step_lines(step, index)
|
|
27
|
+
lines = [
|
|
28
|
+
@colorizer.colorize("#{index + 1}. #{step.owner}##{@method_name}", :owner),
|
|
29
|
+
@colorizer.colorize(" defined at:", :label),
|
|
30
|
+
" #{@colorizer.format_source_location(step.source_location)}"
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
return lines unless @graph && index < @steps.length - 1
|
|
34
|
+
|
|
35
|
+
lines + ["", @colorizer.colorize(" calls super ->", :arrow), ""]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/why_chain/explainer.rb
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "explainer/colorizer"
|
|
4
|
+
require_relative "explainer/teaching_formatter"
|
|
5
|
+
require_relative "explainer/compact_formatter"
|
|
6
|
+
|
|
3
7
|
module WhyChain
|
|
4
8
|
# Formats a human-friendly runtime dispatch explanation.
|
|
5
9
|
class Explainer
|
|
6
|
-
def initialize(trace, method_name)
|
|
10
|
+
def initialize(trace, method_name, style: :teaching, color: :auto, graph: true)
|
|
7
11
|
@trace = trace
|
|
8
12
|
@method_name = method_name
|
|
13
|
+
@style = style
|
|
14
|
+
@graph = graph
|
|
15
|
+
@colorizer = Colorizer.new(color)
|
|
9
16
|
end
|
|
10
17
|
|
|
11
18
|
def to_s
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
case @style
|
|
20
|
+
when :teaching
|
|
21
|
+
TeachingFormatter.new(@method_name, explanation_steps, @graph, @colorizer).render
|
|
22
|
+
when :compact
|
|
23
|
+
CompactFormatter.new(@method_name, explanation_steps, @graph, @colorizer).render
|
|
24
|
+
else
|
|
25
|
+
raise ArgumentError, "Unknown style: #{@style.inspect}"
|
|
16
26
|
end
|
|
17
|
-
|
|
18
|
-
lines.join("\n")
|
|
19
27
|
end
|
|
20
28
|
|
|
21
29
|
private
|
|
@@ -25,21 +33,5 @@ module WhyChain
|
|
|
25
33
|
|
|
26
34
|
[DispatchStep.new(owner: @trace.owner, source_location: @trace.source_location)]
|
|
27
35
|
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
36
|
end
|
|
45
37
|
end
|
data/lib/why_chain/tracer.rb
CHANGED
|
@@ -37,14 +37,27 @@ module WhyChain
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def steps
|
|
40
|
+
receiver_class = @object.is_a?(Module) ? @object : @object.class
|
|
41
|
+
receiver_class_index = lookup_chain.index(receiver_class)
|
|
42
|
+
|
|
40
43
|
lookup_chain.filter_map do |mod|
|
|
41
44
|
next unless MethodDefinition.defined_directly?(mod, @method_name)
|
|
42
45
|
|
|
43
46
|
DispatchStep.new(
|
|
44
47
|
owner: mod,
|
|
45
|
-
source_location: mod.instance_method(@method_name).source_location
|
|
48
|
+
source_location: mod.instance_method(@method_name).source_location,
|
|
49
|
+
kind: step_kind_for(mod, receiver_class_index)
|
|
46
50
|
)
|
|
47
51
|
end
|
|
48
52
|
end
|
|
53
|
+
|
|
54
|
+
def step_kind_for(mod, receiver_class_index)
|
|
55
|
+
return :singleton if mod == @object.singleton_class
|
|
56
|
+
return :class if mod.is_a?(Class)
|
|
57
|
+
|
|
58
|
+
return :include unless receiver_class_index
|
|
59
|
+
|
|
60
|
+
lookup_chain.index(mod) < receiver_class_index ? :prepend : :include
|
|
61
|
+
end
|
|
49
62
|
end
|
|
50
63
|
end
|
data/lib/why_chain/version.rb
CHANGED
data/lib/why_chain.rb
CHANGED
|
@@ -16,7 +16,13 @@ module WhyChain
|
|
|
16
16
|
Tracer.new(object, method_name).trace
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def self.explain(object, method_name)
|
|
20
|
-
Explainer.new(
|
|
19
|
+
def self.explain(object, method_name, style: :teaching, color: :auto, graph: true)
|
|
20
|
+
Explainer.new(
|
|
21
|
+
trace(object, method_name),
|
|
22
|
+
method_name,
|
|
23
|
+
style: style,
|
|
24
|
+
color: color,
|
|
25
|
+
graph: graph
|
|
26
|
+
).to_s
|
|
21
27
|
end
|
|
22
28
|
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.3.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-15 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.
|
|
@@ -30,6 +30,9 @@ files:
|
|
|
30
30
|
- lib/why_chain/dispatch_step.rb
|
|
31
31
|
- lib/why_chain/dispatch_trace.rb
|
|
32
32
|
- lib/why_chain/explainer.rb
|
|
33
|
+
- lib/why_chain/explainer/colorizer.rb
|
|
34
|
+
- lib/why_chain/explainer/compact_formatter.rb
|
|
35
|
+
- lib/why_chain/explainer/teaching_formatter.rb
|
|
33
36
|
- lib/why_chain/method_definition.rb
|
|
34
37
|
- lib/why_chain/method_locator.rb
|
|
35
38
|
- lib/why_chain/tracer.rb
|