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.

String parses are memoized: Combine::BranchesCombiner and Combine::MethodsCombiner derive a merge identity from every key of both sides on every pairwise merge, so collating N resultsets parses each key string N-1 times — and Ripper dominates the wall time of a large collate. Key strings repeat across folds and within report building, while the set of unique keys is bounded by the project's branch and method count, so a permanent cache stays small. Cached arrays are frozen element-wise (String class names included, not just the tuple): every caller destructures without mutating, and sharing one array across callers must stay that way. The cache key needs no such care — Hash#[]= dups and freezes String keys on its own.



31
32
33
34
35
36
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 31

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

  string = structure.to_s
  parse_cache[string] ||= parse_array_string(string).each(&:freeze).freeze
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)


44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 44

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_cacheObject



38
39
40
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 38

def parse_cache
  @parse_cache ||= {} #: Hash[String, untyped]
end

.parse_element(node) ⇒ Object



58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 58

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



72
73
74
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 72

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

.parse_symbol_node(node) ⇒ Object



76
77
78
79
80
81
82
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 76

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. The pattern recurses because singleton methods on instances nest one inspect segment inside another (#<Class:#<Object:0x...>>), and stopping at the first > leaves a dangling > that fails both Ripper passes and crashes the merge.



105
106
107
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 105

def quote_inspected_class_segments(str)
  str.gsub(/#<(?:[^<>]|\g<0>)*>/) { |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.



87
88
89
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 87

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: \XX for any X.



93
94
95
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 93

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