Class: ProductionBreakpoints::Parser

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby-production-breakpoints/parser.rb

Overview

FIXME: this class is a mess, figure out interface and properly separate private / public

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source_file) ⇒ Parser

Returns a new instance of Parser.



8
9
10
11
12
# File 'lib/ruby-production-breakpoints/parser.rb', line 8

def initialize(source_file)
  @root_node = RubyVM::AbstractSyntaxTree.parse_file(source_file)
  @source_lines = File.read(source_file).lines
  @logger = ProductionBreakpoints.config.logger
end

Instance Attribute Details

#root_nodeObject (readonly)

Returns the value of attribute root_node.



6
7
8
# File 'lib/ruby-production-breakpoints/parser.rb', line 6

def root_node
  @root_node
end

#source_linesObject (readonly)

Returns the value of attribute source_lines.



6
7
8
# File 'lib/ruby-production-breakpoints/parser.rb', line 6

def source_lines
  @source_lines
end

Instance Method Details

#find_definition_namespace(target) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/ruby-production-breakpoints/parser.rb', line 32

def find_definition_namespace(target)
  lineage = find_lineage(target)

  namespaces = []
  lineage.each do |n|
    next unless n.type == :MODULE || n.type == :CLASS

    symbols = n.children.select { |c| c.is_a?(RubyVM::AbstractSyntaxTree::Node) && c.type == :COLON2 }
    if symbols.size != 1
      @logger.error("Couldn't determine symbol location for parent namespace")
    end
    symbol = symbols.first

    symstr = @source_lines[symbol.first_lineno - 1][symbol.first_column..symbol.last_column].strip
    namespaces << symstr
  end

  namespaces.join('::')
end

#find_definition_node(start_line, end_line) ⇒ Object



59
60
61
# File 'lib/ruby-production-breakpoints/parser.rb', line 59

def find_definition_node(start_line, end_line)
  _find_definition_node(@root_node, start_line, end_line)
end

#find_definition_symbol(start_line, end_line) ⇒ Object



52
53
54
55
56
57
# File 'lib/ruby-production-breakpoints/parser.rb', line 52

def find_definition_symbol(start_line, end_line)
  def_node = _find_definition_node(@root_node, start_line, end_line)
  def_column_start = def_node.first_column
  def_column_end = _find_args_start(def_node).first_column
  @source_lines[def_node.first_lineno - 1][(def_column_start + 3 + 1)..def_column_end].strip.to_sym
end

#find_lineage(target) ⇒ Object



26
27
28
29
30
# File 'lib/ruby-production-breakpoints/parser.rb', line 26

def find_lineage(target)
  lineage = _find_lineage(@root_node, target)
  lineage.pop # FIXME: verify leafy node is equal to target or throw an error?
  lineage
end

#find_node(node, type, first, last, depth: 0) ⇒ Object

FIXME: set a max depth here to pretent unbounded recursion? probably should



15
16
17
18
19
20
21
22
23
24
# File 'lib/ruby-production-breakpoints/parser.rb', line 15

def find_node(node, type, first, last, depth: 0)
  child_nodes = node.children.select { |c| c.is_a?(RubyVM::AbstractSyntaxTree::Node) }
  # @logger.debug("D: #{depth} #{node.type} has #{child_nodes.size} children and spans #{node.first_lineno}:#{node.first_column} to #{node.last_lineno}:#{node.last_column}")

  if node.type == type && first >= node.first_lineno && last <= node.last_lineno
    return node
  end

  child_nodes.map { |n| find_node(n, type, first, last, depth: depth + 1) }.flatten
end

#inject_metaprogramming_handlers(handler, def_start, def_end) ⇒ Object

This method is a litle weird and pretty deep into metaprogramming, so i’ll try to explain it

Given the source method some_method, and a range of lines to apply the breakpoint to, we will inject calls two breakpoint methods. We will pass these calls the string representation of the original source code. If the string of original source is part of the “handle” block, it will run withing the binding of the method up to that point, and allow for us to run our custom handler method to apply our debugging automation.

Any remaining code in the method also needs to be eval’d, as we want it to be recognized in the original binding, and the same binding as we’ve used for evaluating our handler. This allows us to keep local variables persisted “between blocks”, as we want our breakpoint code to have no impact to the original bindings and source code.

A generated breakpoint is shown below, the resulting string. is what will be evaluated on the method that we will prepend to the original parent in order to initiate our override.

def some_method

a = 1
sleep 0.5
b = a + 1
ProductionBreakpoints.installed_breakpoints[:test_breakpoint_install].handle(Kernel.binding)

end



84
85
86
87
88
89
# File 'lib/ruby-production-breakpoints/parser.rb', line 84

def inject_metaprogramming_handlers(handler, def_start, def_end)
  source = @source_lines.dup

  source.insert(def_end - 1, "#{handler}\n") # FIXME: columns? and indenting?
  source[(def_start - 1)..(def_end)].join
end

#ruby_source(start_line, end_line) ⇒ Object



91
92
93
# File 'lib/ruby-production-breakpoints/parser.rb', line 91

def ruby_source(start_line, end_line)
  @source_lines[(start_line - 1)..(end_line - 1)].join
end