simplecov 0.22.0 → 1.1.1
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/LICENSE +1 -1
- data/README.md +71 -916
- data/exe/simplecov +11 -0
- data/lib/minitest/simplecov_plugin.rb +13 -5
- data/lib/simplecov/atomic_file.rb +70 -0
- data/lib/simplecov/autostart.rb +11 -0
- data/lib/simplecov/cli/clean.rb +87 -0
- data/lib/simplecov/cli/command_helpers.rb +55 -0
- data/lib/simplecov/cli/coverage.rb +86 -0
- data/lib/simplecov/cli/coverage_file.rb +65 -0
- data/lib/simplecov/cli/diff.rb +147 -0
- data/lib/simplecov/cli/dotfile.rb +111 -0
- data/lib/simplecov/cli/merge.rb +120 -0
- data/lib/simplecov/cli/open.rb +48 -0
- data/lib/simplecov/cli/report.rb +97 -0
- data/lib/simplecov/cli/run.rb +36 -0
- 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 +85 -0
- data/lib/simplecov/cli/uncovered.rb +103 -0
- data/lib/simplecov/cli.rb +163 -0
- data/lib/simplecov/color.rb +83 -0
- data/lib/simplecov/combine/branches_combiner.rb +54 -7
- 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 +53 -23
- data/lib/simplecov/combine/methods_combiner.rb +74 -0
- data/lib/simplecov/combine/results_combiner.rb +13 -36
- data/lib/simplecov/combine.rb +5 -23
- data/lib/simplecov/command_guesser.rb +104 -33
- data/lib/simplecov/configuration/coverage.rb +172 -0
- data/lib/simplecov/configuration/coverage_criteria.rb +154 -0
- data/lib/simplecov/configuration/eval_coverage.rb +41 -0
- data/lib/simplecov/configuration/filters.rb +164 -0
- data/lib/simplecov/configuration/formatting.rb +138 -0
- data/lib/simplecov/configuration/groups.rb +42 -0
- data/lib/simplecov/configuration/ignored_entries.rb +63 -0
- data/lib/simplecov/configuration/merging.rb +165 -0
- data/lib/simplecov/configuration/thresholds.rb +179 -0
- data/lib/simplecov/configuration.rb +83 -420
- data/lib/simplecov/coverage_json.rb +24 -0
- data/lib/simplecov/coverage_statistics.rb +16 -10
- data/lib/simplecov/coverage_violations.rb +159 -0
- data/lib/simplecov/defaults.rb +34 -23
- data/lib/simplecov/deprecation.rb +45 -0
- data/lib/simplecov/directive.rb +167 -0
- data/lib/simplecov/exit_codes/check.rb +33 -0
- data/lib/simplecov/exit_codes/exit_code_handling.rb +8 -2
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +18 -65
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +32 -0
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +17 -36
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +30 -0
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +37 -33
- data/lib/simplecov/exit_codes.rb +20 -0
- data/lib/simplecov/exit_handling.rb +128 -0
- data/lib/simplecov/file_list.rb +70 -24
- data/lib/simplecov/filter.rb +105 -26
- data/lib/simplecov/formatter/base.rb +118 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +97 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +87 -0
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +96 -0
- data/lib/simplecov/formatter/html_formatter.rb +105 -0
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +88 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +118 -0
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +93 -0
- data/lib/simplecov/formatter/json_formatter.rb +46 -0
- data/lib/simplecov/formatter/multi_formatter.rb +12 -8
- data/lib/simplecov/formatter/simple_formatter.rb +14 -11
- data/lib/simplecov/formatter.rb +16 -0
- data/lib/simplecov/group_names.rb +32 -0
- data/lib/simplecov/last_run.rb +18 -4
- data/lib/simplecov/lines_classifier.rb +48 -15
- data/lib/simplecov/load_global_config.rb +12 -4
- data/lib/simplecov/parallel_adapters/base.rb +78 -0
- data/lib/simplecov/parallel_adapters/generic.rb +45 -0
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +93 -0
- data/lib/simplecov/parallel_adapters.rb +85 -0
- data/lib/simplecov/parallel_coordination.rb +134 -0
- data/lib/simplecov/parallel_result_merger.rb +230 -0
- data/lib/simplecov/process.rb +52 -12
- data/lib/simplecov/profiles/bundler_filter.rb +1 -1
- data/lib/simplecov/profiles/hidden_filter.rb +1 -1
- data/lib/simplecov/profiles/rails.rb +24 -10
- data/lib/simplecov/profiles/root_filter.rb +6 -5
- data/lib/simplecov/profiles/strict.rb +32 -0
- data/lib/simplecov/profiles/test_frameworks.rb +1 -4
- data/lib/simplecov/profiles.rb +32 -3
- data/lib/simplecov/report_deferral.rb +49 -0
- data/lib/simplecov/report_stamp.rb +28 -0
- data/lib/simplecov/result/missing_source_files_reporter.rb +49 -0
- data/lib/simplecov/result/source_file_builder.rb +51 -0
- data/lib/simplecov/result.rb +145 -26
- data/lib/simplecov/result_adapter.rb +139 -8
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +28 -0
- data/lib/simplecov/result_merger/resultset_file.rb +74 -0
- data/lib/simplecov/result_merger/resultset_run_identity.rb +67 -0
- data/lib/simplecov/result_merger/resultset_store.rb +53 -0
- data/lib/simplecov/result_merger/unloaded_files.rb +103 -0
- data/lib/simplecov/result_merger.rb +109 -114
- data/lib/simplecov/result_processing.rb +218 -0
- data/lib/simplecov/run_identity.rb +77 -0
- data/lib/simplecov/simulate_coverage.rb +83 -12
- data/lib/simplecov/source_file/branch.rb +1 -3
- data/lib/simplecov/source_file/branch_builder.rb +114 -0
- data/lib/simplecov/source_file/builder_context.rb +28 -0
- data/lib/simplecov/source_file/line.rb +9 -4
- data/lib/simplecov/source_file/line_builder.rb +43 -0
- data/lib/simplecov/source_file/method.rb +58 -0
- data/lib/simplecov/source_file/method_builder.rb +60 -0
- data/lib/simplecov/source_file/ruby_data_parser.rb +110 -0
- data/lib/simplecov/source_file/skip_chunks.rb +72 -0
- data/lib/simplecov/source_file/source_loader.rb +79 -0
- data/lib/simplecov/source_file/statistics.rb +65 -0
- data/lib/simplecov/source_file.rb +66 -232
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +239 -0
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +236 -0
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +62 -0
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +55 -0
- data/lib/simplecov/static_coverage_extractor/value_position.rb +74 -0
- data/lib/simplecov/static_coverage_extractor/visitor.rb +183 -0
- data/lib/simplecov/static_coverage_extractor.rb +110 -0
- data/lib/simplecov/unloaded_file_injector.rb +75 -0
- data/lib/simplecov/useless_results_remover.rb +16 -7
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov-html.rb +4 -0
- data/lib/simplecov.rb +185 -380
- data/lib/simplecov_json_formatter.rb +4 -0
- data/sig/simplecov.rbs +1777 -0
- metadata +111 -62
- data/CHANGELOG.md +0 -191
- data/doc/alternate-formatters.md +0 -71
- data/doc/commercial-services.md +0 -25
- data/doc/editor-integration.md +0 -18
- data/lib/simplecov/combine/files_combiner.rb +0 -24
- data/lib/simplecov/default_formatter.rb +0 -20
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module StaticCoverageExtractor
|
|
5
|
+
# Detects the `if` / `unless` / ternary conditions CRuby folds away.
|
|
6
|
+
# When a condition is a statically-known-truthy/falsy literal the
|
|
7
|
+
# compiler eliminates the dead arm and Coverage emits NO branch, so the
|
|
8
|
+
# extractor must not synthesize one either — otherwise the arm is a
|
|
9
|
+
# phantom that no loaded run can ever hit, the same unmergeable-tuple
|
|
10
|
+
# failure mode as #1226 / #1233.
|
|
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
|
+
|
|
30
|
+
# Prism node types for the literals that fold. `while` / `until` do
|
|
31
|
+
# NOT fold (`while true` is a real branch), so only the if-like
|
|
32
|
+
# visitors consult this. Regexp and Range literals are excluded on
|
|
33
|
+
# purpose: as conditions they mean `=~ $_` / flip-flop, which
|
|
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
|
|
39
|
+
STATIC_CONDITION_TYPES = [
|
|
40
|
+
::Prism::IntegerNode, ::Prism::FloatNode, ::Prism::RationalNode,
|
|
41
|
+
::Prism::ImaginaryNode, ::Prism::SymbolNode, ::Prism::StringNode,
|
|
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)
|
|
89
|
+
].freeze
|
|
90
|
+
# simplecov:enable branch
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
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
|
|
157
|
+
end
|
|
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.
|
|
164
|
+
def unwrap_parentheses(node)
|
|
165
|
+
# @type var current: untyped
|
|
166
|
+
current = node
|
|
167
|
+
while current.is_a?(::Prism::ParenthesesNode)
|
|
168
|
+
body = current.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) }
|
|
173
|
+
|
|
174
|
+
current = statements.last
|
|
175
|
+
end
|
|
176
|
+
current
|
|
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
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "prism_compat"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module StaticCoverageExtractor
|
|
7
|
+
# The source ranges Ruby's Coverage assigns to branch conditions and
|
|
8
|
+
# arms, resolved from Prism nodes. Simulated entries only ever merge
|
|
9
|
+
# with real entries produced by the running Ruby, and CRuby 3.4
|
|
10
|
+
# changed several of these conventions, so every resolver here emits
|
|
11
|
+
# whichever shape this Ruby's Coverage uses. See issues #1226 / #1233.
|
|
12
|
+
#
|
|
13
|
+
# rubocop:disable Metrics/ModuleLength -- one cohesive home for the
|
|
14
|
+
# per-construct, per-Ruby-version Coverage location conventions;
|
|
15
|
+
# splitting it would scatter closely-related resolvers.
|
|
16
|
+
module LocationConventions
|
|
17
|
+
LEGACY_COVERAGE_LOCATIONS = Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.4")
|
|
18
|
+
|
|
19
|
+
# A zero-width stand-in for Prism locations, for the arms Coverage
|
|
20
|
+
# anchors to a point rather than a range.
|
|
21
|
+
PointLocation = Data.define(:start_line, :start_column, :end_line, :end_column)
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# simplecov:disable
|
|
26
|
+
# Which arm of each conditional below runs is fixed by the running
|
|
27
|
+
# Ruby's version, so no single process can cover both sides, and
|
|
28
|
+
# the legacy-only helpers are unreachable on modern Rubies (and
|
|
29
|
+
# vice versa). The "runtime tuple equivalence" spec exercises this
|
|
30
|
+
# module against real Coverage output on every CI Ruby.
|
|
31
|
+
|
|
32
|
+
# The range Coverage assigns to an if-like node itself. Modern
|
|
33
|
+
# CRuby uses the node's full source range for every form; 3.2/3.3
|
|
34
|
+
# end an `elsif` clause's range at its last content instead of the
|
|
35
|
+
# shared `end` keyword the clause doesn't own.
|
|
36
|
+
def if_like_location(node, type)
|
|
37
|
+
return node.location unless LEGACY_COVERAGE_LOCATIONS && type == :if && elsif_node?(node)
|
|
38
|
+
|
|
39
|
+
span(node.location, legacy_content_end(node))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def elsif_node?(node)
|
|
43
|
+
keyword = node.if_keyword_loc
|
|
44
|
+
!keyword.nil? && keyword.slice == "elsif"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Where an if/elsif chain's content ends, for the legacy range
|
|
48
|
+
# convention: the deepest trailing clause's statements, or that
|
|
49
|
+
# clause's predicate / `else` keyword when its body is empty.
|
|
50
|
+
def legacy_content_end(node)
|
|
51
|
+
tail = node
|
|
52
|
+
while tail.is_a?(::Prism::IfNode)
|
|
53
|
+
sub = PrismCompat.subsequent(tail)
|
|
54
|
+
break unless sub
|
|
55
|
+
|
|
56
|
+
tail = sub
|
|
57
|
+
end
|
|
58
|
+
return (tail.statements || tail.predicate).location if tail.is_a?(::Prism::IfNode)
|
|
59
|
+
|
|
60
|
+
tail.statements ? tail.statements.location : tail.else_keyword_loc
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Location of the then arm. Coverage uses the body statements'
|
|
64
|
+
# range; with an empty then body the arm collapses to a zero-width
|
|
65
|
+
# point at the predicate's end — always on a modern `if`, and on
|
|
66
|
+
# legacy Rubies only when the construct is in void position (a
|
|
67
|
+
# trailing statement discards its value). In value (tail) position,
|
|
68
|
+
# legacy Rubies and `unless` fall back to the node's range.
|
|
69
|
+
def if_like_then_location(node, type)
|
|
70
|
+
return node.statements.location if node.statements
|
|
71
|
+
return point_at_end(node.predicate.location) if empty_arm_collapses?(node, type)
|
|
72
|
+
|
|
73
|
+
if_like_location(node, type)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Resolve the source range Coverage attributes to a real-or-synthetic
|
|
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).
|
|
81
|
+
def if_like_else_location(node, type)
|
|
82
|
+
sub = PrismCompat.subsequent(node)
|
|
83
|
+
return if_like_location(node, type) unless sub
|
|
84
|
+
# An `elsif` arrives as a nested IfNode. Coverage attributes the
|
|
85
|
+
# outer else arm to the clause's own range, not its then body
|
|
86
|
+
# (which is what created phantom unmergeable arms).
|
|
87
|
+
return if_like_location(sub, :if) if sub.is_a?(::Prism::IfNode)
|
|
88
|
+
return sub.statements.location if sub.statements
|
|
89
|
+
|
|
90
|
+
empty_else_location(node, sub, type)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Location of an empty explicit `else`: a modern `if` uses the
|
|
94
|
+
# else..end span; a legacy Ruby in void position collapses to a point
|
|
95
|
+
# at the `else` keyword's end; otherwise (legacy value position, or
|
|
96
|
+
# `unless`) it uses the condition's range.
|
|
97
|
+
def empty_else_location(node, sub, type)
|
|
98
|
+
return sub.location if !LEGACY_COVERAGE_LOCATIONS && type == :if
|
|
99
|
+
return point_at_end(sub.else_keyword_loc) if LEGACY_COVERAGE_LOCATIONS && !value_position?(node)
|
|
100
|
+
|
|
101
|
+
if_like_location(node, type)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Arm location for a when/in clause: its body statements, or — when
|
|
105
|
+
# the body is empty — the clause's own range on modern Rubies, a
|
|
106
|
+
# point at the pattern's end for a legacy `in`, and for a legacy
|
|
107
|
+
# `when` a point at the clause's end in void position or the tail
|
|
108
|
+
# convention (keyword through the case's remaining content) in value.
|
|
109
|
+
def case_arm_location(case_node, when_node, when_type)
|
|
110
|
+
return when_node.statements.location if when_node.statements
|
|
111
|
+
return when_node.location unless LEGACY_COVERAGE_LOCATIONS
|
|
112
|
+
return point_at_end(when_node.pattern.location) if when_type == :in
|
|
113
|
+
return point_at_end(when_node.location) unless value_position?(case_node)
|
|
114
|
+
|
|
115
|
+
legacy_when_value_location(case_node, when_node)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def legacy_when_value_location(case_node, when_node)
|
|
119
|
+
span(when_node.location, legacy_case_tail_end(case_node, when_node))
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# The last body content in the case after `when_node`, falling
|
|
123
|
+
# back to the clause's final condition value.
|
|
124
|
+
def legacy_case_tail_end(case_node, when_node)
|
|
125
|
+
following_case_content(case_node, when_node).last ||
|
|
126
|
+
(when_node.conditions.last || when_node).location
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def following_case_content(case_node, when_node)
|
|
130
|
+
clauses = case_node.conditions
|
|
131
|
+
index = clauses.index { |clause| clause.equal?(when_node) } || 0
|
|
132
|
+
# A when-clause's own location ends where its body ends (or at its
|
|
133
|
+
# condition when empty), so the whole clause extends the range
|
|
134
|
+
# through trailing EMPTY clauses that have no `statements`.
|
|
135
|
+
content = clauses.drop(index + 1).map(&:location)
|
|
136
|
+
else_statements = PrismCompat.else_clause(case_node)&.statements
|
|
137
|
+
content << else_statements.location if else_statements
|
|
138
|
+
content
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Resolve the source range Coverage attributes to a synthetic-or-real
|
|
142
|
+
# `:else` arm of a case construct: the body of an explicit else,
|
|
143
|
+
# the case's full range when no else is present, and — for an
|
|
144
|
+
# explicit else with an empty body — the else..end span on modern
|
|
145
|
+
# Rubies or the case's full range on legacy ones.
|
|
146
|
+
def else_arm_location(node)
|
|
147
|
+
else_clause = PrismCompat.else_clause(node)
|
|
148
|
+
return node.location unless else_clause
|
|
149
|
+
return else_clause.statements.location if else_clause.statements
|
|
150
|
+
return else_clause.location unless LEGACY_COVERAGE_LOCATIONS
|
|
151
|
+
# Empty explicit `else`: a point at the `else` keyword's end in void
|
|
152
|
+
# position, the whole case's range in value position.
|
|
153
|
+
return point_at_end(else_clause.else_keyword_loc) unless value_position?(node)
|
|
154
|
+
|
|
155
|
+
node.location
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# An empty loop body falls back to the loop's range on modern
|
|
159
|
+
# Rubies and collapses to a point at the predicate's end on legacy
|
|
160
|
+
# ones.
|
|
161
|
+
def loop_body_location(node)
|
|
162
|
+
return legacy_do_while_body_location(node) if LEGACY_COVERAGE_LOCATIONS && begin_modifier_loop?(node)
|
|
163
|
+
return node.statements.location if node.statements
|
|
164
|
+
return point_at_end(node.predicate.location) if LEGACY_COVERAGE_LOCATIONS
|
|
165
|
+
|
|
166
|
+
node.location
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# `begin ... end while/until cond` (the do-while form) parses as a
|
|
170
|
+
# while/until whose sole statement is the BeginNode. Modern Coverage
|
|
171
|
+
# attributes the body to that whole `begin ... end` span (which the
|
|
172
|
+
# generic `node.statements.location` already yields), but 3.3 uses
|
|
173
|
+
# the begin's inner statements instead — or a point at the end of
|
|
174
|
+
# the `begin` keyword when the body is empty.
|
|
175
|
+
def begin_modifier_loop?(node)
|
|
176
|
+
node.respond_to?(:begin_modifier?) && node.begin_modifier?
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def legacy_do_while_body_location(node)
|
|
180
|
+
begin_node = node.statements.body.first
|
|
181
|
+
inner = begin_node.statements
|
|
182
|
+
inner ? inner.location : point_at_end(begin_node.begin_keyword_loc)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Coverage's safe-navigation branch spans the receiver through the
|
|
186
|
+
# end of the call's arguments (or just the message when there are
|
|
187
|
+
# none), but never includes a trailing block: `x&.foo { ... }` and
|
|
188
|
+
# `x&.foo(1) { ... }` both end exactly where `x&.foo` / `x&.foo(1)`
|
|
189
|
+
# would without the block. `node.location` includes an attached
|
|
190
|
+
# block, so build the end position from `closing_loc` (closing
|
|
191
|
+
# paren) / `arguments` (paren-less args) / `message_loc` instead.
|
|
192
|
+
# This convention is the same on legacy and modern Rubies. See
|
|
193
|
+
# issue #1233.
|
|
194
|
+
def safe_navigation_location(node)
|
|
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)
|
|
200
|
+
PointLocation.new(
|
|
201
|
+
start_line: from.start_line, start_column: from.start_column,
|
|
202
|
+
end_line: to.end_line, end_column: to.end_column
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def point_at_end(location)
|
|
207
|
+
PointLocation.new(
|
|
208
|
+
start_line: location.end_line, start_column: location.end_column,
|
|
209
|
+
end_line: location.end_line, end_column: location.end_column
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Whether an empty then arm collapses to a point at the predicate's
|
|
214
|
+
# end. Modern Coverage does this for every `if` (but not `unless`);
|
|
215
|
+
# legacy Coverage does it only in void position, for both.
|
|
216
|
+
def empty_arm_collapses?(node, type)
|
|
217
|
+
return type == :if unless LEGACY_COVERAGE_LOCATIONS
|
|
218
|
+
|
|
219
|
+
!value_position?(node)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Whether `node` sits in value (method-return) position, which on
|
|
223
|
+
# legacy Rubies keeps an empty arm's range instead of collapsing it
|
|
224
|
+
# to a point. `@value_positions` is computed once per parse by
|
|
225
|
+
# ValuePositions (only on legacy; nil elsewhere, which reads as
|
|
226
|
+
# "value" — the safe, pre-audit default).
|
|
227
|
+
def value_position?(node)
|
|
228
|
+
return true if @value_positions.nil?
|
|
229
|
+
|
|
230
|
+
@value_positions.key?(node)
|
|
231
|
+
end
|
|
232
|
+
# simplecov:enable
|
|
233
|
+
end
|
|
234
|
+
# rubocop:enable Metrics/ModuleLength
|
|
235
|
+
end
|
|
236
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module StaticCoverageExtractor
|
|
5
|
+
# Visitor mixin that collects method tuples and tracks the lexical
|
|
6
|
+
# class / module nesting that names them, in the shape Ruby's
|
|
7
|
+
# `Coverage` reports methods. Mixed into `Visitor`, it shares that
|
|
8
|
+
# visitor's `@methods` / `@class_stack` state and keeps the
|
|
9
|
+
# method-collection concern separate from branch extraction.
|
|
10
|
+
module MethodCollector
|
|
11
|
+
# Track class/module nesting so method tuples carry the lexical
|
|
12
|
+
# class name. Module + Class are both treated as namespaces here
|
|
13
|
+
# since `Coverage` reports both as the constant.
|
|
14
|
+
def visit_class_node(node)
|
|
15
|
+
with_class(constant_name(node.constant_path)) { super }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def visit_module_node(node)
|
|
19
|
+
with_class(constant_name(node.constant_path)) { super }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# `def name(...)` and `def self.name(...)` both produce DefNode.
|
|
23
|
+
# The class context is the surrounding lexical class/module (or
|
|
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).
|
|
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
|
|
33
|
+
loc = node.location
|
|
34
|
+
class_name = @class_stack.last || "Object"
|
|
35
|
+
key = [class_name, node.name, loc.start_line, loc.start_column, loc.end_line, loc.end_column]
|
|
36
|
+
@methods[key] = 0
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# Render a constant path (e.g., `Foo::Bar`) as its source-form
|
|
43
|
+
# string. Defensive nil / to_s fallbacks: ClassNode and ModuleNode
|
|
44
|
+
# always carry a constant_path in practice.
|
|
45
|
+
# simplecov:disable
|
|
46
|
+
def constant_name(node)
|
|
47
|
+
return "<anonymous>" if node.nil?
|
|
48
|
+
return node.slice if node.respond_to?(:slice)
|
|
49
|
+
|
|
50
|
+
node.to_s
|
|
51
|
+
end
|
|
52
|
+
# simplecov:enable
|
|
53
|
+
|
|
54
|
+
def with_class(name)
|
|
55
|
+
@class_stack.push(name)
|
|
56
|
+
yield
|
|
57
|
+
ensure
|
|
58
|
+
@class_stack.pop
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -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
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "prism_compat"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module StaticCoverageExtractor
|
|
7
|
+
# Ruby 3.3 value-position analysis for the extractor's legacy branch
|
|
8
|
+
# conventions (see LocationConventions and the #1233 audit).
|
|
9
|
+
#
|
|
10
|
+
# On Ruby 3.3, the source range Coverage assigns to an EMPTY branch arm
|
|
11
|
+
# depends on whether its construct is in value position — its result is
|
|
12
|
+
# the method's return value — or void position, where the result is
|
|
13
|
+
# discarded. Value position keeps the whole-construct range; void
|
|
14
|
+
# collapses the arm to a point at its header's end. Ruby 3.4 dropped the
|
|
15
|
+
# distinction, so this pass only runs on legacy Rubies.
|
|
16
|
+
#
|
|
17
|
+
# "Value position" here is narrower than general value-use: it is
|
|
18
|
+
# strictly method-return (tail) position. It reaches a node only through
|
|
19
|
+
# statement tails and `if`/`unless`/`when` arms. Assignments, blocks,
|
|
20
|
+
# lambdas, method arguments, `case/in` arms, and loop bodies all discard
|
|
21
|
+
# it (Coverage treats their empty arms as void). So `tail_children`
|
|
22
|
+
# names the constructs that forward tail position and everything else
|
|
23
|
+
# falls through to the void default.
|
|
24
|
+
module ValuePositions
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# simplecov:disable
|
|
28
|
+
# This whole pass runs only on legacy Rubies (the modern dogfood
|
|
29
|
+
# never calls it), so its lines can't be covered on the CI Ruby that
|
|
30
|
+
# enforces 100%. Behavior is pinned instead by the differential
|
|
31
|
+
# tuple-equivalence spec, which runs against real Coverage on 3.3.
|
|
32
|
+
|
|
33
|
+
# An identity set (a `compare_by_identity` Hash used as a set) of the
|
|
34
|
+
# Prism nodes Coverage treats as being in value position.
|
|
35
|
+
def call(root)
|
|
36
|
+
positions = {} #: Hash[untyped, bool]
|
|
37
|
+
positions.compare_by_identity
|
|
38
|
+
mark(root, true, positions)
|
|
39
|
+
positions
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def mark(node, in_value, positions)
|
|
43
|
+
return unless node.is_a?(::Prism::Node)
|
|
44
|
+
|
|
45
|
+
positions[node] = true if in_value
|
|
46
|
+
children = tail_children(node, in_value)
|
|
47
|
+
node.compact_child_nodes.each do |child|
|
|
48
|
+
mark(child, children.any? { |c| c.equal?(child) }, positions)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The children of `node` that inherit its tail position; empty for the
|
|
53
|
+
# void default. A method body is a tail context even when the `def`
|
|
54
|
+
# itself is not (the method still returns its last expression), so it
|
|
55
|
+
# is included regardless of `in_value`.
|
|
56
|
+
def tail_children(node, in_value)
|
|
57
|
+
# A method body is a tail context even when the `def` is not.
|
|
58
|
+
return [node.body] if node.is_a?(::Prism::DefNode)
|
|
59
|
+
return [] unless in_value
|
|
60
|
+
|
|
61
|
+
# `case/in` (CaseMatchNode) is intentionally not a tail construct:
|
|
62
|
+
# its `in` arms and `else` both discard tail position.
|
|
63
|
+
case node
|
|
64
|
+
when ::Prism::StatementsNode then [node.body.last]
|
|
65
|
+
when ::Prism::IfNode, ::Prism::UnlessNode then [node.statements, PrismCompat.subsequent(node)]
|
|
66
|
+
when ::Prism::CaseNode then [*node.conditions, PrismCompat.else_clause(node)]
|
|
67
|
+
when ::Prism::ElseNode, ::Prism::WhenNode, ::Prism::BeginNode, ::Prism::ProgramNode then [node.statements]
|
|
68
|
+
else []
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
# simplecov:enable
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|