1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
|
# frozen_string_literal: true
require 'racc/grammarfileparser'
# Unfortunately, Ruby's Coverage module ignores module_eval statements,
# which Racc uses to map `parser.y` locations in the generated
# `parser.rb`.
module RaccCoverage
@coverage = {}
@base_path = nil
@trace = nil
def self.start(parsers, base_path)
@base_path = base_path
parsers.each do |parser|
@coverage[parser] = extract_interesting_lines(parser, base_path)
end
@trace = TracePoint.new(:line) do |trace|
lineno = trace.lineno - 1
if (line_coverage = @coverage[trace.path])
if line_coverage[lineno]
line_coverage[lineno] += 1
end
end
end
@trace.enable
end
def self.stop
@trace.disable
end
# Ruby's TracePoint#lineno will point only on "interesting" lines,
# i.e.: only code (no comments or empty lines), no `end` keywords,
# and for multi-line statements, only the first line of the statement.
#
# This method implements a very dumb Ruby parser, which skips empty lines
# or lines with just comments, `end` keywords, and correctly handles
# multi-line statements of the following form:
#
# * All lines of the statement except the last must end with `,`, `.` or `(`.
#
# Coverage can be disabled for code regions with annotations :nocov: and :cov:.
#
# Also, for best results, all actions should be delimited by at least
# one non-action line.
#
def self.extract_interesting_lines(parser, base_path)
grammar_source = File.join(@base_path, parser)
grammar_file = Racc::GrammarFileParser.parse_file(grammar_source)
ruby_sources = [
# Header and footer aren't passed through module_eval
# in Racc-generated file, so the location info is lost.
*grammar_file.params.inner,
].compact
grammar_file.grammar.each_rule do |rule|
source = rule.action.source
next if source.nil?
ruby_sources << source
end
lines = []
ruby_sources.each do |source|
first_line = source.lineno
state = :first_line
source.text.each_line.with_index do |line, index|
line = line.strip
continues = line.end_with?(',') ||
line.end_with?('(') ||
line.end_with?('.')
case state
when :first_line
if line =~ /:nocov/
state = :nocov
next
elsif line.empty? ||
line == 'end' ||
line.start_with?('#')
next
elsif continues
state = :mid_line
end
lines[first_line + index - 1] = 0
when :mid_line
unless continues
state = :first_line
end
when :nocov
if line =~ /:cov:/
state = :first_line
end
end
end
end
lines
end
def self.result
result =
@coverage.map do |parser, coverage|
[File.join(@base_path, parser), coverage]
end
Hash[result]
end
end
class << SimpleCov
def result_with_racc_coverage
@result ||= SimpleCov::Result.new(
Coverage.result.merge(RaccCoverage.result))
result_without_racc_coverage
end
alias result_without_racc_coverage result
alias result result_with_racc_coverage
end
|