Module: SimpleCov::SourceFile::RubyDataParser
- Extended by:
- RubyDataParser
- Included in:
- 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 (#801). The grammar covers symbols,
strings, integers, unary minus, and constant paths: every shape Coverage
ever emits.
Instance Method Summary collapse
-
#call(structure) ⇒ Object
String parses are memoized:
Combine::BranchesCombinerandCombine::MethodsCombinerderive 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. -
#const_path(node) ⇒ Object
A constant path's namespace is an element in its own right, so
Foo::Bar::Bazcomes back together one segment at a time. -
#literal_text(node) ⇒ Object
Ripper may emit zero, one, or many fragments under a literal depending on its form, and the escapes it leaves in the source text come back out.
-
#parse_array_string(str) ⇒ Object
Parses a string like '[:if, 0, 3, 4, 3, 21]' or '["ClassName", :method1, 2, 2, 5, 5]' back into a Ruby array.
- #parse_cache ⇒ Object
-
#parse_element(node) ⇒ Object
Each
whenalready knows the node's shape, so the elements read straight out of it rather than through a second dispatch. -
#quote_inspected_class_segments(str) ⇒ Object
Method coverage keys can contain inspect-format class references like
#<Class:Foo>, which aren't valid Ruby syntax. -
#unescape_ruby(raw) ⇒ Object
Undoes the same backslash-prefix escapes the previous hand-rolled parser undid:
\X->Xfor any X.
Instance Method Details
#call(structure) ⇒ Object
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. 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:
every caller destructures without mutating, and sharing one array
across callers must stay that way. The cache key needs no such care,
since Hash#[]= dups and freezes String keys on its own.
27 28 29 30 31 |
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 27 def call(structure) return structure if structure.is_a?(Array) parse_cache[structure] ||= parse_array_string(structure).each(&:freeze).freeze end |
#const_path(node) ⇒ Object
A constant path's namespace is an element in its own right, so
Foo::Bar::Baz comes back together one segment at a time.
76 77 78 |
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 76 def const_path(node) "#{parse_element(node.fetch(1))}::#{node.fetch(2).fetch(1)}" end |
#literal_text(node) ⇒ Object
Ripper may emit zero, one, or many fragments under a literal depending on its form, and the escapes it leaves in the source text come back out.
82 83 84 |
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 82 def literal_text(node) unescape_ruby(node.fetch(1)[1..].map { |fragment| fragment.fetch(1) }.join) end |
#parse_array_string(str) ⇒ Object
Parses a string like '[:if, 0, 3, 4, 3, 21]' or '["ClassName", :method1, 2, 2, 5, 5]' back into a Ruby array.
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 39 def parse_array_string(str) # Plain Ripper first: pre-quoting `#<...>` inspect segments would 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)) # Ripper wraps what it parsed in a `:program` node holding the # statements, and this input is supposed to be one array literal and # nothing else. Both passes answer nil for input Ruby can't parse at # all, so there may be no statement to unwrap. _program, statements = sexp first_statement, *extra = statements array_node = Array(first_statement) unless extra.empty? && array_node.first.equal?(:array) raise ArgumentError, "expected array literal: #{str.inspect}" end Array(array_node.fetch(1)).map { |element| parse_element(element) } end |
#parse_cache ⇒ Object
33 34 35 |
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 33 def parse_cache @parse_cache ||= {} #: Hash[String, untyped] end |
#parse_element(node) ⇒ Object
Each when already knows the node's shape, so the elements read straight
out of it rather than through a second dispatch. A symbol's text sits
where a string's does, one node deeper.
61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 61 def parse_element(node) case node.fetch(0) when :@int then Integer(node.fetch(1)) when :unary then -Integer(node.fetch(2).fetch(1)) # `-2` when :symbol_literal, :dyna_symbol then literal_text(node).to_sym when :string_literal then literal_text(node) when :var_ref then node.fetch(1).fetch(1) # `Foo` when :const_path_ref then const_path(node) # `Foo::Bar` else raise ArgumentError, "unexpected element: #{node}" end end |
#quote_inspected_class_segments(str) ⇒ Object
Method coverage keys can contain inspect-format class references like
#<Class:Foo>, which aren't valid Ruby syntax. Wrapping them in quotes
lets Ripper parse the surrounding array literal. 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.
98 99 100 |
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 98 def quote_inspected_class_segments(str) str.gsub(/#<(?:[^<>]|\g<0>)+>/) { |segment| %("#{segment.gsub('"', '\\"')}") } end |
#unescape_ruby(raw) ⇒ Object
Undoes the same backslash-prefix escapes the previous hand-rolled parser
undid: \X -> X for any X.
88 89 90 |
# File 'lib/simplecov/source_file/ruby_data_parser.rb', line 88 def unescape_ruby(raw) raw.gsub(/\\(.)/) { Regexp.last_match(1) } end |