visualize_ruby 0.8.0 → 0.11.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 +17 -0
- data/README.md +90 -3
- data/lib/visualize_ruby/ast_helper.rb +21 -0
- data/lib/visualize_ruby/builder.rb +70 -16
- data/lib/visualize_ruby/edge.rb +34 -12
- data/lib/visualize_ruby/execution_tracer.rb +64 -0
- data/lib/visualize_ruby/graph.rb +17 -4
- data/lib/visualize_ruby/graphviz.rb +26 -12
- data/lib/visualize_ruby/highlight_tracer.rb +74 -0
- data/lib/visualize_ruby/node.rb +22 -9
- data/lib/visualize_ruby/optionalable.rb +15 -0
- data/lib/visualize_ruby/parser/and.rb +0 -12
- data/lib/visualize_ruby/parser/begin.rb +23 -3
- data/lib/visualize_ruby/parser/block.rb +15 -18
- data/lib/visualize_ruby/parser/case.rb +2 -2
- data/lib/visualize_ruby/parser/conditions.rb +16 -0
- data/lib/visualize_ruby/parser/if.rb +21 -12
- data/lib/visualize_ruby/parser/or.rb +0 -10
- data/lib/visualize_ruby/parser/send.rb +1 -1
- data/lib/visualize_ruby/parser/str.rb +1 -1
- data/lib/visualize_ruby/parser.rb +0 -1
- data/lib/visualize_ruby/runner.rb +36 -0
- data/lib/visualize_ruby/touchable.rb +23 -0
- data/lib/visualize_ruby/version.rb +1 -1
- data/lib/visualize_ruby.rb +15 -0
- metadata +8 -3
- data/lib/visualize_ruby/parser/ast_helper.rb +0 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d233d2d1af5c3e092d04d497fddfc4c7165d56dbd021662cd58703d04ada8ce4
|
4
|
+
data.tar.gz: f60ac523929e8ae331ef4af3811ed600a4aa380ca9738d3bcdb72a6377f60097
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c46c248303e7d46e42ea47ddf2928baedfaa58cb12e6e2ed1b0f95b2f85df677d921278abac0a50bef8d13fdd820005a833866d9df1b6c87044fadab67eb79f4
|
7
|
+
data.tar.gz: 515f8cc61f5ecc2944402106db9707a365c35ad2a343811fd02ef7acd63b365aa27de36c339df308220221ae7bf80aacfedbe91517e298fde4d7da8f00a480d4
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,23 @@
|
|
1
1
|
# Changelog
|
2
2
|
All notable changes to this project will be documented in this file.
|
3
3
|
|
4
|
+
## 0.11.0 - 2018-07-26
|
5
|
+
### Enhancement
|
6
|
+
* Improved highlights execution path on flow chart. It using ruby code file or string to build the graph then a file or string of calling code.
|
7
|
+
* Better display blocks without arguments.
|
8
|
+
* Added DSL for graphing and tracing code.
|
9
|
+
|
10
|
+
## 0.10.0 - 2018-07-20
|
11
|
+
### Enhancement
|
12
|
+
* Highlights execution path on flow chart. It using ruby code file or string to build the graph then a file or string of calling code.
|
13
|
+
|
14
|
+
## 0.9.0 - 2018-07-17
|
15
|
+
### Enhancement
|
16
|
+
* Properly render Messy code, gilded Rose as example https://github.com/amckinnell/Gilded-Rose-Ruby/blob/master/lib/gilded_rose.rb
|
17
|
+
* Conditions with no else statement have an edge to an END node.
|
18
|
+
* All nodes have unique IDs based on source location.
|
19
|
+
Nodes can be merged based on there label with VisualizeRuby::Graphviz(graphs, label, unique_nodes: false)
|
20
|
+
|
4
21
|
## 0.8.0 - 2018-07-17
|
5
22
|
### Enhancement
|
6
23
|
* Better handle conditions outside of if statements.
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# VisualizeRuby
|
2
2
|
|
3
|
-
Write a Ruby
|
3
|
+
Write a Ruby code and see method interactions on a flow chart. Works with procedural code, bare methods, and Classes.
|
4
4
|
This is experimental project and does not support all types of code.
|
5
5
|
If you'd like it to support more types of code please pull request.
|
6
6
|
|
@@ -21,13 +21,25 @@ And then execute:
|
|
21
21
|
Or install it yourself as:
|
22
22
|
|
23
23
|
$ gem install visualize_ruby
|
24
|
+
|
25
|
+
### Install GraphViz
|
26
|
+
|
27
|
+
MacOS
|
28
|
+
|
29
|
+
$ brew install graphviz
|
30
|
+
|
31
|
+
Linux
|
32
|
+
|
33
|
+
$ sudo apt-get install graphviz
|
24
34
|
|
25
35
|
## Usage
|
26
36
|
|
37
|
+
### Create a graph by statically parsing the code.
|
38
|
+
|
27
39
|
```ruby
|
28
40
|
require "visualize_ruby"
|
29
41
|
|
30
|
-
ruby_code =
|
42
|
+
ruby_code = <<~RUBY
|
31
43
|
if hungry?
|
32
44
|
eat
|
33
45
|
else
|
@@ -36,8 +48,83 @@ ruby_code = <<-RUBY
|
|
36
48
|
RUBY
|
37
49
|
|
38
50
|
results = VisualizeRuby::Builder.new(ruby_code: ruby_code).build
|
39
|
-
VisualizeRuby::Graphviz.new(
|
51
|
+
VisualizeRuby::Graphviz.new(results).to_graph(path: "example.png")
|
52
|
+
```
|
53
|
+
[](./spec/examples/base_method.png)
|
54
|
+
### Add an execution path to graph
|
55
|
+
```ruby
|
56
|
+
require "visualize_ruby"
|
57
|
+
|
58
|
+
ruby_code = <<~RUBY
|
59
|
+
class Worker
|
60
|
+
def initialize(hungry:)
|
61
|
+
@hungry = hungry
|
62
|
+
end
|
63
|
+
|
64
|
+
def next_action
|
65
|
+
if hungry?
|
66
|
+
:eat
|
67
|
+
else
|
68
|
+
:work
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def hungry?
|
73
|
+
@hungry
|
74
|
+
end
|
75
|
+
end
|
76
|
+
RUBY
|
77
|
+
|
78
|
+
calling_code = <<~RUBY
|
79
|
+
Worker.new(hungry: true).next_action
|
80
|
+
RUBY
|
81
|
+
|
82
|
+
VisualizeRuby.new do |vb|
|
83
|
+
vb.ruby_code = ruby_code # String, IO
|
84
|
+
vb.trace(calling_code) # String, IO - optional
|
85
|
+
vb.output_path = "runner_trace.png" # file name with media extension.
|
86
|
+
end
|
40
87
|
```
|
88
|
+
[](./spec/examples/runner_trace.png)
|
89
|
+
|
90
|
+
### Visualize Loops
|
91
|
+
Adds a count if the node is called more than once.
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
require "visualize_ruby"
|
95
|
+
|
96
|
+
ruby_code = <<~RUBY
|
97
|
+
class Looping
|
98
|
+
def call
|
99
|
+
(0..5).each do
|
100
|
+
paint_town!
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def paint_town!
|
105
|
+
"hello"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
RUBY
|
109
|
+
|
110
|
+
calling_code = <<~RUBY
|
111
|
+
Worker.new(hungry: true).next_action
|
112
|
+
RUBY
|
113
|
+
|
114
|
+
VisualizeRuby.new do |vb|
|
115
|
+
vb.ruby_code = ruby_code # String, IO
|
116
|
+
vb.trace(calling_code) # String, IO - optional
|
117
|
+
vb.output_path = "loop.png" # file name with media extension.
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
[](./spec/examples/highlight_tracer_loop.png)
|
122
|
+
|
123
|
+
### Complex unrefactored code example
|
124
|
+
[Gilded Rose](https://github.com/amckinnell/Gilded-Rose-Ruby)
|
125
|
+
|
126
|
+
[](./spec/examples/gilded_rose.png)
|
127
|
+
|
41
128
|
|
42
129
|
## Development
|
43
130
|
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module VisualizeRuby
|
2
|
+
class AstHelper
|
3
|
+
def initialize(ast)
|
4
|
+
@ast = ast
|
5
|
+
end
|
6
|
+
|
7
|
+
def description
|
8
|
+
return @ast unless @ast.respond_to?(:type)
|
9
|
+
Unparser.unparse(@ast)
|
10
|
+
end
|
11
|
+
|
12
|
+
def id(description: self.description)
|
13
|
+
description.to_s + " L#{[@ast.location.first_line, @ast.location.last_line].compact.uniq.join("-")}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def first_line
|
17
|
+
return unless @ast
|
18
|
+
@ast.location.first_line
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -1,22 +1,55 @@
|
|
1
1
|
require "dissociated_introspection"
|
2
|
+
require "stringio"
|
3
|
+
require "tempfile"
|
2
4
|
|
3
5
|
module VisualizeRuby
|
4
6
|
class Builder
|
5
|
-
|
7
|
+
# @param [String, IO] ruby_code
|
6
8
|
def initialize(ruby_code:)
|
7
|
-
@ruby_code = ruby_code
|
9
|
+
@ruby_code = ruby_code.is_a?(String) ? StringIO.new(ruby_code) : ruby_code
|
8
10
|
end
|
9
11
|
|
10
12
|
def build
|
11
|
-
ruby_code = DissociatedIntrospection::RubyCode.build_from_source(@ruby_code)
|
13
|
+
ruby_code = DissociatedIntrospection::RubyCode.build_from_source(@ruby_code.read)
|
12
14
|
ruby_class = DissociatedIntrospection::RubyClass.new(ruby_code)
|
13
15
|
|
14
16
|
if ruby_class.class?
|
15
|
-
|
17
|
+
Result.new(
|
18
|
+
ruby_code: ruby_class.ruby_code.source,
|
19
|
+
ast: ruby_code.ast,
|
20
|
+
graphs: build_from_class(ruby_class),
|
21
|
+
options: { label: ruby_class.class_name }
|
22
|
+
)
|
16
23
|
elsif bare_methods?(ruby_code)
|
17
|
-
|
24
|
+
Result.new(
|
25
|
+
ruby_code: @ruby_code,
|
26
|
+
ast: ruby_code.ast,
|
27
|
+
graphs: wrap_bare_methods(ruby_code)
|
28
|
+
)
|
18
29
|
else
|
19
|
-
|
30
|
+
Result.new(
|
31
|
+
ruby_code: @ruby_code,
|
32
|
+
ast: ruby_code.ast,
|
33
|
+
graphs: [Graph.new(ast: ruby_code.ast)]
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Result
|
39
|
+
# @return [Array<VisualizeRuby::Graph>]
|
40
|
+
attr_reader :graphs
|
41
|
+
# @return [Hash{Symbol => Object}]
|
42
|
+
attr_reader :options
|
43
|
+
# @return [IO]
|
44
|
+
attr_reader :ruby_code
|
45
|
+
# @return [Parser:AST]
|
46
|
+
attr_reader :ast
|
47
|
+
|
48
|
+
def initialize(ruby_code:, graphs:, options: {}, ast:)
|
49
|
+
@ruby_code = ruby_code
|
50
|
+
@graphs = graphs
|
51
|
+
@options = options
|
52
|
+
@ast = ast
|
20
53
|
end
|
21
54
|
end
|
22
55
|
|
@@ -28,11 +61,22 @@ module VisualizeRuby
|
|
28
61
|
graphs.each do |graph|
|
29
62
|
graphs.each do |sub_graph|
|
30
63
|
sub_graph.nodes.each do |node|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
64
|
+
if node.name == graph.name
|
65
|
+
found = sub_graph.edges.select do |e|
|
66
|
+
e.node_a == node
|
67
|
+
end
|
68
|
+
found.first
|
69
|
+
|
70
|
+
graph_edge = Edge.new(
|
71
|
+
nodes: [node, graph.nodes.first],
|
72
|
+
style: :dashed, # indicate method call
|
73
|
+
)
|
74
|
+
sub_graph.edges.insert(sub_graph.edges.index(found.first) || -1, graph_edge)
|
75
|
+
found.each do |edge|
|
76
|
+
edge.options(style: :dashed) # indicate method call
|
77
|
+
edge.nodes[0] = graph.nodes.first
|
78
|
+
end
|
79
|
+
end
|
36
80
|
end
|
37
81
|
end
|
38
82
|
end
|
@@ -40,15 +84,25 @@ module VisualizeRuby
|
|
40
84
|
graphs
|
41
85
|
end
|
42
86
|
|
87
|
+
def edge_search(a: nil, b: nil, edges:)
|
88
|
+
edges.select do |e|
|
89
|
+
e.node_a == a || e.node_b == b
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
43
93
|
def build_graphs_by_method(ruby_class)
|
44
|
-
|
45
|
-
Graph.new(
|
94
|
+
ruby_class.defs.map do |meth|
|
95
|
+
Graph.new(
|
96
|
+
ruby_code: meth.body,
|
97
|
+
name: meth.name,
|
98
|
+
ast: meth.send(:ruby_code).ast.children[2] # method body ast
|
99
|
+
)
|
46
100
|
end
|
47
101
|
end
|
48
102
|
|
49
103
|
def bare_methods?(ruby_code)
|
50
104
|
ruby_code.ast.type == :def ||
|
51
|
-
|
105
|
+
ruby_code.ast.type == :begin && ruby_code.ast.children.map(&:type).uniq == [:def]
|
52
106
|
end
|
53
107
|
|
54
108
|
def wrap_bare_methods(ruby_code)
|
@@ -57,8 +111,8 @@ module VisualizeRuby
|
|
57
111
|
#{ruby_code.source}
|
58
112
|
end
|
59
113
|
Ruby
|
60
|
-
di_ruby_code
|
61
|
-
ruby_class
|
114
|
+
di_ruby_code = DissociatedIntrospection::RubyCode.build_from_source(wrapped_ruby_code)
|
115
|
+
ruby_class = DissociatedIntrospection::RubyClass.new(di_ruby_code)
|
62
116
|
build_from_class(ruby_class)
|
63
117
|
end
|
64
118
|
end
|
data/lib/visualize_ruby/edge.rb
CHANGED
@@ -1,19 +1,31 @@
|
|
1
1
|
module VisualizeRuby
|
2
2
|
class Edge
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
include Touchable
|
4
|
+
include Optionalable
|
5
|
+
attr_reader :nodes,
|
6
6
|
:dir,
|
7
|
-
:style
|
8
|
-
:color
|
7
|
+
:style
|
9
8
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
@
|
15
|
-
@
|
16
|
-
@
|
9
|
+
attr_accessor :color,
|
10
|
+
:display
|
11
|
+
|
12
|
+
def initialize(name: nil, nodes:, dir: :forward, type: :default, display: :visual, **opts)
|
13
|
+
@name = name.to_s if name
|
14
|
+
@nodes = nodes
|
15
|
+
@dir = dir
|
16
|
+
@style = style
|
17
|
+
@color = color
|
18
|
+
@type = type
|
19
|
+
@display = display
|
20
|
+
post_initialize(opts)
|
21
|
+
end
|
22
|
+
|
23
|
+
def node_a
|
24
|
+
nodes[0]
|
25
|
+
end
|
26
|
+
|
27
|
+
def node_b
|
28
|
+
nodes[1]
|
17
29
|
end
|
18
30
|
|
19
31
|
def to_a
|
@@ -38,6 +50,16 @@ module VisualizeRuby
|
|
38
50
|
"#<VisualizeRuby::Edge #{to_a.join(" ")}>"
|
39
51
|
end
|
40
52
|
|
53
|
+
def ==(other)
|
54
|
+
other.class == self.class && other.hash == self.hash
|
55
|
+
end
|
56
|
+
|
57
|
+
alias_method :eql?, :==
|
58
|
+
|
59
|
+
def hash
|
60
|
+
[dir, name, nodes.map(&:hash), style, color].hash
|
61
|
+
end
|
62
|
+
|
41
63
|
alias_method :to_s, :inspect
|
42
64
|
end
|
43
65
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require "tracer"
|
2
|
+
require "tempfile"
|
3
|
+
|
4
|
+
module VisualizeRuby
|
5
|
+
class ExecutionTracer
|
6
|
+
TRACE_POINT_OPTIONS = [
|
7
|
+
:line
|
8
|
+
]
|
9
|
+
attr_reader :executed_events
|
10
|
+
# @param [String, File] ruby_code
|
11
|
+
# @param [File, String] calling_code
|
12
|
+
# @param [Array<Symbol>] trace_point_options
|
13
|
+
def initialize(builder = nil, ruby_code: builder.ruby_code, calling_code:, trace_point_options: TRACE_POINT_OPTIONS)
|
14
|
+
@ruby_code = ruby_code
|
15
|
+
@calling_code = calling_code
|
16
|
+
@trace_point_options = trace_point_options
|
17
|
+
@temp_files = []
|
18
|
+
@executed_events = []
|
19
|
+
end
|
20
|
+
|
21
|
+
def trace
|
22
|
+
load(ruby_file.path)
|
23
|
+
tracer.enable { eval(calling_file.read) }
|
24
|
+
self
|
25
|
+
ensure
|
26
|
+
temp_files.each(&:close!)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :trace_point_options
|
32
|
+
|
33
|
+
def tracer
|
34
|
+
@tracer ||= TracePoint.new(*trace_point_options) do |tp|
|
35
|
+
if tp.path == ruby_file.path
|
36
|
+
executed_events << { line: tp.lineno, event: tp.event}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
attr_reader :temp_files
|
42
|
+
|
43
|
+
def temp_file(ruby_code)
|
44
|
+
@temp_files ||= []
|
45
|
+
file = Tempfile.new(%w(ruby_file .rb), File.expand_path(File.join(File.dirname(__FILE__), "../../tmp")))
|
46
|
+
file.write(ruby_code)
|
47
|
+
file.rewind
|
48
|
+
@temp_files << file
|
49
|
+
file
|
50
|
+
end
|
51
|
+
|
52
|
+
def ruby_file
|
53
|
+
@ruby_file ||= file!(@ruby_code)
|
54
|
+
end
|
55
|
+
|
56
|
+
def calling_file
|
57
|
+
@calling_file ||= file!(@calling_code)
|
58
|
+
end
|
59
|
+
|
60
|
+
def file!(code)
|
61
|
+
code.is_a?(String) ? temp_file(code) : code
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/lib/visualize_ruby/graph.rb
CHANGED
@@ -1,10 +1,17 @@
|
|
1
1
|
module VisualizeRuby
|
2
2
|
class Graph
|
3
|
-
|
3
|
+
attr_accessor :name, :nodes, :edges
|
4
4
|
|
5
|
-
def initialize(ruby_code
|
6
|
-
@name
|
7
|
-
@nodes, @edges
|
5
|
+
def initialize(ruby_code: nil, name: nil, ast: nil, **opts)
|
6
|
+
@name = name.to_s if name
|
7
|
+
@nodes, @edges = (ast ? Parser.new(ast: ast) : Parser.new(ruby_code)).parse
|
8
|
+
@ast = ast
|
9
|
+
@graph_viz_options = opts
|
10
|
+
end
|
11
|
+
|
12
|
+
def options(**args)
|
13
|
+
@graph_viz_options.merge!(args)
|
14
|
+
@graph_viz_options
|
8
15
|
end
|
9
16
|
|
10
17
|
def to_hash
|
@@ -14,5 +21,11 @@ module VisualizeRuby
|
|
14
21
|
nodes: nodes.map(&:to_a),
|
15
22
|
}
|
16
23
|
end
|
24
|
+
|
25
|
+
def uniq_elements!
|
26
|
+
@edges = edges.uniq
|
27
|
+
@nodes = nodes.uniq
|
28
|
+
self
|
29
|
+
end
|
17
30
|
end
|
18
31
|
end
|
@@ -2,11 +2,12 @@ require "graphviz"
|
|
2
2
|
|
3
3
|
module VisualizeRuby
|
4
4
|
class Graphviz
|
5
|
-
attr_reader :graphs, :label
|
5
|
+
attr_reader :graphs, :label, :unique_nodes
|
6
6
|
|
7
|
-
def initialize(graphs, label: nil)
|
8
|
-
@graphs
|
9
|
-
@label
|
7
|
+
def initialize(builder_result = nil, graphs: nil, label: nil, unique_nodes: true)
|
8
|
+
@graphs = graphs || builder_result.graphs
|
9
|
+
@label = builder_result ? builder_result.options[:label] : label
|
10
|
+
@unique_nodes = unique_nodes
|
10
11
|
end
|
11
12
|
|
12
13
|
def to_graph(format: nil, path: nil)
|
@@ -46,14 +47,23 @@ module VisualizeRuby
|
|
46
47
|
@nodes ||= {}
|
47
48
|
end
|
48
49
|
|
50
|
+
def node_id(node)
|
51
|
+
if unique_nodes
|
52
|
+
node.id
|
53
|
+
else
|
54
|
+
node.name
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
49
58
|
def create_edges(sub_graphs)
|
50
59
|
sub_graphs.each do |r_graph, g_graph|
|
51
60
|
r_graph.edges.each do |edge|
|
61
|
+
next unless edge.display == :visual
|
52
62
|
::Graphviz::Edge.new(
|
53
63
|
g_graph,
|
54
|
-
nodes[edge.node_a
|
55
|
-
nodes[edge.node_b
|
56
|
-
**compact({ label: edge.name, dir: edge.dir, style: edge.style,
|
64
|
+
nodes[node_id(edge.node_a)],
|
65
|
+
nodes[node_id(edge.node_b)],
|
66
|
+
**compact({ label: edge.name, dir: edge.dir, style: edge.style, **edge.options})
|
57
67
|
)
|
58
68
|
end
|
59
69
|
end
|
@@ -62,16 +72,20 @@ module VisualizeRuby
|
|
62
72
|
def create_sub_graph(graph, index)
|
63
73
|
main_graph.add_subgraph(
|
64
74
|
"cluster_#{index}",
|
65
|
-
**compact({ label: graph.name, style: graphs.count == 1 ? :invis : :dotted })
|
75
|
+
**compact({ label: graph.name, style: graphs.count == 1 ? :invis : :dotted, **graph.options })
|
66
76
|
)
|
67
77
|
end
|
68
78
|
|
69
79
|
def create_nodes(graph, sub_graph)
|
70
80
|
graph.nodes.each do |node|
|
71
|
-
nodes[node
|
72
|
-
node
|
73
|
-
|
74
|
-
|
81
|
+
nodes[node_id(node)] = sub_graph.add_node(
|
82
|
+
node_id(node),
|
83
|
+
compact({
|
84
|
+
shape: node.shape,
|
85
|
+
style: node.style,
|
86
|
+
label: node.name,
|
87
|
+
**node.options
|
88
|
+
})
|
75
89
|
)
|
76
90
|
end
|
77
91
|
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module VisualizeRuby
|
2
|
+
class HighlightTracer
|
3
|
+
OPTIONS = {
|
4
|
+
color: :forestgreen
|
5
|
+
}
|
6
|
+
|
7
|
+
# @param [VisualizeRuby::Builder::Result] builder
|
8
|
+
# @param [Hash{line: Integer, event: Symbol}] executed_events
|
9
|
+
# @param [Symbol] color
|
10
|
+
def initialize(builder:, executed_events: [], color: OPTIONS.fetch(:color))
|
11
|
+
@builder = builder
|
12
|
+
@executed_events = executed_events
|
13
|
+
@color = color
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [VisualizeRuby::Builder::Result]
|
17
|
+
def highlight!
|
18
|
+
mark!
|
19
|
+
builder
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_reader :builder, :color, :executed_events
|
25
|
+
|
26
|
+
def mark!
|
27
|
+
last_touch = nil
|
28
|
+
(paired_line_events).to_a.each do |a, b|
|
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)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def check_lineno_connections(e)
|
41
|
+
e.nodes.each do |n|
|
42
|
+
if n.lineno_connection
|
43
|
+
n.lineno_connection.touch(color)
|
44
|
+
touch_nodes(n.lineno_connection, except: e.nodes)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def touch_nodes(edge, except: [])
|
50
|
+
edge.nodes.each { |n| n.touch(color) unless except.include?(n) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def all_edges
|
54
|
+
@all_edges ||= builder.graphs.flat_map(&:edges)
|
55
|
+
end
|
56
|
+
|
57
|
+
def paired_line_events
|
58
|
+
line_events = executed_events.select { |e| e[:event] == :line } + [executed_events.last]
|
59
|
+
setup_paring(line_events) do |a, b|
|
60
|
+
[line_events[a][:line], line_events[b][:line]]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def setup_paring(array, offset = -2)
|
65
|
+
(0..(array.length + offset)).to_a.map do |l|
|
66
|
+
yield(l, l + 1)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def executed_lines
|
71
|
+
@executed_lines ||= executed_events.map { |event| event[:line] }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/visualize_ruby/node.rb
CHANGED
@@ -1,16 +1,17 @@
|
|
1
1
|
module VisualizeRuby
|
2
2
|
class Node
|
3
|
-
|
4
|
-
|
3
|
+
include Touchable
|
4
|
+
include Optionalable
|
5
|
+
attr_reader :style, :line
|
6
|
+
attr_accessor :type, :id, :lineno_connection
|
5
7
|
|
6
|
-
def initialize(name
|
7
|
-
@name = name.
|
8
|
+
def initialize(name: nil, type: :action, style: :rounded, ast: nil, line: nil, id: nil, **opts)
|
9
|
+
@name = name || (ast ? AstHelper.new(ast).description : nil)
|
8
10
|
@type = type
|
9
11
|
@style = style
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
name.to_s.gsub(" ", "_").to_sym
|
12
|
+
@id = id || (ast ? AstHelper.new(ast).id : nil)
|
13
|
+
@line = line || AstHelper.new(ast).first_line
|
14
|
+
post_initialize(opts)
|
14
15
|
end
|
15
16
|
|
16
17
|
def to_a
|
@@ -36,11 +37,23 @@ module VisualizeRuby
|
|
36
37
|
:ellipse
|
37
38
|
when :argument
|
38
39
|
:box
|
40
|
+
else
|
41
|
+
:box
|
39
42
|
end
|
40
43
|
end
|
41
44
|
|
42
45
|
def inspect
|
43
|
-
"#<VisualizeRuby::Node #{type_display} #{
|
46
|
+
"#<VisualizeRuby::Node #{type_display} #{id}>"
|
47
|
+
end
|
48
|
+
|
49
|
+
def ==(other)
|
50
|
+
other.class == self.class && other.hash == self.hash
|
51
|
+
end
|
52
|
+
|
53
|
+
alias_method :eql?, :==
|
54
|
+
|
55
|
+
def hash
|
56
|
+
[type, name, style, id].hash
|
44
57
|
end
|
45
58
|
|
46
59
|
alias_method :to_s, :inspect
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module VisualizeRuby
|
2
|
+
module Optionalable
|
3
|
+
def post_initialize(**opts)
|
4
|
+
@graph_viz_options = opts
|
5
|
+
super if defined? super
|
6
|
+
end
|
7
|
+
|
8
|
+
def options(args={})
|
9
|
+
@graph_viz_options.merge!(args)
|
10
|
+
@graph_viz_options
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :graph_viz_options
|
14
|
+
end
|
15
|
+
end
|
@@ -2,18 +2,6 @@ module VisualizeRuby
|
|
2
2
|
class Parser
|
3
3
|
class And < Base
|
4
4
|
include Conditions
|
5
|
-
# @return [Array<VisualizeRuby::Node>, Array<VisualizeRuby::Edge>]
|
6
|
-
def parse
|
7
|
-
last_node = nil
|
8
|
-
edges = []
|
9
|
-
nodes = @ast.children.reverse.map do |c|
|
10
|
-
node = set_conditions(c).first
|
11
|
-
edges << Edge.new(name: "AND", nodes: [node, last_node]) if last_node
|
12
|
-
last_node = node
|
13
|
-
node
|
14
|
-
end.reverse
|
15
|
-
return nodes, edges
|
16
|
-
end
|
17
5
|
end
|
18
6
|
end
|
19
7
|
end
|
@@ -4,16 +4,36 @@ module VisualizeRuby
|
|
4
4
|
# @return [Array<VisualizeRuby::Node>, Array<VisualizeRuby::Edge>]
|
5
5
|
def parse
|
6
6
|
last_node = nil
|
7
|
-
@ast.children.to_a.compact.reverse.each do |a|
|
7
|
+
@ast.children.to_a.compact.reverse.each do |a| # builds tree from bottom up
|
8
8
|
_nodes, _edges = Parser.new(ast: a).parse
|
9
9
|
edges.concat(_edges.reverse)
|
10
10
|
nodes.concat(_nodes.reverse)
|
11
|
-
|
11
|
+
connect_nodes(_edges, _nodes, last_node) if last_node
|
12
12
|
last_node = _nodes.first
|
13
13
|
end
|
14
|
-
|
15
14
|
return nodes.reverse, edges.reverse
|
16
15
|
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def connect_nodes(_edges, _nodes, last_node)
|
20
|
+
no_top_edge_nodes(_edges, _nodes).each do |n|
|
21
|
+
if n.type == :branch_leaf # remove inserted branch leaf, added from Parser::If, and connect to last_node
|
22
|
+
# (node-bool->branch_leaf) REPLACE WITH (node-bool->last_node)
|
23
|
+
edge = _edges.detect { |e| e.node_b == n }
|
24
|
+
edge.nodes[1] = last_node
|
25
|
+
nodes.delete(n)
|
26
|
+
else # 1. (-> n) 2. (-> n -> last_node)
|
27
|
+
edges << Edge.new(nodes: [n, last_node])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def no_top_edge_nodes(_edges, _nodes)
|
33
|
+
_nodes.select do |n| # ONLY (-> n) NOT (n ->)
|
34
|
+
_edges.none? { |e| e.node_a == n }
|
35
|
+
end
|
36
|
+
end
|
17
37
|
end
|
18
38
|
end
|
19
39
|
end
|
@@ -7,32 +7,29 @@ module VisualizeRuby
|
|
7
7
|
item = arguments.children[0]
|
8
8
|
collection, iterator_type = iterator.to_a
|
9
9
|
if enumerable?(collection) || enumerable?(iterator_type)
|
10
|
-
|
10
|
+
yield_block(action, item, iterator, "blue", true)
|
11
11
|
else
|
12
|
-
yield_block(action, item, iterator)
|
12
|
+
yield_block(action, item, iterator, "orange", false)
|
13
13
|
end
|
14
14
|
return nodes, edges
|
15
15
|
end
|
16
16
|
|
17
17
|
private
|
18
18
|
|
19
|
-
def yield_block(action, item, on_object)
|
20
|
-
nodes << on_object_node = Node.new(
|
21
|
-
nodes << item_node = Node.new(
|
22
|
-
nodes << action_node = Node.new(
|
23
|
-
|
24
|
-
|
25
|
-
|
19
|
+
def yield_block(action, item, on_object, color, enumerable)
|
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
|
+
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
|
26
31
|
|
27
|
-
|
28
|
-
nodes << collection_node = Node.new(name: AstHelper.new(collection).description)
|
29
|
-
nodes << item_node = Node.new(name: AstHelper.new(item).description, type: :argument)
|
30
|
-
nodes << iterator_node = Node.new(name: iterator_type)
|
31
|
-
nodes << action_node = Node.new(name: AstHelper.new(action).description)
|
32
|
-
edges << Edge.new(nodes: [collection_node, iterator_node])
|
33
|
-
edges << Edge.new(nodes: [iterator_node, item_node], color: "blue")
|
34
|
-
edges << Edge.new(nodes: [item_node, action_node], color: "blue")
|
35
|
-
edges << Edge.new(nodes: [action_node, iterator_node], color: "blue", name: "↺")
|
32
|
+
edges << Edge.new(nodes: [action_node, on_object_node], color: color, name: "↺") if enumerable
|
36
33
|
end
|
37
34
|
|
38
35
|
def enumerable?(meth)
|
@@ -4,7 +4,7 @@ module VisualizeRuby
|
|
4
4
|
# @return [Array<VisualizeRuby::Node>, Array<VisualizeRuby::Edge>]
|
5
5
|
def parse
|
6
6
|
condition, *_whens, _else = @ast.children
|
7
|
-
condition_node = Node.new(
|
7
|
+
condition_node = Node.new(ast: condition, type: :decision)
|
8
8
|
nodes << condition_node
|
9
9
|
_whens.each do |_when|
|
10
10
|
edge_name, actions = _when.children
|
@@ -13,7 +13,7 @@ module VisualizeRuby
|
|
13
13
|
nodes.concat(action_nodes)
|
14
14
|
edges.concat(action_edges)
|
15
15
|
end
|
16
|
-
_else_node = Node.new(
|
16
|
+
_else_node = Node.new(ast: _else, type: :action)
|
17
17
|
_else_edge = Edge.new(name: "else", nodes: [condition_node, _else_node])
|
18
18
|
nodes << _else_node
|
19
19
|
edges << _else_edge
|
@@ -8,6 +8,22 @@ module VisualizeRuby
|
|
8
8
|
edges.concat(condition_edges)
|
9
9
|
condition_nodes
|
10
10
|
end
|
11
|
+
|
12
|
+
# @return [Array<VisualizeRuby::Node>, Array<VisualizeRuby::Edge>]
|
13
|
+
def parse
|
14
|
+
last_node = nil
|
15
|
+
edges = []
|
16
|
+
nodes = @ast.children.reverse.map do |c|
|
17
|
+
node = set_conditions(c).first
|
18
|
+
if last_node
|
19
|
+
edges << Edge.new(name: self.class.name.split("::").last.upcase, nodes: [node, last_node])
|
20
|
+
last_node.lineno_connection = edges.last
|
21
|
+
end
|
22
|
+
last_node = node
|
23
|
+
node
|
24
|
+
end.reverse
|
25
|
+
return nodes, edges
|
26
|
+
end
|
11
27
|
end
|
12
28
|
end
|
13
29
|
end
|
@@ -6,28 +6,37 @@ module VisualizeRuby
|
|
6
6
|
def parse
|
7
7
|
break_ast
|
8
8
|
|
9
|
-
condition_nodes
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
edges.
|
9
|
+
condition_nodes = set_conditions(condition)
|
10
|
+
on_true_nodes, on_true_edges = branch(on_true)
|
11
|
+
on_false_nodes, on_false_edges = branch(on_false)
|
12
|
+
last_condition = condition_nodes.last
|
13
|
+
on_true_nodes[0] = on_true_node = (on_true_nodes.first || branch_leaf(last_condition, "true"))
|
14
|
+
on_false_nodes[0] = on_false_node = (on_false_nodes.first || branch_leaf(last_condition, "false"))
|
15
|
+
nodes.concat(on_true_nodes)
|
16
|
+
nodes.concat(on_false_nodes)
|
17
|
+
edges << Edge.new(name: "true", nodes: [last_condition, on_true_node])
|
18
|
+
edges << Edge.new(name: "false", nodes: [last_condition, on_false_node])
|
19
|
+
edges.concat(on_false_edges)
|
18
20
|
edges.concat(on_true_edges)
|
19
21
|
return [nodes, edges]
|
20
22
|
end
|
21
23
|
|
22
24
|
def branch(on_bool)
|
25
|
+
return [], [] unless on_bool
|
23
26
|
on_bool_nodes, on_bool_edges = Parser.new(ast: on_bool).parse
|
24
|
-
|
25
|
-
nodes.concat(on_bool_nodes)
|
26
|
-
return on_bool_node, on_bool_edges
|
27
|
+
return on_bool_nodes, on_bool_edges
|
27
28
|
end
|
28
29
|
|
29
30
|
private
|
30
31
|
|
32
|
+
def branch_leaf(last_condition, type)
|
33
|
+
Node.new(
|
34
|
+
name: "END",
|
35
|
+
type: :branch_leaf,
|
36
|
+
id: "end-#{type}-'#{last_condition.id}'"
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
31
40
|
attr_reader :condition, :on_true, :on_false
|
32
41
|
|
33
42
|
def break_ast
|
@@ -2,16 +2,6 @@ module VisualizeRuby
|
|
2
2
|
class Parser
|
3
3
|
class Or < Base
|
4
4
|
include Conditions
|
5
|
-
# @return [Array<VisualizeRuby::Node>, Array<VisualizeRuby::Edge>]
|
6
|
-
def parse
|
7
|
-
last_node = nil
|
8
|
-
@ast.children.reverse.map do |c|
|
9
|
-
node = set_conditions(c).first
|
10
|
-
edges << Edge.new(name: "OR", nodes: [node, last_node]) if last_node
|
11
|
-
last_node = node
|
12
|
-
end
|
13
|
-
return nodes.reverse, edges
|
14
|
-
end
|
15
5
|
end
|
16
6
|
end
|
17
7
|
end
|
@@ -3,7 +3,6 @@ require_relative "parser/conditions"
|
|
3
3
|
require_relative "parser/base"
|
4
4
|
require_relative "parser/or"
|
5
5
|
require_relative "parser/and"
|
6
|
-
require_relative "parser/ast_helper"
|
7
6
|
require_relative "parser/begin"
|
8
7
|
require_relative "parser/send"
|
9
8
|
require_relative "parser/str"
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module VisualizeRuby
|
2
|
+
class Runner
|
3
|
+
# @return [String, IO]
|
4
|
+
attr_accessor :calling_code
|
5
|
+
# @return [String, IO]
|
6
|
+
attr_accessor :ruby_code
|
7
|
+
# @return [Symbol, NilClass]
|
8
|
+
attr_accessor :output_format
|
9
|
+
# @return [String]
|
10
|
+
attr_accessor :output_path
|
11
|
+
# @param [String, IO]
|
12
|
+
attr_reader :calling_code
|
13
|
+
|
14
|
+
# @param [String, IO]
|
15
|
+
def trace(calling_code)
|
16
|
+
@calling_code = calling_code
|
17
|
+
end
|
18
|
+
|
19
|
+
def run!
|
20
|
+
highlight_trace
|
21
|
+
VisualizeRuby::Graphviz.new(builder).to_graph({ path: output_path, format: output_format }.compact)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def builder
|
27
|
+
@builder ||= VisualizeRuby::Builder.new(ruby_code: ruby_code).build
|
28
|
+
end
|
29
|
+
|
30
|
+
def highlight_trace
|
31
|
+
return unless calling_code
|
32
|
+
executed_events = VisualizeRuby::ExecutionTracer.new(builder, calling_code: calling_code).trace.executed_events
|
33
|
+
VisualizeRuby::HighlightTracer.new(builder: builder, executed_events: executed_events).highlight!
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module VisualizeRuby
|
2
|
+
module Touchable
|
3
|
+
def post_initialize(**args)
|
4
|
+
@touched = 0
|
5
|
+
super if defined? super
|
6
|
+
end
|
7
|
+
|
8
|
+
def touch(color)
|
9
|
+
options.merge!(color: color)
|
10
|
+
@touched += 1
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
if [0,1].include?(@touched)
|
15
|
+
@name
|
16
|
+
else
|
17
|
+
"#{@name} (#{@touched})"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :touched
|
22
|
+
end
|
23
|
+
end
|
data/lib/visualize_ruby.rb
CHANGED
@@ -1,10 +1,25 @@
|
|
1
1
|
require "visualize_ruby/version"
|
2
2
|
require "visualize_ruby/parser"
|
3
|
+
require "visualize_ruby/optionalable"
|
4
|
+
require "visualize_ruby/touchable"
|
3
5
|
require "visualize_ruby/node"
|
4
6
|
require "visualize_ruby/edge"
|
5
7
|
require "visualize_ruby/graph"
|
6
8
|
require "visualize_ruby/builder"
|
7
9
|
require "visualize_ruby/graphviz"
|
10
|
+
require "visualize_ruby/ast_helper"
|
11
|
+
require "visualize_ruby/runner"
|
12
|
+
require "visualize_ruby/execution_tracer"
|
13
|
+
require "visualize_ruby/highlight_tracer"
|
8
14
|
|
9
15
|
module VisualizeRuby
|
16
|
+
def self.new
|
17
|
+
runner = Runner.new
|
18
|
+
if block_given?
|
19
|
+
yield(runner)
|
20
|
+
runner.run!
|
21
|
+
else
|
22
|
+
runner
|
23
|
+
end
|
24
|
+
end
|
10
25
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: visualize_ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.11.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dustin Zeisler
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-07-
|
11
|
+
date: 2018-07-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -127,14 +127,17 @@ files:
|
|
127
127
|
- bin/setup
|
128
128
|
- exe/visualize_ruby
|
129
129
|
- lib/visualize_ruby.rb
|
130
|
+
- lib/visualize_ruby/ast_helper.rb
|
130
131
|
- lib/visualize_ruby/builder.rb
|
131
132
|
- lib/visualize_ruby/edge.rb
|
133
|
+
- lib/visualize_ruby/execution_tracer.rb
|
132
134
|
- lib/visualize_ruby/graph.rb
|
133
135
|
- lib/visualize_ruby/graphviz.rb
|
136
|
+
- lib/visualize_ruby/highlight_tracer.rb
|
134
137
|
- lib/visualize_ruby/node.rb
|
138
|
+
- lib/visualize_ruby/optionalable.rb
|
135
139
|
- lib/visualize_ruby/parser.rb
|
136
140
|
- lib/visualize_ruby/parser/and.rb
|
137
|
-
- lib/visualize_ruby/parser/ast_helper.rb
|
138
141
|
- lib/visualize_ruby/parser/base.rb
|
139
142
|
- lib/visualize_ruby/parser/begin.rb
|
140
143
|
- lib/visualize_ruby/parser/block.rb
|
@@ -147,6 +150,8 @@ files:
|
|
147
150
|
- lib/visualize_ruby/parser/str.rb
|
148
151
|
- lib/visualize_ruby/parser/true.rb
|
149
152
|
- lib/visualize_ruby/parser/type.rb
|
153
|
+
- lib/visualize_ruby/runner.rb
|
154
|
+
- lib/visualize_ruby/touchable.rb
|
150
155
|
- lib/visualize_ruby/version.rb
|
151
156
|
- visualize_ruby.gemspec
|
152
157
|
homepage: https://github.com/zeisler/visualize_ruby
|