Module: SimpleCov::SourceFile::RubyDataParser

Defined in:
lib/simplecov/source_file/ruby_data_parser.rb

Overview

‘Coverage.result` reports condition and method keys as Ruby arrays. When the resultset is round-tripped through JSON those array keys become their stringified inspect form, so this parser walks the literal back into a real Array without using `eval` (see #801). The grammar covers symbols, strings, integers, unary minus, and constant paths — every shape Coverage ever emits.

Class Method Summary collapse

Class Method Details

.call(structure) ⇒ Object

Tests use the real data structures (except for integration tests) so no need to put them through here.



18
19
20
21
22
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 18

def call(structure)
  return structure if structure.is_a?(Array)

  parse_array_string(structure.to_s)
end

.parse_array_string(str) ⇒ Object

Parse a string like ‘[:if, 0, 3, 4, 3, 21]’ or ‘[“ClassName”, :method1, 2, 2, 5, 5]’ back into a Ruby array.

Raises:

  • (ArgumentError)


26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 26

def parse_array_string(str)
  # Try plain Ripper first; only pre-quote `#<...>` inspect segments
  # if the input isn't already valid Ruby (otherwise we corrupt
  # `"#<Class:Foo>"` strings that *are* valid Ruby literals — exactly
  # the shape simplecov-on-simplecov method-coverage keys take).
  sexp = Ripper.sexp(str) || Ripper.sexp(quote_inspected_class_segments(str))
  # simplecov:disable — defensive: Ripper.sexp returning nil from both passes requires malformed input
  array_node = sexp&.dig(1, 0)
  # simplecov:enable
  raise ArgumentError, "expected array literal: #{str.inspect}" unless array_node && array_node[0] == :array

  Array(array_node[1]).map { |element| parse_element(element) }
end

.parse_element(node) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 40

def parse_element(node)
  case node[0]
  when :@int, :unary                 then parse_integer_node(node)
  when :symbol_literal, :dyna_symbol then parse_symbol_node(node)
  when :string_literal               then unescape_ruby(string_literal_text(node[1]))
  when :var_ref                      then node.dig(1, 1) # `Foo`
  when :const_path_ref               then "#{parse_element(node[1])}::#{node[2][1]}" # `Foo::Bar`
  else
    # simplecov:disable — defensive fallback for unexpected Ripper node shapes
    raise ArgumentError, "unexpected element: #{node.inspect}"
    # simplecov:enable
  end
end

.parse_integer_node(node) ⇒ Object



54
55
56
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 54

def parse_integer_node(node)
  node[0] == :@int ? node[1].to_i : -node[2][1].to_i
end

.parse_symbol_node(node) ⇒ Object



58
59
60
61
62
63
64
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 58

def parse_symbol_node(node)
  if node[0] == :symbol_literal
    node.dig(1, 1, 1).to_sym
  else
    unescape_ruby(string_literal_text(node[1])).to_sym
  end
end

.quote_inspected_class_segments(str) ⇒ Object

Method coverage keys can contain inspect-format class references like ‘#<Class:Foo>` or `#<Class:0x…>`, which aren’t valid Ruby syntax. Wrap them in quotes so Ripper can parse the surrounding array literal; downstream we treat them as opaque strings.



83
84
85
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 83

def quote_inspected_class_segments(str)
  str.gsub(/#<[^>]*>/) { |segment| %("#{segment.gsub('"', '\\"')}") }
end

.string_literal_text(string_content) ⇒ Object

Concatenate the text fragments of a ‘:string_content` node. Ripper may emit zero, one, or many `:@tstring_content` children depending on the literal.



69
70
71
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 69

def string_literal_text(string_content)
  Array(string_content[1..]).map { |child| child[1] }.join
end

.unescape_ruby(raw) ⇒ Object

Undo the same backslash-prefix escapes the previous hand-rolled parser undid: ‘X` → `X` for any X.



75
76
77
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 75

def unescape_ruby(raw)
  raw.gsub(/\\(.)/) { ::Regexp.last_match(1) }
end