visualize_ruby 0.11.0 → 0.16.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 +28 -1
- data/README.md +12 -13
- data/docs/asset-manifest.json +6 -0
- data/docs/index.html +1 -0
- data/docs/manifest.json +15 -0
- data/docs/service-worker.js +1 -0
- data/docs/static/css/main.8d2a69c3.css +2 -0
- data/docs/static/css/main.8d2a69c3.css.map +1 -0
- data/docs/static/js/main.fcab249e.js +2 -0
- data/docs/static/js/main.fcab249e.js.map +1 -0
- data/lib/visualize_ruby.rb +2 -0
- data/lib/visualize_ruby/builder.rb +37 -19
- data/lib/visualize_ruby/edge.rb +7 -3
- data/lib/visualize_ruby/execution_tracer.rb +14 -33
- data/lib/visualize_ruby/graphviz.rb +23 -16
- data/lib/visualize_ruby/highlight_tracer.rb +89 -24
- data/lib/visualize_ruby/input_coercer.rb +61 -0
- data/lib/visualize_ruby/namable.rb +37 -0
- data/lib/visualize_ruby/node.rb +5 -4
- data/lib/visualize_ruby/parser.rb +1 -0
- data/lib/visualize_ruby/parser/begin.rb +1 -1
- data/lib/visualize_ruby/parser/block.rb +1 -10
- data/lib/visualize_ruby/parser/case.rb +13 -8
- data/lib/visualize_ruby/parser/conditions.rb +5 -6
- data/lib/visualize_ruby/parser/return.rb +12 -0
- data/lib/visualize_ruby/runner.rb +78 -16
- data/lib/visualize_ruby/touchable.rb +13 -6
- data/lib/visualize_ruby/version.rb +1 -1
- data/logo.jpg +0 -0
- data/visualize_ruby.gemspec +4 -3
- metadata +33 -14
- data/CODE_OF_CONDUCT.md +0 -74
@@ -8,9 +8,12 @@ module VisualizeRuby
|
|
8
8
|
# @param [Hash{line: Integer, event: Symbol}] executed_events
|
9
9
|
# @param [Symbol] color
|
10
10
|
def initialize(builder:, executed_events: [], color: OPTIONS.fetch(:color))
|
11
|
-
@builder
|
12
|
-
@executed_events
|
13
|
-
@color
|
11
|
+
@builder = builder
|
12
|
+
@executed_events = executed_events
|
13
|
+
@color = color
|
14
|
+
@last_touched_node = nil
|
15
|
+
@last_touched_edge = nil
|
16
|
+
@step_increment = 0
|
14
17
|
end
|
15
18
|
|
16
19
|
# @return [VisualizeRuby::Builder::Result]
|
@@ -21,43 +24,105 @@ module VisualizeRuby
|
|
21
24
|
|
22
25
|
private
|
23
26
|
|
24
|
-
attr_reader :builder,
|
27
|
+
attr_reader :builder,
|
28
|
+
:color,
|
29
|
+
:executed_events,
|
30
|
+
:last_touched_node,
|
31
|
+
:last_touched_edge
|
25
32
|
|
26
33
|
def mark!
|
27
|
-
|
28
|
-
|
29
|
-
all_edges.detect do |e|
|
30
|
-
if e.node_a.line == a && (e.node_b.line || executed_lines.last) == b # end nodes do not have lineno
|
31
|
-
touch_nodes(e, except: [last_touch])
|
32
|
-
last_touch = e.nodes[1]
|
33
|
-
check_lineno_connections(e)
|
34
|
-
e.touch(color)
|
35
|
-
end
|
36
|
-
end || (last_touch = nil)
|
34
|
+
paired_line_events.to_a.each.with_index do |(a, c)|
|
35
|
+
build_exe_edge(a, c) unless find_edge(a, c)
|
37
36
|
end
|
38
37
|
end
|
39
38
|
|
40
|
-
def
|
41
|
-
|
42
|
-
if
|
43
|
-
|
44
|
-
|
39
|
+
def find_edge(a, c)
|
40
|
+
all_edges.detect do |e|
|
41
|
+
if e.node_a.line == a && (e.node_b.line || executed_lines.last) == c # end nodes do not have lineno
|
42
|
+
check_lineno_connections(e)
|
43
|
+
touch(e)
|
45
44
|
end
|
46
45
|
end
|
47
46
|
end
|
48
47
|
|
49
|
-
def
|
50
|
-
|
48
|
+
def step(increment=true)
|
49
|
+
if increment
|
50
|
+
@step_increment += 1
|
51
|
+
else
|
52
|
+
@step_increment
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def touch(e, except = nil)
|
57
|
+
touched_edge = false
|
58
|
+
e.nodes.reject { |n| n == last_touched_node || n == except }.each do |n|
|
59
|
+
touched_edge = touch_in_order(e, n, touched_edge)
|
60
|
+
end
|
61
|
+
touch_edge(e, touched_edge) # if it didn't happen already
|
62
|
+
end
|
63
|
+
|
64
|
+
def touch_in_order(e, n, touched_edge)
|
65
|
+
if e.node_a == last_touched_node
|
66
|
+
touched_edge = touch_edge(e, touched_edge)
|
67
|
+
n.touch(color, step: step)
|
68
|
+
else
|
69
|
+
n.touch(color, step: step)
|
70
|
+
touched_edge = touch_edge(e, touched_edge)
|
71
|
+
end
|
72
|
+
@last_touched_node = n
|
73
|
+
touched_edge
|
74
|
+
end
|
75
|
+
|
76
|
+
def touch_edge(e, touched_edge)
|
77
|
+
if !touched_edge && e != last_touched_edge
|
78
|
+
e.touch(color, step: step)
|
79
|
+
@last_touched_edge = e
|
80
|
+
touched_edge = true
|
81
|
+
end
|
82
|
+
touched_edge
|
83
|
+
end
|
84
|
+
|
85
|
+
def build_exe_edge(a, c)
|
86
|
+
node_a, graph_a = find_node(line: a, graphs: builder.graphs)
|
87
|
+
node_b, _ = find_node(line: c, graphs: builder.graphs)
|
88
|
+
if node_a && node_b && node_a != node_b
|
89
|
+
touch(exe_edge(graph_a, node_a, node_b))
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def find_node(line:, graphs: builder.graphs)
|
94
|
+
graphs.each do |graph|
|
95
|
+
graph = graph
|
96
|
+
node = graph.nodes.detect { |n| n.line == line }
|
97
|
+
return node, graph if node
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def exe_edge(graph_a, node_a, node_b)
|
102
|
+
if (exe_edge = all_edges.detect { |e| e.node_b == node_b && e.node_a == node_a })
|
103
|
+
exe_edge
|
104
|
+
else
|
105
|
+
new = Edge.new(nodes: [node_a, node_b], type: :execution, style: :dotted)
|
106
|
+
graph_a.edges << new
|
107
|
+
new
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Associated nodes and edges that are on the same line
|
112
|
+
def check_lineno_connections(e)
|
113
|
+
e.nodes.each do |n|
|
114
|
+
touch(n.lineno_connection, e.nodes) if n.lineno_connection
|
115
|
+
end
|
51
116
|
end
|
52
117
|
|
53
118
|
def all_edges
|
54
|
-
|
119
|
+
builder.graphs.flat_map(&:edges)
|
55
120
|
end
|
56
121
|
|
57
122
|
def paired_line_events
|
58
123
|
line_events = executed_events.select { |e| e[:event] == :line } + [executed_events.last]
|
59
|
-
setup_paring(line_events) do |a,
|
60
|
-
[line_events[a][:line], line_events[
|
124
|
+
setup_paring(line_events) do |a, c|
|
125
|
+
[line_events[a][:line], line_events[c][:line]]
|
61
126
|
end
|
62
127
|
end
|
63
128
|
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require "tempfile"
|
2
|
+
|
3
|
+
module VisualizeRuby
|
4
|
+
class InputCoercer
|
5
|
+
attr_reader :name, :input
|
6
|
+
|
7
|
+
def initialize(input, name:)
|
8
|
+
@input = input
|
9
|
+
@name = name
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_file
|
13
|
+
@to_file ||= case input
|
14
|
+
when File
|
15
|
+
input
|
16
|
+
when Pathname
|
17
|
+
File.open(input)
|
18
|
+
when String
|
19
|
+
temp_file
|
20
|
+
else
|
21
|
+
raise ArgumentError, "#{name} was given an unknown type #{input.class}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def read
|
26
|
+
case input
|
27
|
+
when String
|
28
|
+
input
|
29
|
+
when File, Tempfile, Pathname
|
30
|
+
to_file.read
|
31
|
+
else
|
32
|
+
raise ArgumentError, "#{name} was given an unknown type #{input.class}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def close!
|
37
|
+
@temp_file.close! if @temp_file
|
38
|
+
end
|
39
|
+
|
40
|
+
def temp_file
|
41
|
+
@temp_file ||= begin
|
42
|
+
file = Tempfile.new(%w(calling_code .rb))
|
43
|
+
file.write(input)
|
44
|
+
file.rewind
|
45
|
+
file
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def load_file
|
50
|
+
load(to_file.path)
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_proc
|
54
|
+
if input.is_a?(Proc)
|
55
|
+
input
|
56
|
+
else
|
57
|
+
Proc.new { load_file }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module VisualizeRuby
|
2
|
+
module Namable
|
3
|
+
DEFAULT_DISPLAYER = -> (attr) do
|
4
|
+
r = attr.values.compact.join(" ")
|
5
|
+
r.empty? ? nil : r
|
6
|
+
end
|
7
|
+
|
8
|
+
def post_initialize(name_displayer: nil, **args)
|
9
|
+
@name_displayer = name_displayer || DEFAULT_DISPLAYER
|
10
|
+
super if defined? super
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
@name_displayer.call(build_name_list)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.included(klass)
|
18
|
+
klass.extend(ClassMethods)
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
def add_names(*names)
|
23
|
+
@name_registry = name_registry.concat(names).uniq
|
24
|
+
end
|
25
|
+
|
26
|
+
def name_registry
|
27
|
+
@name_registry ||= []
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def build_name_list
|
34
|
+
self.class.name_registry.each_with_object({}) { |meth, hash| hash[meth] = __send__(meth) }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/visualize_ruby/node.rb
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
module VisualizeRuby
|
2
2
|
class Node
|
3
|
+
include Namable
|
4
|
+
add_names :label
|
3
5
|
include Touchable
|
4
6
|
include Optionalable
|
5
|
-
attr_reader :style, :line
|
7
|
+
attr_reader :style, :line, :label
|
6
8
|
attr_accessor :type, :id, :lineno_connection
|
7
|
-
|
8
9
|
def initialize(name: nil, type: :action, style: :rounded, ast: nil, line: nil, id: nil, **opts)
|
9
|
-
@
|
10
|
+
@label = name || (ast ? AstHelper.new(ast).description : nil)
|
10
11
|
@type = type
|
11
12
|
@style = style
|
12
|
-
@id = id || (ast ? AstHelper.new(ast).id :
|
13
|
+
@id = id || (ast ? AstHelper.new(ast).id : @label)
|
13
14
|
@line = line || AstHelper.new(ast).first_line
|
14
15
|
post_initialize(opts)
|
15
16
|
end
|
@@ -23,7 +23,7 @@ module VisualizeRuby
|
|
23
23
|
edge = _edges.detect { |e| e.node_b == n }
|
24
24
|
edge.nodes[1] = last_node
|
25
25
|
nodes.delete(n)
|
26
|
-
|
26
|
+
elsif n.type != :return # 1. (-> n) 2. (-> n -> last_node)
|
27
27
|
edges << Edge.new(nodes: [n, last_node])
|
28
28
|
end
|
29
29
|
end
|
@@ -18,17 +18,8 @@ module VisualizeRuby
|
|
18
18
|
|
19
19
|
def yield_block(action, item, on_object, color, enumerable)
|
20
20
|
nodes << on_object_node = Node.new(ast: on_object, color: color)
|
21
|
-
nodes << item_node = Node.new(type: :argument, ast: item, color: color) if item
|
22
21
|
nodes << action_node = Node.new(ast: action, color: color)
|
23
|
-
|
24
|
-
if item_node
|
25
|
-
edges << Edge.new(nodes: [on_object_node, item_node], color: color)
|
26
|
-
edges << Edge.new(nodes: [item_node, action_node], color: color)
|
27
|
-
action_node.lineno_connection = edges.last
|
28
|
-
else
|
29
|
-
edges << Edge.new(nodes: [on_object_node, action_node], color: color)
|
30
|
-
end
|
31
|
-
|
22
|
+
edges << Edge.new(nodes: [on_object_node, action_node], name: item, color: color)
|
32
23
|
edges << Edge.new(nodes: [action_node, on_object_node], color: color, name: "↺") if enumerable
|
33
24
|
end
|
34
25
|
|
@@ -3,20 +3,25 @@ module VisualizeRuby
|
|
3
3
|
class Case < Base
|
4
4
|
# @return [Array<VisualizeRuby::Node>, Array<VisualizeRuby::Edge>]
|
5
5
|
def parse
|
6
|
-
|
7
|
-
condition_node = Node.new(ast:
|
6
|
+
ast_condition_node, *ast_when_nodes, ast_else_node = @ast.children
|
7
|
+
condition_node = Node.new(ast: ast_condition_node, type: :decision)
|
8
8
|
nodes << condition_node
|
9
|
-
|
10
|
-
|
9
|
+
|
10
|
+
ast_when_nodes.each do |ast_when_node|
|
11
|
+
edge_name, actions = ast_when_node.children
|
11
12
|
action_nodes, action_edges = Parser.new(ast: actions).parse
|
12
13
|
edges << Edge.new(name: AstHelper.new(edge_name).description, nodes: [condition_node, action_nodes.first])
|
13
14
|
nodes.concat(action_nodes)
|
14
15
|
edges.concat(action_edges)
|
15
16
|
end
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
|
18
|
+
if ast_else_node
|
19
|
+
else_node = Node.new(ast: ast_else_node, type: :action)
|
20
|
+
else_edge = Edge.new(name: "else", nodes: [condition_node, else_node])
|
21
|
+
nodes << else_node
|
22
|
+
edges << else_edge
|
23
|
+
end
|
24
|
+
|
20
25
|
return nodes, edges
|
21
26
|
end
|
22
27
|
end
|
@@ -4,24 +4,23 @@ module VisualizeRuby
|
|
4
4
|
def set_conditions(condition)
|
5
5
|
condition_nodes, condition_edges = Parser.new(ast: condition).parse
|
6
6
|
condition_nodes.first.type = :decision
|
7
|
-
nodes.
|
8
|
-
edges.
|
7
|
+
nodes.unshift(*condition_nodes)
|
8
|
+
edges.unshift(*condition_edges)
|
9
9
|
condition_nodes
|
10
10
|
end
|
11
11
|
|
12
12
|
# @return [Array<VisualizeRuby::Node>, Array<VisualizeRuby::Edge>]
|
13
13
|
def parse
|
14
14
|
last_node = nil
|
15
|
-
|
16
|
-
|
17
|
-
node = set_conditions(c).first
|
15
|
+
@ast.children.reverse.map do |c|
|
16
|
+
node = set_conditions(c).last
|
18
17
|
if last_node
|
19
18
|
edges << Edge.new(name: self.class.name.split("::").last.upcase, nodes: [node, last_node])
|
20
19
|
last_node.lineno_connection = edges.last
|
21
20
|
end
|
22
21
|
last_node = node
|
23
22
|
node
|
24
|
-
end
|
23
|
+
end
|
25
24
|
return nodes, edges
|
26
25
|
end
|
27
26
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module VisualizeRuby
|
2
|
+
class Parser
|
3
|
+
class Return < Base
|
4
|
+
# @return [Array<VisualizeRuby::Node>, Array<VisualizeRuby::Edge>]
|
5
|
+
def parse
|
6
|
+
nodes, edges = Parser.new(ast: @ast.children[0]).parse
|
7
|
+
nodes.last.type = :return
|
8
|
+
return [nodes, edges]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -1,36 +1,98 @@
|
|
1
|
+
require "active_support/core_ext/hash/compact"
|
2
|
+
|
1
3
|
module VisualizeRuby
|
2
4
|
class Runner
|
3
|
-
# @return [String,
|
5
|
+
# @return [String, File, Pathname, Proc] The code that calls the graphed code.
|
4
6
|
attr_accessor :calling_code
|
5
|
-
# @return [String,
|
7
|
+
# @return [String, File, Pathname] The code to be graphed.
|
6
8
|
attr_accessor :ruby_code
|
7
|
-
# @return [Symbol, NilClass]
|
9
|
+
# @return [Symbol, NilClass, String] To output DOT format as a string use String class as value.
|
8
10
|
attr_accessor :output_format
|
9
|
-
# @return [String]
|
11
|
+
# @return [String, Pathname] Add the exe name to select a format ie. png, jpg, svg, dot...
|
10
12
|
attr_accessor :output_path
|
11
|
-
# @param [
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
# @param [TrueClass, FalseClass] in line body when calling methods on self. Looks better when tracing execution.
|
14
|
+
attr_writer :in_line_local_method_calls
|
15
|
+
# @param [TrueClass, FalseClass] Duplicate nodes with the same description are merged to point single node.
|
16
|
+
attr_writer :unique_nodes
|
17
|
+
# @params [Array<String>, NilClass] When a graph has many sub-graphs only include listed.
|
18
|
+
attr_writer :only_graphs
|
19
|
+
# @param [String, File]
|
20
|
+
# @param [Proc]
|
21
|
+
def trace(calling_code = nil, &block)
|
22
|
+
@calling_code = calling_code || block
|
17
23
|
end
|
18
24
|
|
25
|
+
attr_reader :output
|
26
|
+
|
19
27
|
def run!
|
20
|
-
|
21
|
-
|
28
|
+
@run ||= begin
|
29
|
+
highlight_trace
|
30
|
+
@output ||= VisualizeRuby::Graphviz.new(
|
31
|
+
builder,
|
32
|
+
graphs: filter_graphs,
|
33
|
+
unique_nodes: unique_nodes,
|
34
|
+
only_graphs: only_graphs,
|
35
|
+
).to_graph({ path: output_path, format: output_format }.compact)
|
36
|
+
end
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def in_line_local_method_calls
|
41
|
+
@in_line_local_method_calls ||= traced?
|
42
|
+
end
|
43
|
+
|
44
|
+
def options(opts={})
|
45
|
+
opts.each do |key, value|
|
46
|
+
public_send("#{key}=", value)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def graphs
|
51
|
+
graphs = builder.graphs.map(&:name).compact
|
52
|
+
[builder.options.fetch(:label, "default")].concat(graphs)
|
22
53
|
end
|
23
54
|
|
24
55
|
private
|
25
56
|
|
26
57
|
def builder
|
27
|
-
@builder ||= VisualizeRuby::Builder.new(
|
58
|
+
@builder ||= VisualizeRuby::Builder.new(
|
59
|
+
ruby_code: ruby_code,
|
60
|
+
in_line_local_method_calls: in_line_local_method_calls
|
61
|
+
).build
|
62
|
+
end
|
63
|
+
|
64
|
+
def filter_graphs
|
65
|
+
if traced?
|
66
|
+
builder.graphs.select do |g|
|
67
|
+
g.nodes.any? { |n| n.touched > 0 }
|
68
|
+
end
|
69
|
+
else
|
70
|
+
builder.graphs
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def traced?
|
75
|
+
!!calling_code
|
76
|
+
end
|
77
|
+
|
78
|
+
def unique_nodes
|
79
|
+
@unique_nodes ||= true
|
80
|
+
end
|
81
|
+
|
82
|
+
def only_graphs
|
83
|
+
@only_graphs ||= nil
|
28
84
|
end
|
29
85
|
|
30
86
|
def highlight_trace
|
31
|
-
return unless
|
32
|
-
executed_events = VisualizeRuby::ExecutionTracer.new(
|
33
|
-
|
87
|
+
return unless traced?
|
88
|
+
executed_events = VisualizeRuby::ExecutionTracer.new(
|
89
|
+
builder,
|
90
|
+
calling_code: calling_code
|
91
|
+
).trace.executed_events
|
92
|
+
VisualizeRuby::HighlightTracer.new(
|
93
|
+
builder: builder,
|
94
|
+
executed_events: executed_events
|
95
|
+
).highlight!
|
34
96
|
end
|
35
97
|
end
|
36
98
|
end
|