visualize_ruby 0.11.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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