visualize_ruby 0.8.0 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![graph](./spec/examples/base_method.png)](./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
|
+
[![graph](./spec/examples/runner_trace.png)](./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
|
+
[![graph](./spec/examples/highlight_tracer_loop.png)](./spec/examples/highlight_tracer_loop.png)
|
122
|
+
|
123
|
+
### Complex unrefactored code example
|
124
|
+
[Gilded Rose](https://github.com/amckinnell/Gilded-Rose-Ruby)
|
125
|
+
|
126
|
+
[![graph](./spec/examples/gilded_rose.png)](./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
|