simplecov 1.0.3 → 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 -1439
- 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 +36 -12
- 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 +6 -3
- 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 +5 -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_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 +6 -2
- 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 +200 -66
- 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
|
@@ -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
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "prism_compat"
|
|
3
4
|
require_relative "condition_folding"
|
|
4
5
|
require_relative "location_conventions"
|
|
5
6
|
require_relative "method_collector"
|
|
@@ -7,42 +8,13 @@ require_relative "value_position"
|
|
|
7
8
|
|
|
8
9
|
module SimpleCov
|
|
9
10
|
module StaticCoverageExtractor
|
|
10
|
-
# `Prism::IfNode#subsequent` was renamed from `consequent` in Prism
|
|
11
|
-
# 1.3 (Dec 2024). Ruby 3.3's stdlib still ships an older Prism that
|
|
12
|
-
# only exposes `consequent`; 3.4+ and any project that's done
|
|
13
|
-
# `gem install prism` exposes `subsequent`. Resolve the method name
|
|
14
|
-
# ONCE here so the per-node hot path stays branch-free. The
|
|
15
|
-
# not-taken arm on whichever Prism version we're on can't be
|
|
16
|
-
# exercised by our own dogfood (we only run on one Prism at a time).
|
|
17
|
-
# simplecov:disable
|
|
18
|
-
IF_NODE_SUBSEQUENT_METHOD =
|
|
19
|
-
if ::Prism::IfNode.method_defined?(:subsequent)
|
|
20
|
-
:subsequent
|
|
21
|
-
else
|
|
22
|
-
:consequent
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# The same Prism 1.3 rename hit the `else` accessor on `UnlessNode`,
|
|
26
|
-
# `CaseNode`, and `CaseMatchNode` (all three: `consequent` ->
|
|
27
|
-
# `else_clause`). Ruby 3.3's stdlib Prism (0.19) only exposes
|
|
28
|
-
# `consequent`, so reaching for `else_clause` there raised
|
|
29
|
-
# NoMethodError inside the extractor — `call` swallowed it and the
|
|
30
|
-
# whole file silently fell back to no simulated data for any
|
|
31
|
-
# `unless`/`else` or empty-arm `case`. Resolve the name once, like
|
|
32
|
-
# IF_NODE_SUBSEQUENT_METHOD. All three nodes renamed together, so one
|
|
33
|
-
# constant (probed off CaseNode) covers them.
|
|
34
|
-
ELSE_CLAUSE_METHOD =
|
|
35
|
-
if ::Prism::CaseNode.method_defined?(:else_clause)
|
|
36
|
-
:else_clause
|
|
37
|
-
else
|
|
38
|
-
:consequent
|
|
39
|
-
end
|
|
40
|
-
# simplecov:enable
|
|
41
|
-
|
|
42
11
|
# Prism visitor that accumulates branch and method tuples in the
|
|
43
12
|
# shape Ruby's `Coverage` reports. Tuple ids are sequential across
|
|
44
|
-
# the file
|
|
45
|
-
#
|
|
13
|
+
# the file like `Coverage`'s, but the numbering order can differ
|
|
14
|
+
# (e.g. `case`/`when` and chained `&.` are visited in a different
|
|
15
|
+
# order than Coverage numbers them). That's fine: the combiners
|
|
16
|
+
# intern on source span and the report output drops ids, so nothing
|
|
17
|
+
# downstream compares them. Only defined when Prism is loadable;
|
|
46
18
|
# `StaticCoverageExtractor.available?` is the runtime gate.
|
|
47
19
|
class Visitor < ::Prism::Visitor
|
|
48
20
|
# Method tuples and the class/module nesting that names them are
|
|
@@ -64,6 +36,7 @@ module SimpleCov
|
|
|
64
36
|
@next_id = 0
|
|
65
37
|
@class_stack = []
|
|
66
38
|
@value_positions = nil
|
|
39
|
+
@suppress_methods = false
|
|
67
40
|
end
|
|
68
41
|
|
|
69
42
|
# Entry point for a parsed file. On legacy Rubies the location of an
|
|
@@ -84,13 +57,27 @@ module SimpleCov
|
|
|
84
57
|
# `else`, another IfNode for `elsif`). When the subsequent is
|
|
85
58
|
# missing, Coverage synthesizes a `:else` arm attributed to the
|
|
86
59
|
# whole condition's range — we do the same.
|
|
60
|
+
#
|
|
61
|
+
# A folded condition emits no tuple, and on modern Rubies only its
|
|
62
|
+
# live arm is descended into: the compiler eliminates the dead
|
|
63
|
+
# arm's entire subtree, so a branch or method nested there would be
|
|
64
|
+
# a phantom no loaded run can produce. A falsy `if`'s elsif chain
|
|
65
|
+
# survives as a plain `if`, which is what visiting the subsequent
|
|
66
|
+
# IfNode emits. On 3.2 the dead arm is visited too, branches only
|
|
67
|
+
# (see visit_folded_arms).
|
|
87
68
|
def visit_if_node(node)
|
|
88
|
-
|
|
69
|
+
verdict = folded_condition(node.predicate)
|
|
70
|
+
return visit_folded_arms(verdict, node.statements, PrismCompat.subsequent(node)) if verdict
|
|
71
|
+
|
|
72
|
+
emit_if_like(node, :if)
|
|
89
73
|
super
|
|
90
74
|
end
|
|
91
75
|
|
|
92
76
|
def visit_unless_node(node)
|
|
93
|
-
|
|
77
|
+
verdict = folded_condition(node.predicate)
|
|
78
|
+
return visit_folded_arms(verdict, PrismCompat.else_clause(node), node.statements) if verdict
|
|
79
|
+
|
|
80
|
+
emit_if_like(node, :unless)
|
|
94
81
|
super
|
|
95
82
|
end
|
|
96
83
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "simulate_coverage"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
# Fills in coverage for files that were tracked (via `cover` / `track_files`)
|
|
7
|
+
# but never loaded, so they count toward the denominators instead of being
|
|
8
|
+
# absent from the report entirely.
|
|
9
|
+
#
|
|
10
|
+
# This is the mechanism; the merge-time policy of *when* to inject and
|
|
11
|
+
# which criteria the simulated files carry lives in
|
|
12
|
+
# `ResultMerger::UnloadedFiles` — easy names to confuse.
|
|
13
|
+
#
|
|
14
|
+
# Everything arrives as arguments rather than being read from the SimpleCov
|
|
15
|
+
# singleton. The merge step performs this injection on behalf of the processes
|
|
16
|
+
# that contributed to it, and it does not necessarily share their
|
|
17
|
+
# configuration: a standalone `collate` never ran `SimpleCov.start`, so it has
|
|
18
|
+
# no `cover` glob of its own and takes the tracked paths from the resultsets
|
|
19
|
+
# instead. See #1250.
|
|
20
|
+
module UnloadedFileInjector
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# Expand `globs` into absolute paths, relative to `root` rather than
|
|
24
|
+
# `Dir.pwd` — test runners that chdir (or CI scripts that invoke the suite
|
|
25
|
+
# from a subdirectory) would otherwise silently miss files and produce a
|
|
26
|
+
# different set per environment. See #1106.
|
|
27
|
+
# `reject:` are the producing process's path-decidable filters. Paths its
|
|
28
|
+
# own report would exclude must not be recorded as tracked, or a merge that
|
|
29
|
+
# does not share the configuration would simulate them back in. Before
|
|
30
|
+
# injection moved to the merge the producer filtered them out of its own
|
|
31
|
+
# result and they never reached a resultset. A block filter is handed the
|
|
32
|
+
# source file and may consult coverage that does not exist yet, so those
|
|
33
|
+
# still fall to the merging process's filter chain. See #1250.
|
|
34
|
+
def discover(globs, root:, reject: [])
|
|
35
|
+
paths = globs.compact
|
|
36
|
+
.flat_map { |glob| Dir.glob(glob, base: root) }
|
|
37
|
+
.uniq
|
|
38
|
+
.map { |path| File.expand_path(path, root) }
|
|
39
|
+
return paths if reject.empty?
|
|
40
|
+
|
|
41
|
+
paths.reject { |path| rejected?(path, reject) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# `SourceFile` reads source and builds lines lazily, so a path-only filter
|
|
45
|
+
# answers without anything here touching disk.
|
|
46
|
+
def rejected?(path, filters)
|
|
47
|
+
candidate = SourceFile.new(path, NO_COVERAGE_YET, loaded: false)
|
|
48
|
+
filters.any? { |filter| filter.matches?(candidate) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
no_coverage_yet = {"lines" => []} #: Hash[String, Array[untyped]]
|
|
52
|
+
NO_COVERAGE_YET = no_coverage_yet.freeze
|
|
53
|
+
private_constant :NO_COVERAGE_YET
|
|
54
|
+
|
|
55
|
+
# Add simulated coverage for every path `coverage` does not already carry.
|
|
56
|
+
# Paths that are present are left alone, whoever put them there, which is
|
|
57
|
+
# what lets this run over resultsets a previous SimpleCov already injected
|
|
58
|
+
# into without double-counting or overwriting real data.
|
|
59
|
+
#
|
|
60
|
+
# Returns the augmented coverage and the set of paths that were added.
|
|
61
|
+
def call(coverage, paths, synthesize:, lines:)
|
|
62
|
+
injected = Set.new
|
|
63
|
+
augmented = coverage.dup
|
|
64
|
+
|
|
65
|
+
paths.each do |path|
|
|
66
|
+
next if augmented.key?(path)
|
|
67
|
+
|
|
68
|
+
augmented[path] = SimulateCoverage.call(path, synthesize: synthesize, lines: lines)
|
|
69
|
+
injected << path
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
[augmented, injected]
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/simplecov/version.rb
CHANGED