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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 065c1db111e1a4716755a68ff69bce287b3192d4939f86cd2913d67ff8aaa0ad
4
- data.tar.gz: 3440f90a823b7fe3c4a1b236f39948a237ecc645ca0e86fcf7cf5b82ea78d864
3
+ metadata.gz: d233d2d1af5c3e092d04d497fddfc4c7165d56dbd021662cd58703d04ada8ce4
4
+ data.tar.gz: f60ac523929e8ae331ef4af3811ed600a4aa380ca9738d3bcdb72a6377f60097
5
5
  SHA512:
6
- metadata.gz: 8179f8b332606875a3fde94e873abee27aa5eb3edb68a3d210a9eef522ba6431013d83be06d77061a31f58eca6d032404d67529b8537c96e9898f8cff6d8d0c3
7
- data.tar.gz: be6299a6bbcf6f6e61e552f050a78ee8710190b4eb4afcbdf0a329bea2ef320cb33aa67ce4b27ed6db0c7532c68f724ae50346f05e1a3ed65da27ecb4754070a
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 class and see method interactions. Works with procedural code and bare methods.</span>
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 = <<-RUBY
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(*results).to_graph(path: "example.png")
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
- [build_from_class(ruby_class), { label: ruby_class.class_name }]
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
- wrap_bare_methods(ruby_code)
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
- Graph.new(ruby_code: @ruby_code)
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
- sub_graph.edges << Edge.new(
32
- nodes: [node, graph.nodes.first],
33
- dir: :none,
34
- style: :dashed
35
- ) if node.name == graph.name
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
- ruby_class.defs.map do |meth|
45
- Graph.new(ruby_code: meth.body, name: meth.name)
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
- ruby_code.ast.type == :begin && ruby_code.ast.children.map(&:type).uniq == [:def]
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 = DissociatedIntrospection::RubyCode.build_from_source(wrapped_ruby_code)
61
- ruby_class = DissociatedIntrospection::RubyClass.new(di_ruby_code)
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
@@ -1,19 +1,31 @@
1
1
  module VisualizeRuby
2
2
  class Edge
3
- attr_reader :name,
4
- :node_a,
5
- :node_b,
3
+ include Touchable
4
+ include Optionalable
5
+ attr_reader :nodes,
6
6
  :dir,
7
- :style,
8
- :color
7
+ :style
9
8
 
10
- def initialize(name: nil, nodes:, dir: :forward, style: :solid, color: nil)
11
- @name = name.to_s if name
12
- @node_a = nodes[0]
13
- @node_b = nodes[1]
14
- @dir = dir
15
- @style = style
16
- @color = color
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
@@ -1,10 +1,17 @@
1
1
  module VisualizeRuby
2
2
  class Graph
3
- attr_reader :name, :nodes, :edges
3
+ attr_accessor :name, :nodes, :edges
4
4
 
5
- def initialize(ruby_code:, name: nil)
6
- @name = name.to_s if name
7
- @nodes, @edges = Parser.new(ruby_code).parse
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 = [*graphs]
9
- @label = 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.name],
55
- nodes[edge.node_b.name],
56
- **compact({ label: edge.name, dir: edge.dir, style: edge.style, color: edge.color })
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.name] = sub_graph.add_node(
72
- node.name,
73
- shape: node.shape,
74
- style: node.style
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
@@ -1,16 +1,17 @@
1
1
  module VisualizeRuby
2
2
  class Node
3
- attr_reader :name, :style
4
- attr_accessor :type
3
+ include Touchable
4
+ include Optionalable
5
+ attr_reader :style, :line
6
+ attr_accessor :type, :id, :lineno_connection
5
7
 
6
- def initialize(name:, type: :action, style: :rounded)
7
- @name = name.to_s
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
- end
11
-
12
- def to_sym
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} #{name}>"
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
- edges << Edge.new(nodes: [_nodes.first, last_node]) if last_node
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
- enumerable(action, collection, iterator_type, item)
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(name: AstHelper.new(on_object).description)
21
- nodes << item_node = Node.new(name: AstHelper.new(item).description, type: :argument)
22
- nodes << action_node = Node.new(name: AstHelper.new(action).description)
23
- edges << Edge.new(nodes: [on_object_node, item_node])
24
- edges << Edge.new(nodes: [item_node, action_node], color: "orange")
25
- end
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
- def enumerable(action, collection, iterator_type, item)
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(name: AstHelper.new(condition).description, type: :decision)
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(name: AstHelper.new(_else).description, type: :action)
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 = set_conditions(condition)
10
- on_true_node, on_true_edges = branch(on_true)
11
- on_false_node, on_false_edges = branch(on_false) if on_false
12
-
13
- condition_nodes.each do |condition_node|
14
- edges << Edge.new(name: "true", nodes: [condition_node, on_true_node])
15
- edges << Edge.new(name: "false", nodes: [condition_node, on_false_node]) if on_false
16
- end
17
- edges.concat(on_false_edges) if on_false
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
- on_bool_node = on_bool_nodes.first
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,7 @@ module VisualizeRuby
3
3
  class Send < Base
4
4
  # @return [Array<VisualizeRuby::Node>, Array<VisualizeRuby::Edge>]
5
5
  def parse
6
- return [Node.new(name: AstHelper.new(@ast).description, type: :action)], []
6
+ return [Node.new(ast: @ast, type: :action)], []
7
7
  end
8
8
  end
9
9
  end
@@ -3,7 +3,7 @@ module VisualizeRuby
3
3
  class Str < Base
4
4
  # @return [Array<VisualizeRuby::Node>, Array<VisualizeRuby::Edge>]
5
5
  def parse
6
- return [Node.new(name: AstHelper.new(@ast).description, type: :action)], []
6
+ return [Node.new(ast: @ast, type: :action)], []
7
7
  end
8
8
  end
9
9
  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
@@ -1,3 +1,3 @@
1
1
  module VisualizeRuby
2
- VERSION = "0.8.0"
2
+ VERSION = "0.11.0"
3
3
  end
@@ -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.8.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-17 00:00:00.000000000 Z
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
@@ -1,14 +0,0 @@
1
- module VisualizeRuby
2
- class Parser
3
- class AstHelper
4
- def initialize(ast)
5
- @ast = ast
6
- end
7
-
8
- def description(ast: @ast)
9
- return ast unless ast.respond_to?(:type)
10
- Unparser.unparse(ast)
11
- end
12
- end
13
- end
14
- end