File: racc_coverage_helper.rb

package info (click to toggle)
ruby-whitequark-parser 3.3.4.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,828 kB
  • sloc: yacc: 40,699; ruby: 20,395; makefile: 12; sh: 8
file content (133 lines) | stat: -rw-r--r-- 3,334 bytes parent folder | download | duplicates (3)
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