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 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