123456789_123456789_123456789_123456789_123456789_

Class: SimpleCov::SourceFile

Relationships & Source Files
Namespace Children
Classes:
Inherits: Object
Defined in: lib/simplecov/source_file.rb,
lib/simplecov/source_file/branch.rb,
lib/simplecov/source_file/line.rb,
lib/simplecov/source_file/method.rb

Overview

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

Constant Summary

Class Attribute Summary

Class Method Summary

Instance Attribute Summary

Instance Method Summary

Constructor Details

.new(filename, coverage_data, loaded: true) ⇒ SourceFile

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 21

def initialize(filename, coverage_data, loaded: true)
  @filename = filename
  @coverage_data = coverage_data
  @loaded = loaded
end

Class Attribute Details

.nocov_warned (readonly)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 216

attr_reader :nocov_warned

Instance Attribute Details

#coverage_data (readonly)

The array of coverage data received from the Coverage.result

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 19

attr_reader :coverage_data

#filename (readonly)

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

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 17

attr_reader :filename

#no_branches?Boolean (readonly)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 111

def no_branches?
  total_branches.empty?
end

#no_lines?Boolean (readonly)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 97

def no_lines?
  lines.empty? || (lines.length == never_lines.size)
end

#not_loaded?Boolean (readonly)

Whether this file was added via track_files but never loaded/required.

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 186

def not_loaded?
  !@loaded
end

Instance Method Details

#branch_coverage_statistics (private)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 464

def branch_coverage_statistics
  # Files added via track_files but never loaded/required have no branch
  # data. Report 0% instead of misleading 100% (see #902).
  if not_loaded? && covered_branches.empty? && missed_branches.empty?
    return {branch: CoverageStatistics.new(covered: 0, missed: 0, percent: 0.0)}
  end

  {
    branch: CoverageStatistics.new(
      covered: covered_branches.size,
      missed: missed_branches.size
    )
  }
end

#branches

Return all the branches inside current source file

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 107

def branches
  @branches ||= build_branches
end

#branches_coverage_percent

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 115

def branches_coverage_percent
  coverage_statistics[:branch].percent
end

#branches_for_line(line_number)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 151

def branches_for_line(line_number)
  branches_report.fetch(line_number, [])
end

#branches_report

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

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 127

def branches_report
  @branches_report ||= build_branches_report
end

#build_branch(branch_data, hit_count, condition_start_line) (private)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 441

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_branchesArray (private)

Call recursive method that transform our static hash to array of objects

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 323

def build_branches
  coverage_branch_data = coverage_data["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) (private)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 427

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_reportHash (private)

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

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 312

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 (private)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 281

def build_lines
  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_methods (private)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 479

def build_methods
  methods = coverage_data.fetch("methods", {}).map do |info, hit_count|
    info = restore_ruby_data_structure(info)
    SourceFile::Method.new(self, info, hit_count)
  end

  process_skipped_methods(methods)
end

#build_no_cov_chunks (private)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 197

def build_no_cov_chunks
  no_cov_lines = src.map.with_index(1).select { |line_src, _index| LinesClassifier.no_cov_line?(line_src) }

  warn_no_cov_deprecation(no_cov_lines.first.last) if no_cov_lines.any?

  # 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_statistics

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 40

def coverage_statistics
  @coverage_statistics ||=
    {
      **line_coverage_statistics,
      **branch_coverage_statistics,
      **method_coverage_statistics
    }
end

#covered_branchesArray

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

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 138

def covered_branches
  @covered_branches ||= branches.select(&:covered?)
end

#covered_lines

Returns all covered lines as SourceFile::Line

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 57

def covered_lines
  @covered_lines ||= lines.select(&:covered?)
end

#covered_methods

Return all covered methods

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 172

def covered_methods
  @covered_methods ||= methods.select(&:covered?)
end

#covered_percent

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

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 89

def covered_percent
  coverage_statistics[:line].percent
end

#covered_strength

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 93

def covered_strength
  coverage_statistics[:line].strength
end

#directive_chunks (private)

Per-category disabled line ranges from # simplecov:disable directives.

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 230

def directive_chunks
  @directive_chunks ||= Directive.disabled_ranges(src)
end

#ensure_remove_undefs(file_lines) (private)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 268

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|
    # simplecov:disable — defensive: only fires for non-UTF-8 source files
    line.encode!("UTF-8", invalid: :replace, undef: :replace) unless line.encoding == Encoding::UTF_8
    # simplecov:enable
  end
end

#line(number)

Access SourceFile::Line source lines by line number

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 84

def line(number)
  lines[number - 1]
end

#line_coverage_statistics (private)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 453

def line_coverage_statistics
  {
    line: CoverageStatistics.new(
      total_strength: lines_strength,
      covered: covered_lines.size,
      missed: missed_lines.size,
      omitted: never_lines.size
    )
  }
end

#line_with_missed_branch?(line_number) ⇒ Boolean

Check if any branches missing on given line number

Parameters:

  • line_number (Integer)
[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 162

def line_with_missed_branch?(line_number)
  branches_for_line(line_number).any? { |_type, count| count.zero? }
end

#lines Also known as: #source_lines

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

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 51

def lines
  @lines ||= build_lines
end

#lines_of_code

Returns the number of relevant lines (covered + missed)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 79

def lines_of_code
  coverage_statistics[:line].total
end

#lines_strength (private)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 301

def lines_strength
  lines.sum { |line| line.coverage.to_i }
end

#load_source (private)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 234

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

#mark_chunks_skipped(lines, chunks) (private)

The array the lines are kept in is 0-based whereas the line numbers in the chunks are 1-based (more understandable elsewhere), so each range needs to be shifted down by one to slice into #lines.

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 297

def mark_chunks_skipped(lines, chunks)
  chunks.each { |chunk| lines[(chunk.begin - 1)..(chunk.end - 1)].each(&:skipped!) }
end

#method_coverage_statistics (private)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 499

def method_coverage_statistics
  if not_loaded? && covered_methods.empty? && missed_methods.empty?
    return {method: CoverageStatistics.new(covered: 0, missed: 0, percent: 0.0)}
  end

  {
    method: CoverageStatistics.new(
      covered: covered_methods.size,
      missed: missed_methods.size
    )
  }
end

#methods

Return all methods detected in this source file

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 167

def methods
  @methods ||= build_methods
end

#methods_coverage_percent

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 181

def methods_coverage_percent
  coverage_statistics[:method].percent
end

#missed_branchesArray

Select the missed branches with coverage equal to zero

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 147

def missed_branches
  @missed_branches ||= branches.select(&:missed?)
end

#missed_lines

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

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 63

def missed_lines
  @missed_lines ||= lines.select(&:missed?)
end

#missed_methods

Return all missed methods

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 177

def missed_methods
  @missed_methods ||= methods.select(&:missed?)
end

#never_lines

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

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 69

def never_lines
  @never_lines ||= lines.select(&:never?)
end

#no_cov_chunks (private)

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

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 193

def no_cov_chunks
  @no_cov_chunks ||= build_no_cov_chunks
end

#parse_array_element(node) (private)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 380

def parse_array_element(node)
  case node[0]
  when :@int, :unary                 then parse_integer_node(node)
  when :symbol_literal, :dyna_symbol then parse_symbol_node(node)
  when :string_literal               then unescape_ruby(string_literal_text(node[1]))
  when :var_ref                      then node.dig(1, 1) # `Foo`
  when :const_path_ref               then "#{parse_array_element(node[1])}::#{node[2][1]}" # `Foo::Bar`
  else
    # simplecov:disable — defensive fallback for unexpected Ripper node shapes
    raise ArgumentError, "unexpected element: #{node.inspect}"
    # simplecov:enable
  end
end

#parse_integer_node(node) (private)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 394

def parse_integer_node(node)
  node[0] == :@int ? node[1].to_i : -node[2][1].to_i
end

#parse_ruby_array_string(str) (private)

Parse a string like '[:if, 0, 3, 4, 3, 21]' or '["ClassName", :method1, 2, 2, 5, 5]' back into a Ruby array, without using eval (see #801). Uses Ripper to walk the literal so we don't need to hand-roll a scanner for symbols, strings, integers, and constant paths.

Raises:

  • (ArgumentError)
[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 366

def parse_ruby_array_string(str)
  # Try plain Ripper first; only pre-quote `#<...>` inspect segments
  # if the input isn't already valid Ruby (otherwise we 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))
  # simplecov:disable — defensive: Ripper.sexp returning nil from both passes requires malformed input
  array_node = sexp&.dig(1, 0)
  # simplecov:enable
  raise ArgumentError, "expected array literal: #{str.inspect}" unless array_node && array_node[0] == :array

  Array(array_node[1]).map { |element| parse_array_element(element) }
end

#parse_symbol_node(node) (private)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 398

def parse_symbol_node(node)
  if node[0] == :symbol_literal
    node.dig(1, 1, 1).to_sym
  else
    unescape_ruby(string_literal_text(node[1])).to_sym
  end
end

#process_skipped_branches(branches) (private)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 332

def process_skipped_branches(branches)
  chunks = no_cov_chunks + directive_chunks.fetch(:branch)
  return branches if chunks.empty?

  # A non-inline branch's source range starts on its arm body (e.g. the
  # `:yes` line of `if cond / :yes / else / :no / end`), but `report_line`
  # is the condition line above it — that's where the user sees the
  # branch in the report and where they would naturally place an inline
  # `# simplecov:disable branch` directive. Honour both.
  branches.each do |branch|
    branch.skipped! if chunks.any? { |chunk| branch.overlaps_with?(chunk) || chunk.include?(branch.report_line) }
  end

  branches
end

#process_skipped_lines(lines) (private)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 288

def process_skipped_lines(lines)
  mark_chunks_skipped(lines, no_cov_chunks)
  mark_chunks_skipped(lines, directive_chunks.fetch(:line))
  lines
end

#process_skipped_methods(methods) (private)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 488

def process_skipped_methods(methods)
  method_chunks = directive_chunks.fetch(:method)
  return methods if method_chunks.empty?

  methods.each do |method|
    method.skipped! if method_chunks.any? { |chunk| method.overlaps_with?(chunk) }
  end

  methods
end

#project_filename

The path to this source file relative to the projects directory

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 28

def project_filename
  @filename.delete_prefix(SimpleCov.root).sub(%r{\A[/\\]}, "")
end

#quote_inspected_class_segments(str) (private)

SourceFile::Method coverage keys can contain inspect-format class references like # or #, which aren't valid Ruby syntax. Wrap them in quotes so Ripper can parse the surrounding array literal; downstream we treat them as opaque strings.

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 423

def quote_inspected_class_segments(str)
  str.gsub(/#<[^>]*>/) { |segment| %("#{segment.gsub('"', '\\"')}") }
end

#read_lines(file, lines, current_line) (private)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 253

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

#relevant_lines

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 101

def relevant_lines
  lines.size - never_lines.size - skipped_lines.size
end

#restore_ruby_data_structure(structure) (private)

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.

This safely parses the string representation back to a Ruby array without using eval. See #801.

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 354

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)

  parse_ruby_array_string(structure.to_s)
end

#set_encoding_based_on_magic_comment(file, line) (private)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 260

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) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 249

def shebang?(line)
  SHEBANG_REGEX.match?(line)
end

#skipped_lines

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

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 74

def skipped_lines
  @skipped_lines ||= lines.select(&:skipped?)
end

#source

Alias for #src.

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 38

alias source src

#source_lines

Alias for #lines.

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 54

alias source_lines lines

#src Also known as: #source

The source code for this file. Aliased as :source

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 33

def src
  # We intentionally read source code lazily to
  # suppress reading unused source code.
  @src ||= load_source
end

#string_literal_text(string_content) (private)

Concatenate the text fragments of a :string_content node. Ripper may emit zero, one, or many :@tstring_content children depending on the literal.

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 409

def string_literal_text(string_content)
  Array(string_content[1..]).map { |child| child[1] }.join
end

#total_branches

Return the relevant branches to source file

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 121

def total_branches
  @total_branches ||= covered_branches + missed_branches
end

#unescape_ruby(raw) (private)

Undo the same backslash-prefix escapes the previous hand-rolled parser undid: \XX for any X.

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 415

def unescape_ruby(raw)
  raw.gsub(/\\(.)/) { ::Regexp.last_match(1) }
end

#warn_no_cov_deprecation(first_line_number) (private)

Emit a one-time-per-file deprecation warning pointing the user at the # simplecov:disable / # simplecov:enable replacement.

[ GitHub ]

  
# File 'lib/simplecov/source_file.rb', line 221

def warn_no_cov_deprecation(first_line_number)
  return unless self.class.nocov_warned.add?(filename)

  token = SimpleCov.current_nocov_token
  warn "#{filename}:#{first_line_number}: [DEPRECATION] `# :#{token}:` is deprecated and will be removed " \
       "in a future release. Replace with `# simplecov:disable` / `# simplecov:enable` block comments."
end