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
-
RUBY_FILE_ENCODING_MAGIC_COMMENT_REGEX =
# File 'lib/simplecov/source_file.rb', line 14/\A#\s*(?:-\*-)?\s*(?:en)?coding:\s*(\S+)\s*(?:-\*-)?\s*\z/ -
SHEBANG_REGEX =
# File 'lib/simplecov/source_file.rb', line 13/\A#!/
Class Attribute Summary
- .nocov_warned readonly
Class Method Summary
Instance Attribute Summary
-
#coverage_data
readonly
The array of coverage data received from the
Coverage.result -
#filename
readonly
The full path to this source file (e.g.
- #no_branches? ⇒ Boolean readonly
- #no_lines? ⇒ Boolean readonly
-
#not_loaded? ⇒ Boolean
readonly
Whether this file was added via track_files but never loaded/required.
Instance Method Summary
-
#branches
Return all the branches inside current source file.
- #branches_coverage_percent
- #branches_for_line(line_number)
-
#branches_report
Return hash with key of line number and branch coverage count as value.
- #coverage_statistics
-
#covered_branches ⇒ Array
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.
-
#covered_lines
Returns all covered lines as
Line -
#covered_methods
Return all covered methods.
-
#covered_percent
The coverage for this file in percent.
- #covered_strength
-
#line(number)
Access
Linesource lines by line number. -
#line_with_missed_branch?(line_number) ⇒ Boolean
Check if any branches missing on given line number.
-
#lines
(also: #source_lines)
Returns all source lines for this file as instances of
Line, and thus including coverage data. -
#lines_of_code
Returns the number of relevant lines (covered + missed).
-
#methods
Return all methods detected in this source file.
- #methods_coverage_percent
-
#missed_branches ⇒ Array
Select the missed branches with coverage equal to zero.
-
#missed_lines
Returns all lines that should have been, but were not covered as instances of
Line -
#missed_methods
Return all missed methods.
-
#never_lines
Returns all lines that are not relevant for coverage as
Lineinstances. -
#project_filename
The path to this source file relative to the projects directory.
- #relevant_lines
-
#skipped_lines
Returns all lines that were skipped as
Lineinstances. -
#source
Alias for #src.
-
#source_lines
Alias for #lines.
-
#src
(also: #source)
The source code for this file.
-
#total_branches
Return the relevant branches to source file.
- #branch_coverage_statistics private
- #build_branch(branch_data, hit_count, condition_start_line) private
-
#build_branches ⇒ Array
private
Call recursive method that transform our static hash to array of objects.
- #build_branches_from(condition, branches) private
-
#build_branches_report ⇒ Hash
private
Build full branches report Root branches represent the wrapper of all condition state that have inside the branches.
- #build_lines private
- #build_methods private
- #build_no_cov_chunks private
-
#directive_chunks
private
Per-category disabled line ranges from
# simplecov:disabledirectives. - #ensure_remove_undefs(file_lines) private
- #line_coverage_statistics private
- #lines_strength private
- #load_source private
-
#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.
- #method_coverage_statistics private
-
#no_cov_chunks
private
no_cov_chunks is zero indexed to work directly with the array holding the lines.
- #parse_array_element(node) private
- #parse_integer_node(node) private
-
#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 usingeval(see#801). - #parse_symbol_node(node) private
- #process_skipped_branches(branches) private
- #process_skipped_lines(lines) private
- #process_skipped_methods(methods) private
-
#quote_inspected_class_segments(str)
private
Methodcoverage keys can contain inspect-format class references like#or#, which aren't valid Ruby syntax. - #read_lines(file, lines, current_line) private
-
#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.
- #set_encoding_based_on_magic_comment(file, line) private
- #shebang?(line) ⇒ Boolean private
-
#string_literal_text(string_content)
private
Concatenate the text fragments of a
:string_contentnode. -
#unescape_ruby(raw)
private
Undo the same backslash-prefix escapes the previous hand-rolled parser undid:
\X→Xfor any X. -
#warn_no_cov_deprecation(first_line_number)
private
Emit a one-time-per-file deprecation warning pointing the user at the
# simplecov:disable/# simplecov:enablereplacement.
Constructor Details
.new(filename, coverage_data, loaded: true) ⇒ SourceFile
# 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
# 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)
# 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.
# 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
# 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
# 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_branches ⇒ Array (private)
Call recursive method that transform our static hash to array of objects
# 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_report ⇒ Hash (private)
Build full branches report Root branches represent the wrapper of all condition state that have inside the branches
# 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_branches ⇒ Array
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
# 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
# File 'lib/simplecov/source_file.rb', line 57
def covered_lines @covered_lines ||= lines.select(&:covered?) end
#covered_methods
Return all covered methods
# 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
# 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.
# 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
# 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
# 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
# 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)
# File 'lib/simplecov/source_file.rb', line 79
def lines_of_code coverage_statistics[:line].total end
#lines_strength (private)
[ GitHub ]#load_source (private)
[ GitHub ]#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.
#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
# 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_branches ⇒ Array
Select the missed branches with coverage equal to zero
# 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
# File 'lib/simplecov/source_file.rb', line 63
def missed_lines @missed_lines ||= lines.select(&:missed?) end
#missed_methods
Return all missed methods
# 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
# 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
# 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.
# 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
#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.
# 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.
# 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)
# 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
# File 'lib/simplecov/source_file.rb', line 74
def skipped_lines @skipped_lines ||= lines.select(&:skipped?) end
#source
Alias for #src.
# File 'lib/simplecov/source_file.rb', line 38
alias source src
#source_lines
Alias for #lines.
# 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
# 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.
# 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
# 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: \X → X for any X.
# 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.
# 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