simplecov 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +65 -1425
- data/lib/simplecov/atomic_file.rb +70 -0
- data/lib/simplecov/cli/clean.rb +43 -3
- data/lib/simplecov/cli/command_helpers.rb +55 -0
- data/lib/simplecov/cli/coverage.rb +21 -28
- data/lib/simplecov/cli/coverage_file.rb +65 -0
- data/lib/simplecov/cli/diff.rb +43 -48
- data/lib/simplecov/cli/dotfile.rb +12 -6
- data/lib/simplecov/cli/merge.rb +12 -10
- data/lib/simplecov/cli/open.rb +3 -5
- data/lib/simplecov/cli/report.rb +31 -23
- data/lib/simplecov/cli/serve/report_preparer.rb +29 -0
- data/lib/simplecov/cli/serve/static_file_handler.rb +122 -0
- data/lib/simplecov/cli/serve.rb +30 -94
- data/lib/simplecov/cli/uncovered.rb +20 -25
- data/lib/simplecov/cli.rb +15 -2
- data/lib/simplecov/combine/branches_combiner.rb +43 -19
- data/lib/simplecov/combine/coverage_accumulator.rb +268 -0
- data/lib/simplecov/combine/identity_interner.rb +30 -0
- data/lib/simplecov/combine/interned_counts.rb +30 -0
- data/lib/simplecov/combine/lines_combiner.rb +48 -20
- data/lib/simplecov/combine/methods_combiner.rb +46 -21
- data/lib/simplecov/combine/results_combiner.rb +12 -37
- data/lib/simplecov/combine.rb +5 -23
- data/lib/simplecov/command_guesser.rb +66 -9
- data/lib/simplecov/configuration/coverage.rb +12 -15
- data/lib/simplecov/configuration/coverage_criteria.rb +34 -38
- data/lib/simplecov/configuration/eval_coverage.rb +41 -0
- data/lib/simplecov/configuration/filters.rb +11 -44
- data/lib/simplecov/configuration/formatting.rb +18 -9
- data/lib/simplecov/configuration/groups.rb +42 -0
- data/lib/simplecov/configuration/merging.rb +8 -7
- data/lib/simplecov/configuration/thresholds.rb +14 -13
- data/lib/simplecov/configuration.rb +13 -43
- data/lib/simplecov/coverage_json.rb +24 -0
- data/lib/simplecov/coverage_statistics.rb +1 -1
- data/lib/simplecov/coverage_violations.rb +15 -5
- data/lib/simplecov/defaults.rb +7 -3
- data/lib/simplecov/directive.rb +1 -1
- data/lib/simplecov/exit_codes/check.rb +33 -0
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +11 -20
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +3 -16
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +14 -23
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +14 -25
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +4 -17
- data/lib/simplecov/exit_codes.rb +1 -0
- data/lib/simplecov/exit_handling.rb +11 -41
- data/lib/simplecov/file_list.rb +5 -10
- data/lib/simplecov/filter.rb +43 -9
- data/lib/simplecov/formatter/base.rb +11 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +97 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +35 -5
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +96 -0
- data/lib/simplecov/formatter/html_formatter.rb +67 -47
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +54 -56
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +78 -88
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +70 -76
- data/lib/simplecov/formatter/json_formatter.rb +5 -48
- data/lib/simplecov/formatter/multi_formatter.rb +1 -1
- data/lib/simplecov/formatter/simple_formatter.rb +8 -5
- data/lib/simplecov/formatter.rb +12 -0
- data/lib/simplecov/group_names.rb +32 -0
- data/lib/simplecov/last_run.rb +16 -9
- data/lib/simplecov/lines_classifier.rb +29 -8
- data/lib/simplecov/load_global_config.rb +5 -2
- data/lib/simplecov/parallel_adapters/base.rb +17 -0
- data/lib/simplecov/parallel_adapters/generic.rb +2 -2
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +2 -2
- data/lib/simplecov/parallel_coordination.rb +6 -1
- data/lib/simplecov/parallel_result_merger.rb +230 -0
- data/lib/simplecov/report_deferral.rb +49 -0
- data/lib/simplecov/report_stamp.rb +28 -0
- data/lib/simplecov/result.rb +40 -10
- data/lib/simplecov/result_adapter.rb +48 -16
- data/lib/simplecov/result_merger/resultset_file.rb +43 -7
- data/lib/simplecov/result_merger/resultset_run_identity.rb +67 -0
- data/lib/simplecov/result_merger/resultset_store.rb +15 -12
- data/lib/simplecov/result_merger/unloaded_files.rb +103 -0
- data/lib/simplecov/result_merger.rb +69 -42
- data/lib/simplecov/result_processing.rb +82 -43
- data/lib/simplecov/run_identity.rb +77 -0
- data/lib/simplecov/simulate_coverage.rb +35 -11
- data/lib/simplecov/source_file/method.rb +7 -1
- data/lib/simplecov/source_file/ruby_data_parser.rb +25 -3
- data/lib/simplecov/source_file/skip_chunks.rb +7 -10
- data/lib/simplecov/source_file/source_loader.rb +23 -7
- data/lib/simplecov/source_file/statistics.rb +24 -16
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +203 -13
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +19 -30
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +7 -0
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +55 -0
- data/lib/simplecov/static_coverage_extractor/value_position.rb +6 -14
- data/lib/simplecov/static_coverage_extractor/visitor.rb +23 -36
- data/lib/simplecov/unloaded_file_injector.rb +75 -0
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov.rb +11 -5
- data/schemas/coverage-v1.0.schema.json +2 -2
- data/schemas/coverage.schema.json +2 -2
- data/sig/simplecov.rbs +206 -67
- metadata +28 -14
- data/doc/alternate-formatters.md +0 -66
- data/doc/commercial-services.md +0 -25
- data/doc/editor-integration.md +0 -18
- data/lib/simplecov/combine/files_combiner.rb +0 -70
- data/lib/simplecov/formatter/html_formatter/public/application.css +0 -1
- data/lib/simplecov/formatter/html_formatter/public/application.js +0 -18
- data/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
- data/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
- data/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
|
@@ -17,17 +17,27 @@ module SimpleCov
|
|
|
17
17
|
lines = [] #: Array[String]
|
|
18
18
|
# The default encoding is UTF-8
|
|
19
19
|
File.open(filename, "rb:UTF-8") do |file|
|
|
20
|
-
current_line = file.gets
|
|
20
|
+
current_line = scrub_invalid(file.gets)
|
|
21
21
|
|
|
22
22
|
if current_line && shebang?(current_line)
|
|
23
23
|
lines << current_line
|
|
24
|
-
current_line = file.gets
|
|
24
|
+
current_line = scrub_invalid(file.gets)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
read_lines(file, lines, current_line)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# A line read as UTF-8 can still carry invalid bytes (a Latin-1
|
|
32
|
+
# source file without a magic comment, say). Replace them before
|
|
33
|
+
# any regex sees the line: the shebang and magic-comment checks
|
|
34
|
+
# would otherwise raise ArgumentError and take the report down.
|
|
35
|
+
def scrub_invalid(line)
|
|
36
|
+
return line if line.nil? || line.valid_encoding?
|
|
37
|
+
|
|
38
|
+
line.scrub
|
|
39
|
+
end
|
|
40
|
+
|
|
31
41
|
def shebang?(line)
|
|
32
42
|
SHEBANG_REGEX.match?(line)
|
|
33
43
|
end
|
|
@@ -47,14 +57,20 @@ module SimpleCov
|
|
|
47
57
|
end
|
|
48
58
|
end
|
|
49
59
|
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
# properly,
|
|
60
|
+
# Guarantee every line leaves the loader as valid UTF-8, replacing
|
|
61
|
+
# what can't be represented: transcode non-UTF-8 lines (setting
|
|
62
|
+
# invalid/undef options on `file.set_encoding` doesn't work
|
|
63
|
+
# properly, and this also works around a JRuby incompatibility)
|
|
64
|
+
# and scrub UTF-8-tagged lines that carry invalid bytes, which
|
|
65
|
+
# would otherwise raise from every regex the classifier runs.
|
|
54
66
|
def ensure_remove_undefs(file_lines)
|
|
55
67
|
file_lines.each do |line|
|
|
56
68
|
# simplecov:disable — defensive: only fires for non-UTF-8 source files
|
|
57
|
-
|
|
69
|
+
if line.encoding == Encoding::UTF_8
|
|
70
|
+
line.scrub! unless line.valid_encoding?
|
|
71
|
+
else
|
|
72
|
+
line.encode!("UTF-8", invalid: :replace, undef: :replace)
|
|
73
|
+
end
|
|
58
74
|
# simplecov:enable
|
|
59
75
|
end
|
|
60
76
|
end
|
|
@@ -22,35 +22,43 @@ module SimpleCov
|
|
|
22
22
|
private
|
|
23
23
|
|
|
24
24
|
def line_statistics
|
|
25
|
-
sf = @source_file
|
|
26
25
|
{
|
|
27
|
-
line:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
omitted: sf.never_lines.size
|
|
26
|
+
line: coverage_statistics(
|
|
27
|
+
@source_file.covered_lines,
|
|
28
|
+
@source_file.missed_lines,
|
|
29
|
+
omitted: @source_file.never_lines.size
|
|
32
30
|
)
|
|
33
31
|
}
|
|
34
32
|
end
|
|
35
33
|
|
|
34
|
+
# Files added via track_files but never loaded have no branch/method
|
|
35
|
+
# data. Report 0% instead of the empty-set default of 100% (see #902).
|
|
36
36
|
def branch_statistics
|
|
37
37
|
sf = @source_file
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if sf.not_loaded? &&
|
|
41
|
-
return {branch: CoverageStatistics.new(covered: 0, missed: 0, percent: 0.0)}
|
|
42
|
-
end
|
|
38
|
+
covered = sf.covered_branches
|
|
39
|
+
missed = sf.missed_branches
|
|
40
|
+
percent = 0.0 if sf.not_loaded? && covered.empty? && missed.empty?
|
|
43
41
|
|
|
44
|
-
{branch:
|
|
42
|
+
{branch: coverage_statistics(covered, missed, percent: percent)}
|
|
45
43
|
end
|
|
46
44
|
|
|
47
45
|
def method_statistics
|
|
48
46
|
sf = @source_file
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
covered = sf.covered_methods
|
|
48
|
+
missed = sf.missed_methods
|
|
49
|
+
percent = 0.0 if sf.not_loaded? && covered.empty? && missed.empty?
|
|
50
|
+
|
|
51
|
+
{method: coverage_statistics(covered, missed, percent: percent)}
|
|
52
|
+
end
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
def coverage_statistics(covered, missed, omitted: 0, percent: nil)
|
|
55
|
+
CoverageStatistics.new(
|
|
56
|
+
total_strength: covered.sum { |item| item.coverage.to_i },
|
|
57
|
+
covered: covered.size,
|
|
58
|
+
missed: missed.size,
|
|
59
|
+
omitted: omitted,
|
|
60
|
+
percent: percent
|
|
61
|
+
)
|
|
54
62
|
end
|
|
55
63
|
end
|
|
56
64
|
end
|
|
@@ -9,41 +9,231 @@ module SimpleCov
|
|
|
9
9
|
# phantom that no loaded run can ever hit, the same unmergeable-tuple
|
|
10
10
|
# failure mode as #1226 / #1233.
|
|
11
11
|
module ConditionFolding
|
|
12
|
+
# CRuby 3.4 rebuilt the fold on the Prism compiler, and the
|
|
13
|
+
# parse.y-based fold it replaced differed in three observable ways,
|
|
14
|
+
# each pinned by the runtime tuple equivalence battery on CI:
|
|
15
|
+
# `__FILE__` folded on 3.2/3.3 but no longer does; parentheses were
|
|
16
|
+
# transparent for every literal on 3.2 (opacity starts at 3.3); and
|
|
17
|
+
# on 3.2 the dead arm's branch table entries survive the fold —
|
|
18
|
+
# parse.y instrumented branches before eliminating dead code — while
|
|
19
|
+
# its `def`s still never register.
|
|
20
|
+
FOLDS_SOURCE_FILE = Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.4")
|
|
21
|
+
PARENS_ALWAYS_TRANSPARENT = Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.3")
|
|
22
|
+
DEAD_ARM_BRANCHES_SURVIVE = PARENS_ALWAYS_TRANSPARENT
|
|
23
|
+
# Container literals in discarded position are eliminated from 3.3
|
|
24
|
+
# on, but the contents rule differs: 3.3's compile.c elides a
|
|
25
|
+
# container whose contents are merely effect-free (`[x]`, `[self]`),
|
|
26
|
+
# while the Prism compiler (3.4+) demands fully static literals
|
|
27
|
+
# (`[1]` goes, `[x]` stays). See `static_container_literal?`.
|
|
28
|
+
CONTAINER_CONTENTS_NEED_STATIC_LITERALS = !FOLDS_SOURCE_FILE
|
|
29
|
+
|
|
12
30
|
# Prism node types for the literals that fold. `while` / `until` do
|
|
13
31
|
# NOT fold (`while true` is a real branch), so only the if-like
|
|
14
32
|
# visitors consult this. Regexp and Range literals are excluded on
|
|
15
33
|
# purpose: as conditions they mean `=~ $_` / flip-flop, which
|
|
16
|
-
# Coverage does branch on.
|
|
34
|
+
# Coverage does branch on. `[]`, `{}`, and interpolated strings do
|
|
35
|
+
# not fold either, and `->` folds while a `lambda` call does not —
|
|
36
|
+
# the compiler only folds what it can prove at compile time, and a
|
|
37
|
+
# method named `lambda` proves nothing.
|
|
38
|
+
# simplecov:disable branch — which arm runs is fixed by the running Ruby's version
|
|
17
39
|
STATIC_CONDITION_TYPES = [
|
|
18
40
|
::Prism::IntegerNode, ::Prism::FloatNode, ::Prism::RationalNode,
|
|
19
41
|
::Prism::ImaginaryNode, ::Prism::SymbolNode, ::Prism::StringNode,
|
|
20
|
-
::Prism::TrueNode, ::Prism::FalseNode, ::Prism::NilNode
|
|
42
|
+
::Prism::TrueNode, ::Prism::FalseNode, ::Prism::NilNode,
|
|
43
|
+
::Prism::SourceLineNode, ::Prism::SourceEncodingNode, ::Prism::LambdaNode,
|
|
44
|
+
*(::Prism::SourceFileNode if FOLDS_SOURCE_FILE)
|
|
45
|
+
].freeze
|
|
46
|
+
# simplecov:enable branch
|
|
47
|
+
|
|
48
|
+
# The literals whose fold eliminates the *then* side: `if false` /
|
|
49
|
+
# `if nil` keep only the else arm. Every other folded literal is
|
|
50
|
+
# truthy and keeps only the then arm.
|
|
51
|
+
FALSY_CONDITION_TYPES = [::Prism::FalseNode, ::Prism::NilNode].freeze
|
|
52
|
+
|
|
53
|
+
# The literals whose fold does NOT see through parentheses: CRuby
|
|
54
|
+
# folds `if nil`, `if "x"`, and `if -> {}` but keeps a real branch
|
|
55
|
+
# for `if (nil)`, `if ("x")`, and `if (-> {})` — verified against
|
|
56
|
+
# Coverage, and pinned by the runtime tuple equivalence battery —
|
|
57
|
+
# while every other literal folds parenthesized or not. `__FILE__`
|
|
58
|
+
# is opaque like other strings on the Rubies that fold it at all.
|
|
59
|
+
# Consulted only when PARENS_ALWAYS_TRANSPARENT is false.
|
|
60
|
+
PAREN_OPAQUE_TYPES = [
|
|
61
|
+
::Prism::NilNode, ::Prism::StringNode, ::Prism::LambdaNode, ::Prism::SourceFileNode
|
|
62
|
+
].freeze
|
|
63
|
+
|
|
64
|
+
# The scalar literals the compiler treats as fully static: a
|
|
65
|
+
# multi-statement paren condition (`if (1; 2)`) folds by its last
|
|
66
|
+
# expression only when every leading statement is eliminated when
|
|
67
|
+
# discarded, and these — bare or composing an Array/Hash/Range —
|
|
68
|
+
# always are. Pinned against real Coverage on 3.2 through 4.0.
|
|
69
|
+
STATIC_LITERAL_LEAF_TYPES = [
|
|
70
|
+
::Prism::IntegerNode, ::Prism::FloatNode, ::Prism::RationalNode,
|
|
71
|
+
::Prism::ImaginaryNode, ::Prism::StringNode, ::Prism::SymbolNode,
|
|
72
|
+
::Prism::TrueNode, ::Prism::FalseNode, ::Prism::NilNode,
|
|
73
|
+
::Prism::SourceLineNode, ::Prism::SourceFileNode, ::Prism::SourceEncodingNode, ::Prism::RegularExpressionNode
|
|
74
|
+
].freeze
|
|
75
|
+
|
|
76
|
+
# Non-literal reads that are also eliminated when discarded.
|
|
77
|
+
# `self` is eliminated by every supported compiler; local/ivar/
|
|
78
|
+
# defined? elimination arrived with the Prism-era compilers.
|
|
79
|
+
# Anything that can raise or run hooks (constants, globals, calls,
|
|
80
|
+
# writes) is never eliminated and keeps the branch real.
|
|
81
|
+
PRISM_ERA_ELIMINABLE_READS = [
|
|
82
|
+
::Prism::LocalVariableReadNode, ::Prism::InstanceVariableReadNode, ::Prism::DefinedNode
|
|
83
|
+
].freeze
|
|
84
|
+
|
|
85
|
+
# simplecov:disable branch — which arm runs is fixed by the running Ruby's version
|
|
86
|
+
ELIMINABLE_READ_TYPES = [
|
|
87
|
+
::Prism::SelfNode,
|
|
88
|
+
*(PRISM_ERA_ELIMINABLE_READS unless PARENS_ALWAYS_TRANSPARENT)
|
|
21
89
|
].freeze
|
|
90
|
+
# simplecov:enable branch
|
|
22
91
|
|
|
23
92
|
private
|
|
24
93
|
|
|
25
|
-
#
|
|
26
|
-
# `
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
94
|
+
# A truthy verdict keeps the first arm live, a falsy one the
|
|
95
|
+
# second. `visit` is nil-safe, so a missing arm just visits
|
|
96
|
+
# nothing. On 3.2 the dead arm's branch table entries survive the
|
|
97
|
+
# fold (parse.y instrumented branches before eliminating dead
|
|
98
|
+
# code, even inside dead `def` bodies and lambdas) while its
|
|
99
|
+
# methods never register, so the dead arm is visited with method
|
|
100
|
+
# collection suppressed.
|
|
101
|
+
def visit_folded_arms(verdict, truthy_arm, falsy_arm)
|
|
102
|
+
live, dead = verdict == :truthy ? [truthy_arm, falsy_arm] : [falsy_arm, truthy_arm]
|
|
103
|
+
visit(live)
|
|
104
|
+
# simplecov:disable branch — the taken arm is fixed by the running Ruby's version
|
|
105
|
+
visit_dead_arm(dead) if DEAD_ARM_BRANCHES_SURVIVE
|
|
106
|
+
# simplecov:enable branch
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# simplecov:disable — 3.2-only; unreachable on the modern dogfood Ruby
|
|
110
|
+
def visit_dead_arm(arm)
|
|
111
|
+
# Save/restore rather than set/clear: a fold nested inside this
|
|
112
|
+
# dead arm re-enters here (possibly with a nil arm) and must not
|
|
113
|
+
# clear suppression for the rest of the outer dead arm.
|
|
114
|
+
previous = @suppress_methods
|
|
115
|
+
begin
|
|
116
|
+
@suppress_methods = true
|
|
117
|
+
visit(arm)
|
|
118
|
+
ensure
|
|
119
|
+
@suppress_methods = previous
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
# simplecov:enable
|
|
123
|
+
|
|
124
|
+
# The compiler's verdict on a condition: `:truthy` or `:falsy` when
|
|
125
|
+
# it folds, nil when it doesn't. The verdict decides which arm
|
|
126
|
+
# survives — the compiler eliminates the other arm's entire subtree,
|
|
127
|
+
# so everything inside it (nested branches, methods) must go
|
|
128
|
+
# unvisited too, not just the folded condition's own tuple.
|
|
129
|
+
#
|
|
130
|
+
# Parentheses are transparent to the fold for most literals
|
|
131
|
+
# (`if (1)` folds like `if 1`) but not all — see
|
|
132
|
+
# PAREN_OPAQUE_TYPES. Compound forms (`!true`, `true || x`) are
|
|
133
|
+
# deliberately not folded: `!` never folds, and `||` / `&&`
|
|
134
|
+
# constant-propagation diverges across Ruby versions, so matching
|
|
135
|
+
# it would trade a rare, version-specific gain for real risk.
|
|
136
|
+
def folded_condition(node)
|
|
137
|
+
unwrapped = unwrap_parentheses(node)
|
|
138
|
+
return nil unless foldable?(node, unwrapped)
|
|
139
|
+
|
|
140
|
+
FALSY_CONDITION_TYPES.any? { |type| unwrapped.is_a?(type) } ? :falsy : :truthy
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# A folding literal, minus the ones parentheses shield from the
|
|
144
|
+
# fold (`unwrapped` differing from `node` is what says parentheses
|
|
145
|
+
# were seen through).
|
|
146
|
+
def foldable?(node, unwrapped)
|
|
147
|
+
return false unless STATIC_CONDITION_TYPES.any? { |type| unwrapped.is_a?(type) }
|
|
148
|
+
|
|
149
|
+
# simplecov:disable — which of these lines runs is fixed by the
|
|
150
|
+
# running Ruby's version: on 3.2 the early return always fires and
|
|
151
|
+
# the opacity check below it is dead code, so it must be excluded
|
|
152
|
+
# from line coverage too, not only branch coverage.
|
|
153
|
+
return true if PARENS_ALWAYS_TRANSPARENT
|
|
154
|
+
|
|
155
|
+
unwrapped.equal?(node) || PAREN_OPAQUE_TYPES.none? { |type| unwrapped.is_a?(type) }
|
|
156
|
+
# simplecov:enable
|
|
34
157
|
end
|
|
35
158
|
|
|
159
|
+
# Parentheses are transparent to the fold, and a multi-statement
|
|
160
|
+
# body (`if (1; 2)`) folds by its LAST expression — but only when
|
|
161
|
+
# the compiler eliminates every leading statement. Stopping at
|
|
162
|
+
# multi-statement bodies synthesized a phantom then/else pair no
|
|
163
|
+
# real run can ever hit.
|
|
36
164
|
def unwrap_parentheses(node)
|
|
37
165
|
# @type var current: untyped
|
|
38
166
|
current = node
|
|
39
167
|
while current.is_a?(::Prism::ParenthesesNode)
|
|
40
168
|
body = current.body
|
|
41
|
-
break unless body.is_a?(::Prism::StatementsNode) && body.body.
|
|
169
|
+
break unless body.is_a?(::Prism::StatementsNode) && !body.body.empty?
|
|
170
|
+
|
|
171
|
+
statements = body.body
|
|
172
|
+
break unless statements.take(statements.size - 1).all? { |leading| eliminable_when_discarded?(leading) }
|
|
42
173
|
|
|
43
|
-
current =
|
|
174
|
+
current = statements.last
|
|
44
175
|
end
|
|
45
176
|
current
|
|
46
177
|
end
|
|
178
|
+
|
|
179
|
+
# Whether the compiler compiles `node` to nothing when its value is
|
|
180
|
+
# discarded.
|
|
181
|
+
def eliminable_when_discarded?(node)
|
|
182
|
+
return true if static_container_literal?(node)
|
|
183
|
+
return true if ELIMINABLE_READ_TYPES.any? { |type| node.is_a?(type) }
|
|
184
|
+
|
|
185
|
+
node.is_a?(::Prism::ParenthesesNode) &&
|
|
186
|
+
node.body.is_a?(::Prism::StatementsNode) &&
|
|
187
|
+
node.body.body.all? { |statement| eliminable_when_discarded?(statement) }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# A scalar literal leaf, or an Array/Hash/Range whose contents pass
|
|
191
|
+
# the running compiler's contents rule (see
|
|
192
|
+
# CONTAINER_CONTENTS_NEED_STATIC_LITERALS). Container elimination
|
|
193
|
+
# only exists from 3.3 on.
|
|
194
|
+
def static_container_literal?(node)
|
|
195
|
+
return true if STATIC_LITERAL_LEAF_TYPES.any? { |type| node.is_a?(type) }
|
|
196
|
+
# simplecov:disable — which of these lines runs is fixed by the
|
|
197
|
+
# running Ruby's version: parse.y (3.2) never eliminates
|
|
198
|
+
# container literals, so there the early return always fires and
|
|
199
|
+
# the dispatch below is dead code.
|
|
200
|
+
return false if PARENS_ALWAYS_TRANSPARENT
|
|
201
|
+
|
|
202
|
+
static_container?(node)
|
|
203
|
+
# simplecov:enable
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# The container dispatch, version-free: the predicate specs
|
|
207
|
+
# exercise it directly on every supported Ruby, 3.2 included.
|
|
208
|
+
def static_container?(node)
|
|
209
|
+
case node
|
|
210
|
+
when ::Prism::ArrayNode then static_array_literal?(node)
|
|
211
|
+
when ::Prism::HashNode then static_hash_literal?(node)
|
|
212
|
+
when ::Prism::RangeNode then static_range_literal?(node)
|
|
213
|
+
else false
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def static_array_literal?(node)
|
|
218
|
+
node.elements.all? { |element| container_contents_eliminable?(element) }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def static_hash_literal?(node)
|
|
222
|
+
node.elements.all? do |element|
|
|
223
|
+
element.is_a?(::Prism::AssocNode) &&
|
|
224
|
+
container_contents_eliminable?(element.key) && container_contents_eliminable?(element.value)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def static_range_literal?(node)
|
|
229
|
+
[node.left, node.right].all? { |bound| bound.nil? || container_contents_eliminable?(bound) }
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def container_contents_eliminable?(node)
|
|
233
|
+
# simplecov:disable branch — which arm runs is fixed by the running Ruby's version
|
|
234
|
+
CONTAINER_CONTENTS_NEED_STATIC_LITERALS ? static_container_literal?(node) : eliminable_when_discarded?(node)
|
|
235
|
+
# simplecov:enable branch
|
|
236
|
+
end
|
|
47
237
|
end
|
|
48
238
|
end
|
|
49
239
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "prism_compat"
|
|
4
|
+
|
|
3
5
|
module SimpleCov
|
|
4
6
|
module StaticCoverageExtractor
|
|
5
7
|
# The source ranges Ruby's Coverage assigns to branch conditions and
|
|
@@ -34,11 +36,7 @@ module SimpleCov
|
|
|
34
36
|
def if_like_location(node, type)
|
|
35
37
|
return node.location unless LEGACY_COVERAGE_LOCATIONS && type == :if && elsif_node?(node)
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
PointLocation.new(
|
|
39
|
-
start_line: node.location.start_line, start_column: node.location.start_column,
|
|
40
|
-
end_line: content_end.end_line, end_column: content_end.end_column
|
|
41
|
-
)
|
|
39
|
+
span(node.location, legacy_content_end(node))
|
|
42
40
|
end
|
|
43
41
|
|
|
44
42
|
def elsif_node?(node)
|
|
@@ -52,7 +50,7 @@ module SimpleCov
|
|
|
52
50
|
def legacy_content_end(node)
|
|
53
51
|
tail = node
|
|
54
52
|
while tail.is_a?(::Prism::IfNode)
|
|
55
|
-
sub =
|
|
53
|
+
sub = PrismCompat.subsequent(tail)
|
|
56
54
|
break unless sub
|
|
57
55
|
|
|
58
56
|
tail = sub
|
|
@@ -76,14 +74,12 @@ module SimpleCov
|
|
|
76
74
|
end
|
|
77
75
|
|
|
78
76
|
# Resolve the source range Coverage attributes to a real-or-synthetic
|
|
79
|
-
# `:else` arm of an if-like construct
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
# When neither is present, the synthesized else inherits the
|
|
84
|
-
# condition's range (matches Coverage's convention).
|
|
77
|
+
# `:else` arm of an if-like construct (`PrismCompat` hides the
|
|
78
|
+
# per-Prism-version accessor split). When no else/elsif is present,
|
|
79
|
+
# the synthesized else inherits the condition's range (matches
|
|
80
|
+
# Coverage's convention).
|
|
85
81
|
def if_like_else_location(node, type)
|
|
86
|
-
sub =
|
|
82
|
+
sub = PrismCompat.subsequent(node)
|
|
87
83
|
return if_like_location(node, type) unless sub
|
|
88
84
|
# An `elsif` arrives as a nested IfNode. Coverage attributes the
|
|
89
85
|
# outer else arm to the clause's own range, not its then body
|
|
@@ -120,11 +116,7 @@ module SimpleCov
|
|
|
120
116
|
end
|
|
121
117
|
|
|
122
118
|
def legacy_when_value_location(case_node, when_node)
|
|
123
|
-
|
|
124
|
-
PointLocation.new(
|
|
125
|
-
start_line: when_node.location.start_line, start_column: when_node.location.start_column,
|
|
126
|
-
end_line: tail_end.end_line, end_column: tail_end.end_column
|
|
127
|
-
)
|
|
119
|
+
span(when_node.location, legacy_case_tail_end(case_node, when_node))
|
|
128
120
|
end
|
|
129
121
|
|
|
130
122
|
# The last body content in the case after `when_node`, falling
|
|
@@ -141,7 +133,7 @@ module SimpleCov
|
|
|
141
133
|
# condition when empty), so the whole clause extends the range
|
|
142
134
|
# through trailing EMPTY clauses that have no `statements`.
|
|
143
135
|
content = clauses.drop(index + 1).map(&:location)
|
|
144
|
-
else_statements =
|
|
136
|
+
else_statements = PrismCompat.else_clause(case_node)&.statements
|
|
145
137
|
content << else_statements.location if else_statements
|
|
146
138
|
content
|
|
147
139
|
end
|
|
@@ -152,7 +144,7 @@ module SimpleCov
|
|
|
152
144
|
# explicit else with an empty body — the else..end span on modern
|
|
153
145
|
# Rubies or the case's full range on legacy ones.
|
|
154
146
|
def else_arm_location(node)
|
|
155
|
-
else_clause =
|
|
147
|
+
else_clause = PrismCompat.else_clause(node)
|
|
156
148
|
return node.location unless else_clause
|
|
157
149
|
return else_clause.statements.location if else_clause.statements
|
|
158
150
|
return else_clause.location unless LEGACY_COVERAGE_LOCATIONS
|
|
@@ -200,10 +192,14 @@ module SimpleCov
|
|
|
200
192
|
# This convention is the same on legacy and modern Rubies. See
|
|
201
193
|
# issue #1233.
|
|
202
194
|
def safe_navigation_location(node)
|
|
203
|
-
|
|
195
|
+
span(node.location, node.closing_loc || node.arguments&.location || node.message_loc)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# The range from `from`'s start through `to`'s end.
|
|
199
|
+
def span(from, to)
|
|
204
200
|
PointLocation.new(
|
|
205
|
-
start_line:
|
|
206
|
-
end_line:
|
|
201
|
+
start_line: from.start_line, start_column: from.start_column,
|
|
202
|
+
end_line: to.end_line, end_column: to.end_column
|
|
207
203
|
)
|
|
208
204
|
end
|
|
209
205
|
|
|
@@ -214,13 +210,6 @@ module SimpleCov
|
|
|
214
210
|
)
|
|
215
211
|
end
|
|
216
212
|
|
|
217
|
-
# The `else`/`elsif` clause of an if-like node, under whichever
|
|
218
|
-
# accessor this Prism version exposes (see the two *_METHOD
|
|
219
|
-
# constants).
|
|
220
|
-
def if_like_subsequent(node)
|
|
221
|
-
node.is_a?(::Prism::IfNode) ? node.public_send(IF_NODE_SUBSEQUENT_METHOD) : node.public_send(ELSE_CLAUSE_METHOD)
|
|
222
|
-
end
|
|
223
|
-
|
|
224
213
|
# Whether an empty then arm collapses to a point at the predicate's
|
|
225
214
|
# end. Modern Coverage does this for every `if` (but not `unless`);
|
|
226
215
|
# legacy Coverage does it only in void position, for both.
|
|
@@ -22,7 +22,14 @@ module SimpleCov
|
|
|
22
22
|
# `def name(...)` and `def self.name(...)` both produce DefNode.
|
|
23
23
|
# The class context is the surrounding lexical class/module (or
|
|
24
24
|
# `Object` at the top level, matching `Coverage`'s convention).
|
|
25
|
+
# Suppression covers 3.2's folded dead arms, where nested branches
|
|
26
|
+
# stay instrumented but a `def` never registers (see
|
|
27
|
+
# Visitor#visit_dead_arm).
|
|
25
28
|
def visit_def_node(node)
|
|
29
|
+
# simplecov:disable branch — suppression is reachable on 3.2 only
|
|
30
|
+
return super if @suppress_methods
|
|
31
|
+
|
|
32
|
+
# simplecov:enable branch
|
|
26
33
|
loc = node.location
|
|
27
34
|
class_name = @class_stack.last || "Object"
|
|
28
35
|
key = [class_name, node.name, loc.start_line, loc.start_column, loc.end_line, loc.end_column]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module StaticCoverageExtractor
|
|
5
|
+
# The Prism 1.3 (Dec 2024) accessor renames for the trailing clause of
|
|
6
|
+
# conditional nodes, resolved ONCE at load so the per-node hot paths
|
|
7
|
+
# stay branch-free. Ruby 3.3's stdlib Prism (0.19) predates the
|
|
8
|
+
# renames; 3.4+ and any installed prism gem post-dates them. Reaching
|
|
9
|
+
# for the modern name on 0.19 raised NoMethodError inside the
|
|
10
|
+
# extractor — `call` swallowed it and the whole file silently fell
|
|
11
|
+
# back to no simulated data (see the 1.0.2 audit).
|
|
12
|
+
#
|
|
13
|
+
# This lives in its own file, required by every consumer, because the
|
|
14
|
+
# constants are referenced from several extractor files and defining
|
|
15
|
+
# them after those files were loaded worked only while the references
|
|
16
|
+
# happened at call time — a load-order trap for the next editor.
|
|
17
|
+
module PrismCompat
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# `Prism::IfNode#subsequent` was renamed from `consequent`. The
|
|
21
|
+
# not-taken arm on whichever Prism version we're on can't be
|
|
22
|
+
# exercised by our own dogfood (we only run on one Prism at a time).
|
|
23
|
+
# simplecov:disable
|
|
24
|
+
IF_NODE_SUBSEQUENT_METHOD =
|
|
25
|
+
if ::Prism::IfNode.method_defined?(:subsequent)
|
|
26
|
+
:subsequent
|
|
27
|
+
else
|
|
28
|
+
:consequent
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The same rename hit the `else` accessor on `UnlessNode`,
|
|
32
|
+
# `CaseNode`, and `CaseMatchNode` (all three: `consequent` ->
|
|
33
|
+
# `else_clause`). All three renamed together, so one constant
|
|
34
|
+
# (probed off CaseNode) covers them.
|
|
35
|
+
ELSE_CLAUSE_METHOD =
|
|
36
|
+
if ::Prism::CaseNode.method_defined?(:else_clause)
|
|
37
|
+
:else_clause
|
|
38
|
+
else
|
|
39
|
+
:consequent
|
|
40
|
+
end
|
|
41
|
+
# simplecov:enable
|
|
42
|
+
|
|
43
|
+
# The `else`/`elsif` clause of an if-like node (an ElseNode, or a
|
|
44
|
+
# nested IfNode for `elsif`), or the `else` clause of anything else
|
|
45
|
+
# that has one, under whichever accessor this Prism exposes.
|
|
46
|
+
def subsequent(node)
|
|
47
|
+
node.is_a?(::Prism::IfNode) ? node.public_send(IF_NODE_SUBSEQUENT_METHOD) : else_clause(node)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def else_clause(node)
|
|
51
|
+
node.public_send(ELSE_CLAUSE_METHOD)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "prism_compat"
|
|
4
|
+
|
|
3
5
|
module SimpleCov
|
|
4
6
|
module StaticCoverageExtractor
|
|
5
7
|
# Ruby 3.3 value-position analysis for the extractor's legacy branch
|
|
@@ -56,26 +58,16 @@ module SimpleCov
|
|
|
56
58
|
return [node.body] if node.is_a?(::Prism::DefNode)
|
|
57
59
|
return [] unless in_value
|
|
58
60
|
|
|
61
|
+
# `case/in` (CaseMatchNode) is intentionally not a tail construct:
|
|
62
|
+
# its `in` arms and `else` both discard tail position.
|
|
59
63
|
case node
|
|
60
64
|
when ::Prism::StatementsNode then [node.body.last]
|
|
61
|
-
when ::Prism::IfNode, ::Prism::UnlessNode then [node.statements, subsequent(node)]
|
|
62
|
-
when ::Prism::CaseNode then [*node.conditions, else_clause(node)]
|
|
65
|
+
when ::Prism::IfNode, ::Prism::UnlessNode then [node.statements, PrismCompat.subsequent(node)]
|
|
66
|
+
when ::Prism::CaseNode then [*node.conditions, PrismCompat.else_clause(node)]
|
|
63
67
|
when ::Prism::ElseNode, ::Prism::WhenNode, ::Prism::BeginNode, ::Prism::ProgramNode then [node.statements]
|
|
64
68
|
else []
|
|
65
69
|
end
|
|
66
70
|
end
|
|
67
|
-
|
|
68
|
-
# The `else`/`elsif` clause of an if-like node, and the `else` clause
|
|
69
|
-
# of a case, under whichever accessor this Prism version exposes.
|
|
70
|
-
# `case/in` (CaseMatchNode) is intentionally not a tail construct: its
|
|
71
|
-
# `in` arms and `else` both discard tail position.
|
|
72
|
-
def subsequent(node)
|
|
73
|
-
node.is_a?(::Prism::IfNode) ? node.public_send(IF_NODE_SUBSEQUENT_METHOD) : else_clause(node)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def else_clause(node)
|
|
77
|
-
node.public_send(ELSE_CLAUSE_METHOD)
|
|
78
|
-
end
|
|
79
71
|
# simplecov:enable
|
|
80
72
|
end
|
|
81
73
|
end
|