class SimpleCov::SourceFile

Representation of a source file including it's coverage data, source code, source lines and featuring helpers to interpret that data.

Constants

RUBY_FILE_ENCODING_MAGIC_COMMENT_REGEX
SHEBANG_REGEX

Attributes

coverage_data[R]

The array of coverage data received from the Coverage.result

filename[R]

The full path to this source file (e.g. /User/colszowka/projects/simplecov/lib/simplecov/source_file.rb)

Public Class Methods

new(filename, coverage_data) click to toggle source
# File lib/simplecov/source_file.rb, line 13
def initialize(filename, coverage_data)
  @filename = filename
  @coverage_data = coverage_data
end

Public Instance Methods

branches() click to toggle source

Return all the branches inside current source file

# File lib/simplecov/source_file.rb, line 97
def branches
  @branches ||= build_branches
end
branches_coverage_percent() click to toggle source
# File lib/simplecov/source_file.rb, line 105
def branches_coverage_percent
  coverage_statistics[:branch]&.percent
end
branches_for_line(line_number) click to toggle source
# File lib/simplecov/source_file.rb, line 141
def branches_for_line(line_number)
  branches_report.fetch(line_number, [])
end
branches_report() click to toggle source

Return hash with key of line number and branch coverage count as value

# File lib/simplecov/source_file.rb, line 117
def branches_report
  @branches_report ||= build_branches_report
end
coverage_statistics() click to toggle source
# File lib/simplecov/source_file.rb, line 31
def coverage_statistics
  @coverage_statistics ||=
    {
      **line_coverage_statistics,
      **branch_coverage_statistics
    }
end
covered_branches() click to toggle source

Select the covered branches Here we user tree schema because some conditions like case may have additional else that is not in declared inside the code but given by default by coverage report

@return [Array]

# File lib/simplecov/source_file.rb, line 128
def covered_branches
  @covered_branches ||= branches.select(&:covered?)
end
covered_lines() click to toggle source

Returns all covered lines as SimpleCov::SourceFile::Line

# File lib/simplecov/source_file.rb, line 47
def covered_lines
  @covered_lines ||= lines.select(&:covered?)
end
covered_percent() click to toggle source

The coverage for this file in percent. 0 if the file has no coverage lines

# File lib/simplecov/source_file.rb, line 79
def covered_percent
  coverage_statistics[:line]&.percent
end
covered_strength() click to toggle source
# File lib/simplecov/source_file.rb, line 83
def covered_strength
  coverage_statistics[:line]&.strength
end
line(number) click to toggle source

Access SimpleCov::SourceFile::Line source lines by line number

# File lib/simplecov/source_file.rb, line 74
def line(number)
  lines[number - 1]
end
line_with_missed_branch?(line_number) click to toggle source

Check if any branches missing on given line number

@param [Integer] line_number

@return [Boolean]

# File lib/simplecov/source_file.rb, line 152
def line_with_missed_branch?(line_number)
  branches_for_line(line_number).select { |_type, count| count.zero? }.any?
end
lines() click to toggle source

Returns all source lines for this file as instances of SimpleCov::SourceFile::Line, and thus including coverage data. Aliased as :source_lines

# File lib/simplecov/source_file.rb, line 41
def lines
  @lines ||= build_lines
end
Also aliased as: source_lines
lines_of_code() click to toggle source

Returns the number of relevant lines (covered + missed)

# File lib/simplecov/source_file.rb, line 69
def lines_of_code
  coverage_statistics[:line]&.total
end
missed_branches() click to toggle source

Select the missed branches with coverage equal to zero

@return [Array]

# File lib/simplecov/source_file.rb, line 137
def missed_branches
  @missed_branches ||= branches.select(&:missed?)
end
missed_lines() click to toggle source

Returns all lines that should have been, but were not covered as instances of SimpleCov::SourceFile::Line

# File lib/simplecov/source_file.rb, line 53
def missed_lines
  @missed_lines ||= lines.select(&:missed?)
end
never_lines() click to toggle source

Returns all lines that are not relevant for coverage as SimpleCov::SourceFile::Line instances

# File lib/simplecov/source_file.rb, line 59
def never_lines
  @never_lines ||= lines.select(&:never?)
end
no_branches?() click to toggle source
# File lib/simplecov/source_file.rb, line 101
def no_branches?
  total_branches.empty?
end
no_lines?() click to toggle source
# File lib/simplecov/source_file.rb, line 87
def no_lines?
  lines.length.zero? || (lines.length == never_lines.size)
end
project_filename() click to toggle source

The path to this source file relative to the projects directory

# File lib/simplecov/source_file.rb, line 19
def project_filename
  @filename.delete_prefix(SimpleCov.root)
end
relevant_lines() click to toggle source
# File lib/simplecov/source_file.rb, line 91
def relevant_lines
  lines.size - never_lines.size - skipped_lines.size
end
skipped_lines() click to toggle source

Returns all lines that were skipped as SimpleCov::SourceFile::Line instances

# File lib/simplecov/source_file.rb, line 64
def skipped_lines
  @skipped_lines ||= lines.select(&:skipped?)
end
source()
Alias for: src
source_lines()
Alias for: lines
src() click to toggle source

The source code for this file. Aliased as :source

# File lib/simplecov/source_file.rb, line 24
def src
  # We intentionally read source code lazily to
  # suppress reading unused source code.
  @src ||= load_source
end
Also aliased as: source
total_branches() click to toggle source

Return the relevant branches to source file

# File lib/simplecov/source_file.rb, line 111
def total_branches
  @total_branches ||= covered_branches + missed_branches
end

Private Instance Methods

branch_coverage_statistics() click to toggle source
# File lib/simplecov/source_file.rb, line 345
def branch_coverage_statistics
  {
    branch: CoverageStatistics.new(
      covered: covered_branches.size,
      missed:  missed_branches.size
    )
  }
end
build_branch(branch_data, hit_count, condition_start_line) click to toggle source
# File lib/simplecov/source_file.rb, line 323
def build_branch(branch_data, hit_count, condition_start_line)
  type, _id, start_line, _start_col, end_line, _end_col = branch_data

  SourceFile::Branch.new(
    start_line: start_line,
    end_line:   end_line,
    coverage:   hit_count,
    inline:     start_line == condition_start_line,
    type:       type
  )
end
build_branches() click to toggle source

Call recursive method that transform our static hash to array of objects @return [Array]

# File lib/simplecov/source_file.rb, line 272
def build_branches
  coverage_branch_data = coverage_data.fetch("branches", {})
  branches = coverage_branch_data.flat_map do |condition, coverage_branches|
    build_branches_from(condition, coverage_branches)
  end

  process_skipped_branches(branches)
end
build_branches_from(condition, branches) click to toggle source
# File lib/simplecov/source_file.rb, line 309
def build_branches_from(condition, branches)
  # the format handed in from the coverage data is like this:
  #
  #     [:then, 4, 6, 6, 6, 10]
  #
  # which is [type, id, start_line, start_col, end_line, end_col]
  _condition_type, _condition_id, condition_start_line, * = restore_ruby_data_structure(condition)

  branches.map do |branch_data, hit_count|
    branch_data = restore_ruby_data_structure(branch_data)
    build_branch(branch_data, hit_count, condition_start_line)
  end
end
build_branches_report() click to toggle source

Build full branches report Root branches represent the wrapper of all condition state that have inside the branches

@return [Hash]

# File lib/simplecov/source_file.rb, line 261
def build_branches_report
  branches.reject(&:skipped?).each_with_object({}) do |branch, coverage_statistics|
    coverage_statistics[branch.report_line] ||= []
    coverage_statistics[branch.report_line] << branch.report
  end
end
build_lines() click to toggle source
# File lib/simplecov/source_file.rb, line 228
def build_lines
  coverage_exceeding_source_warn if coverage_data["lines"].size > src.size
  lines = src.map.with_index(1) do |src, i|
    SimpleCov::SourceFile::Line.new(src, i, coverage_data["lines"][i - 1])
  end
  process_skipped_lines(lines)
end
build_no_cov_chunks() click to toggle source
# File lib/simplecov/source_file.rb, line 163
def build_no_cov_chunks
  no_cov_lines = src.map.with_index(1).select { |line_src, _index| LinesClassifier.no_cov_line?(line_src) }

  # if we have an uneven number of nocovs we assume they go to the
  # end of the file, the source doesn't really matter
  # Can't deal with this within the each_slice due to differing
  # behavior in JRuby: jruby/jruby#6048
  no_cov_lines << ["", src.size] if no_cov_lines.size.odd?

  no_cov_lines.each_slice(2).map do |(_line_src_start, index_start), (_line_src_end, index_end)|
    index_start..index_end
  end
end
coverage_exceeding_source_warn() click to toggle source

Warning to identify condition from Issue #56

# File lib/simplecov/source_file.rb, line 250
def coverage_exceeding_source_warn
  warn "Warning: coverage data provided by Coverage [#{coverage_data['lines'].size}] exceeds number of lines in #{filename} [#{src.size}]"
end
ensure_remove_undefs(file_lines) click to toggle source
# File lib/simplecov/source_file.rb, line 213
def ensure_remove_undefs(file_lines)
  # invalid/undef replace are technically not really necessary but nice to
  # have and work around a JRuby incompatibility. Also moved here from
  # simplecov-html to have encoding shenaningans in one place. See #866
  # also setting these option on `file.set_encoding` doesn't seem to work
  # properly so it has to be done here.
  file_lines.each do |line|
    if line.encoding == Encoding::UTF_8
      line
    else
      line.encode!("UTF-8", invalid: :replace, undef: :replace)
    end
  end
end
line_coverage_statistics() click to toggle source
# File lib/simplecov/source_file.rb, line 335
def line_coverage_statistics
  {
    line: CoverageStatistics.new(
      total_strength: lines_strength,
      covered:  covered_lines.size,
      missed:   missed_lines.size
    )
  }
end
lines_strength() click to toggle source
# File lib/simplecov/source_file.rb, line 245
def lines_strength
  lines.sum { |line| line.coverage.to_i }
end
load_source() click to toggle source
# File lib/simplecov/source_file.rb, line 177
def load_source
  lines = []
  # The default encoding is UTF-8
  File.open(filename, "rb:UTF-8") do |file|
    current_line = file.gets

    if shebang?(current_line)
      lines << current_line
      current_line = file.gets
    end

    read_lines(file, lines, current_line)
  end
end
no_cov_chunks() click to toggle source

#no_cov_chunks is zero indexed to work directly with the array holding the lines

# File lib/simplecov/source_file.rb, line 159
def no_cov_chunks
  @no_cov_chunks ||= build_no_cov_chunks
end
process_skipped_branches(branches) click to toggle source
# File lib/simplecov/source_file.rb, line 281
def process_skipped_branches(branches)
  return branches if no_cov_chunks.empty?

  branches.each do |branch|
    branch.skipped! if no_cov_chunks.any? { |no_cov_chunk| branch.overlaps_with?(no_cov_chunk) }
  end

  branches
end
process_skipped_lines(lines) click to toggle source
# File lib/simplecov/source_file.rb, line 236
def process_skipped_lines(lines)
  # the array the lines are kept in is 0-based whereas the line numbers in the nocov
  # chunks are 1-based and are expected to be like this in other parts (and it's also
  # arguably more understandable)
  no_cov_chunks.each { |chunk| lines[(chunk.begin - 1)..(chunk.end - 1)].each(&:skipped!) }

  lines
end
read_lines(file, lines, current_line) click to toggle source
# File lib/simplecov/source_file.rb, line 197
def read_lines(file, lines, current_line)
  return lines unless current_line

  set_encoding_based_on_magic_comment(file, current_line)
  lines.concat([current_line], ensure_remove_undefs(file.readlines))
end
restore_ruby_data_structure(structure) click to toggle source

Since we are dumping to and loading from JSON, and we have arrays as keys those don't make their way back to us intact e.g. just as a string

We should probably do something different here, but as it stands these are our data structures that we write so eval isn't too bad.

See #801

# File lib/simplecov/source_file.rb, line 299
def restore_ruby_data_structure(structure)
  # Tests use the real data structures (except for integration tests) so no need to
  # put them through here.
  return structure if structure.is_a?(Array)

  # rubocop:disable Security/Eval
  eval structure
  # rubocop:enable Security/Eval
end
set_encoding_based_on_magic_comment(file, line) click to toggle source
# File lib/simplecov/source_file.rb, line 205
def set_encoding_based_on_magic_comment(file, line)
  # Check for encoding magic comment
  # Encoding magic comment must be placed at first line except for shebang
  if (match = RUBY_FILE_ENCODING_MAGIC_COMMENT_REGEX.match(line))
    file.set_encoding(match[1], "UTF-8")
  end
end
shebang?(line) click to toggle source
# File lib/simplecov/source_file.rb, line 193
def shebang?(line)
  SHEBANG_REGEX.match?(line)
end