class Rcov::FileStatistics

counts would normally be obtained from a Rcov::CodeCoverageAnalyzer.
The array of strings representing the source code and the array of execution
sf.code_coverage # => 0.6<br>sf.coverage # => :inferred<br>sf.coverage # => true
sf.num_code_lines # => 5
sf.num_lines # => 5
“puts 2”, “end”], [1, 1, 0, 0, 0])
sf = FileStatistics.new(“foo.rb”, [“puts 1”, “if true &&”, “ false”,
Basic usage is as follows:
refine the initial (incomplete) coverage information.
number of heuristics to determine what is code and what is a comment, and to
=begin/=end, heredocs, many multiline-expressions… It uses a
FileStatistics is relatively intelligent: it handles normal comments,
times each line has been executed).
associated source code, and an array holding execution counts (i.e. how many
A FileStatistics object can be therefore be built given the filename, the
3. the per-line execution counts
2. the per-line coverage information after correction using rcov’s heuristics
1. its source code
A FileStatistics object associates a filename to:

def code_coverage

total amount of lines of code (loc). Returns a float from 0 to 1.0.
Code coverage rate: fraction of lines of code executed, relative to the
def code_coverage
  indices = (0...@lines.size).select{|i| is_code? i }
  return 0 if indices.size == 0
  count = 0
  indices.each {|i| count += 1 if @coverage[i] }
  1.0 * count / indices.size
end

def code_coverage_for_report

def code_coverage_for_report
  code_coverage * 100
end

def extend_heredocs

def extend_heredocs
  i = 0
  while i < @lines.size
    unless is_code? i
      i += 1
      next
    end
    #FIXME: using a restrictive regexp so that only <<[A-Z_a-z]\w*
    # matches when unquoted, so as to avoid problems with 1<<2
    # (keep in mind that whereas puts <<2 is valid, puts 1<<2 is a
    # parse error, but  a = 1<<2  is of course fine)
    scanner = StringScanner.new(@lines[i])
    j = k = i
    loop do
      scanned_text = scanner.search_full(/<<(-?)(?:(['"`])((?:(?!\2).)+)\2|([A-Z_a-z]\w*))/, true, true)
      # k is the first line after the end delimiter for the last heredoc
      # scanned so far
      unless scanner.matched?
        i = k
        break
      end
      term = scanner[3] || scanner[4]
      # try to ignore symbolic bitshifts like  1<<LSHIFT
      ident_text = "<<#{scanner[1]}#{scanner[2]}#{term}#{scanner[2]}"
      if scanned_text[/\d+\s*#{Regexp.escape(ident_text)}/]
        # it was preceded by a number, ignore
        i = k
        break
      end
      must_mark = []
      end_of_heredoc = (scanner[1] == "-") ? /^\s*#{Regexp.escape(term)}$/ : /^#{Regexp.escape(term)}$/
      loop do
        break if j == @lines.size
        must_mark << j
        if end_of_heredoc =~ @lines[j]
          must_mark.each do |n|
            @heredoc_start[n] = i
          end
          if (must_mark + [i]).any?{|lineidx| @coverage[lineidx]}
            @coverage[i] ||= :inferred
            must_mark.each{|lineidx| @coverage[lineidx] ||= :inferred}
          end
          # move the "first line after heredocs" index
          if @lines[j+=1] =~ /^\s*\n$/
            k = j
          end
          break
        end
        j += 1
      end
    end
    i += 1
  end
end

def find_multiline_strings

def find_multiline_strings
  state = :awaiting_string
  wanted_delimiter = nil
  string_begin_line = 0
  @lines.each_with_index do |line, i|
    matching_delimiters = Hash.new{|h,k| k} 
    matching_delimiters.update("{" => "}", "[" => "]", "(" => ")")
    case state
    when :awaiting_string
      # very conservative, doesn't consider the last delimited string but
      # only the very first one
      if md = /^[^#]*%(?:[qQ])?(.)/.match(line)
        if !/"%"/.match(line)
          wanted_delimiter = /(?!\\).#{Regexp.escape(matching_delimiters[md[1]])}/
          # check if closed on the very same line
          # conservative again, we might have several quoted strings with the
          # same delimiter on the same line, leaving the last one open
          unless wanted_delimiter.match(md.post_match)
            state = :want_end_delimiter
            string_begin_line = i
          end
        end
      end
    when :want_end_delimiter
      @multiline_string_start[i] = string_begin_line
      if wanted_delimiter.match(line)
        state = :awaiting_string
      end
    end
  end
end

def initialize(name, lines, counts, comments_run_by_default = false)

def initialize(name, lines, counts, comments_run_by_default = false)
  @name = name
  @lines = lines
  initial_coverage = counts.map{|x| (x || 0) > 0 ? true : false }
  @coverage = CoverageInfo.new initial_coverage
  @counts = counts
  @is_begin_comment = nil
  # points to the line defining the heredoc identifier
  # but only if it was marked (we don't care otherwise)
  @heredoc_start = Array.new(lines.size, false)
  @multiline_string_start = Array.new(lines.size, false)
  extend_heredocs
  find_multiline_strings
  precompute_coverage comments_run_by_default
end

def is_code?(lineno)

comment (either # or =begin/=end blocks).
Returns true if the given line number corresponds to code, as opposed to a
def is_code?(lineno)
  unless @is_begin_comment
    @is_begin_comment = Array.new(@lines.size, false)
    pending = []
    state = :code
    @lines.each_with_index do |line, index|
      line.force_encoding("utf-8") if line.respond_to?(:force_encoding)
      case state
      when :code
        if /^=begin\b/ =~ line
          state = :comment
          pending << index
        end
      when :comment
        pending << index
        if /^=end\b/ =~ line
          state = :code
          pending.each{|idx| @is_begin_comment[idx] = true}
          pending.clear
        end
      end
    end
  end
  @lines[lineno] && !@is_begin_comment[lineno] && @lines[lineno] !~ /^\s*(#|$)/ 
end

def is_nocov?(line)

def is_nocov?(line)
  line =~ /#:nocov:/
end

def mark_nocov_regions(nocov_line_numbers, coverage)

def mark_nocov_regions(nocov_line_numbers, coverage)
  while nocov_line_numbers.size > 0
    begin_line, end_line = nocov_line_numbers.shift, nocov_line_numbers.shift
    next unless begin_line && end_line
    (begin_line..end_line).each do |line_num|
      coverage[line_num] ||= :inferred
    end
  end
end

def merge(lines, coverage, counts)

Execution counts are just summated on a per-line basis.

* not covered (false) if it was uncovered in both
in either one
indicate that it was definitely executed, but it was inferred
* considered :inferred if the neither +self+ nor the +coverage+ array
+coverage+ array
* covered for sure (true) if it is covered in either +self+ or in the
As for code coverage, a line will be considered
Merge code coverage and execution count information.
def merge(lines, coverage, counts)
  coverage.each_with_index do |v, idx|
    case @coverage[idx]
    when :inferred 
      @coverage[idx] = v || @coverage[idx]
    when false 
      @coverage[idx] ||= v
    end
  end
  counts.each_with_index{|v, idx| @counts[idx] += v }
  precompute_coverage false
end

def next_expr_marked?(lineno)

def next_expr_marked?(lineno)
  return false if lineno >= @lines.size
  found = false
  idx = (lineno+1).upto(@lines.size-1) do |i|
    next unless is_code? i
    found = true
    break i
  end
  return false unless found
  @coverage[idx]
end

def num_code_lines

Number of lines of code (loc).
def num_code_lines
  (0...@lines.size).select{|i| is_code? i}.size
end

def num_lines

Total number of lines.
def num_lines
  @lines.size
end

def precompute_coverage(comments_run_by_default = true)

def precompute_coverage(comments_run_by_default = true)
  changed = false
  lastidx = lines.size - 1
  if (!is_code?(lastidx) || /^__END__$/ =~ @lines[-1]) && !@coverage[lastidx]
    # mark the last block of comments
    @coverage[lastidx] ||= :inferred
    (lastidx-1).downto(0) do |i|
      break if is_code?(i)
      @coverage[i] ||= :inferred
    end
  end
  nocov_line_numbers = []
  
  (0...lines.size).each do |i|
    nocov_line_numbers << i if is_nocov?(@lines[i])
    next if @coverage[i]
    line = @lines[i]
    if /^\s*(begin|ensure|else|case)\s*(?:#.*)?$/ =~ line && next_expr_marked?(i) or
      /^\s*(?:end|\})\s*(?:#.*)?$/ =~ line && prev_expr_marked?(i) or
      /^\s*(?:end\b|\})/ =~ line && prev_expr_marked?(i) && next_expr_marked?(i) or
      /^\s*rescue\b/ =~ line && next_expr_marked?(i) or
      /(do|\{)\s*(\|[^|]*\|\s*)?(?:#.*)?$/ =~ line && next_expr_marked?(i) or
      prev_expr_continued?(i) && prev_expr_marked?(i) or
      comments_run_by_default && !is_code?(i) or 
      /^\s*((\)|\]|\})\s*)+(?:#.*)?$/ =~ line && prev_expr_marked?(i) or
      prev_expr_continued?(i+1) && next_expr_marked?(i)
      @coverage[i] ||= :inferred
      changed = true
    end
    
  end
  mark_nocov_regions(nocov_line_numbers, @coverage)
  
  (@lines.size-1).downto(0) do |i|
    next if @coverage[i]
    if !is_code?(i) and @coverage[i+1] 
      @coverage[i] = :inferred
      changed = true
    end
  end
  extend_heredocs if changed
  # if there was any change, we have to recompute; we'll eventually
  # reach a fixed point and stop there
  precompute_coverage(comments_run_by_default) if changed
end

def prev_expr_continued?(lineno)

def prev_expr_continued?(lineno)
  return false if lineno <= 0
  return false if lineno >= @lines.size
  found = false
  if @multiline_string_start[lineno] && 
    @multiline_string_start[lineno] < lineno
    return true
  end
  # find index of previous code line
  idx = (lineno-1).downto(0) do |i|
    if @heredoc_start[i]
      found = true
      break @heredoc_start[i] 
    end
    next unless is_code? i
    found = true
    break i
  end
  return false unless found
  #TODO: write a comprehensive list
  if is_code?(lineno) && /^\s*((\)|\]|\})\s*)+(?:#.*)?$/.match(@lines[lineno])
    return true
  end
  #FIXME: / matches regexps too
  # the following regexp tries to reject #{interpolation}
  r = /(,|\.|\+|-|\*|\/|<|>|%|&&|\|\||<<|\(|\[|\{|=|and|or|\\)\s*(?:#(?![{$@]).*)?$/.match @lines[idx]
  # try to see if a multi-line expression with opening, closing delimiters
  # started on that line
  [%w!( )!].each do |opening_str, closing_str| 
    # conservative: only consider nesting levels opened in that line, not
    # previous ones too.
    # next regexp considers interpolation too
    line = @lines[idx].gsub(/#(?![{$@]).*$/, "")
    opened = line.scan(/#{Regexp.escape(opening_str)}/).size
    closed = line.scan(/#{Regexp.escape(closing_str)}/).size
    return true if opened - closed > 0
  end
  if /(do|\{)\s*\|[^|]*\|\s*(?:#.*)?$/.match @lines[idx]
    return false
  end
  r
end

def prev_expr_marked?(lineno)

def prev_expr_marked?(lineno)
  return false if lineno <= 0
  found = false
  idx = (lineno-1).downto(0) do |i|
    next unless is_code? i
    found = true
    break i
  end
  return false unless found
  @coverage[idx]
end

def total_coverage

considered executed if the the next statement was executed.
A comment is attached to the code following it (RDoc-style): it will be
a fraction, i.e. from 0 to 1.0.
Total coverage rate if comments are also considered "executable", given as
def total_coverage
  return 0 if @coverage.size == 0
  @coverage.inject(0.0) {|s,a| s + (a ? 1:0) } / @coverage.size
end

def total_coverage_for_report

def total_coverage_for_report
  total_coverage * 100
end