module SimpleCov::SourceFile::RubyDataParser

def call(structure)

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

so no need to put them through here.
Tests use the real data structures (except for integration tests)
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

def parse_array_string(str)

'["ClassName", :method1, 2, 2, 5, 5]' back into a Ruby array.
Parse a string like '[:if, 0, 3, 4, 3, 21]' or
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

def parse_cache

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

def parse_element(node)

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

def parse_integer_node(node)

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

def parse_symbol_node(node)

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

def quote_inspected_class_segments(str)

array literal; downstream we treat them as opaque strings.
syntax. Wrap them in quotes so Ripper can parse the surrounding
like `#` or `#`, which aren't valid Ruby
Method coverage keys can contain inspect-format class references
def quote_inspected_class_segments(str)
  str.gsub(/#<[^>]*>/) { |segment| %("#{segment.gsub('"', '\\"')}") }
end

def string_literal_text(string_content)

on the literal.
may emit zero, one, or many `:@tstring_content` children depending
Concatenate the text fragments of a `:string_content` node. Ripper
def string_literal_text(string_content)
  Array(string_content[1..]).map { |child| child[1] }.join
end

def unescape_ruby(raw)

parser undid: `\X` → `X` for any X.
Undo the same backslash-prefix escapes the previous hand-rolled
def unescape_ruby(raw)
  raw.gsub(/\\(.)/) { ::Regexp.last_match(1) }
end