visualize_ruby 0.11.0 → 0.16.0

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