simplecov 1.1.1 → 1.3.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 +148 -5
- data/exe/simplecov +0 -3
- data/lib/minitest/simplecov_plugin.rb +12 -9
- data/lib/simplecov/atomic_file.rb +16 -19
- data/lib/simplecov/autostart.rb +5 -7
- data/lib/simplecov/baseline/parser.rb +58 -0
- data/lib/simplecov/baseline.rb +170 -0
- data/lib/simplecov/cli/affected/changed_files.rb +57 -0
- data/lib/simplecov/cli/affected/selection.rb +117 -0
- data/lib/simplecov/cli/affected.rb +148 -0
- data/lib/simplecov/cli/annotations.rb +166 -0
- data/lib/simplecov/cli/badge/svg.rb +70 -0
- data/lib/simplecov/cli/badge.rb +62 -0
- data/lib/simplecov/cli/clean.rb +23 -24
- data/lib/simplecov/cli/command_helpers.rb +55 -19
- data/lib/simplecov/cli/completions/scripts.rb +114 -0
- data/lib/simplecov/cli/completions.rb +83 -0
- data/lib/simplecov/cli/coverage.rb +25 -28
- data/lib/simplecov/cli/coverage_file.rb +58 -14
- data/lib/simplecov/cli/dead_code/output.rb +100 -0
- data/lib/simplecov/cli/dead_code.rb +139 -0
- data/lib/simplecov/cli/diff/output.rb +45 -0
- data/lib/simplecov/cli/diff.rb +35 -67
- data/lib/simplecov/cli/dotfile.rb +83 -38
- data/lib/simplecov/cli/git.rb +41 -0
- data/lib/simplecov/cli/history/output.rb +135 -0
- data/lib/simplecov/cli/history.rb +67 -0
- data/lib/simplecov/cli/merge.rb +34 -39
- data/lib/simplecov/cli/open.rb +12 -15
- data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
- data/lib/simplecov/cli/patch/output.rb +151 -0
- data/lib/simplecov/cli/patch.rb +175 -0
- data/lib/simplecov/cli/ratchet/output.rb +44 -0
- data/lib/simplecov/cli/ratchet.rb +83 -0
- data/lib/simplecov/cli/report.rb +18 -24
- data/lib/simplecov/cli/run.rb +8 -9
- data/lib/simplecov/cli/serve/report_preparer.rb +7 -6
- data/lib/simplecov/cli/serve/static_file_handler.rb +62 -56
- data/lib/simplecov/cli/serve.rb +27 -24
- data/lib/simplecov/cli/show/annotator.rb +87 -0
- data/lib/simplecov/cli/show/sweep.rb +36 -0
- data/lib/simplecov/cli/show.rb +133 -0
- data/lib/simplecov/cli/status/facts.rb +87 -0
- data/lib/simplecov/cli/status.rb +93 -0
- data/lib/simplecov/cli/tests/redundancy.rb +78 -0
- data/lib/simplecov/cli/tests.rb +148 -0
- data/lib/simplecov/cli/uncovered/misses.rb +33 -0
- data/lib/simplecov/cli/uncovered.rb +58 -34
- data/lib/simplecov/cli/usage.rb +153 -0
- data/lib/simplecov/cli/watch/live_report.rb +73 -0
- data/lib/simplecov/cli/watch/narrator.rb +55 -0
- data/lib/simplecov/cli/watch/poller.rb +45 -0
- data/lib/simplecov/cli/watch/session.rb +134 -0
- data/lib/simplecov/cli/watch/test_plan.rb +43 -0
- data/lib/simplecov/cli/watch.rb +85 -0
- data/lib/simplecov/cli.rb +43 -92
- data/lib/simplecov/color.rb +19 -37
- data/lib/simplecov/combine/branches_combiner.rb +7 -31
- data/lib/simplecov/combine/coverage_accumulator.rb +49 -142
- data/lib/simplecov/combine/identity_interner.rb +10 -13
- data/lib/simplecov/combine/interned_counts.rb +7 -9
- data/lib/simplecov/combine/lines_combiner.rb +20 -44
- data/lib/simplecov/combine/methods_combiner.rb +9 -35
- data/lib/simplecov/combine/results_combiner.rb +4 -21
- data/lib/simplecov/combine.rb +0 -6
- data/lib/simplecov/command_guesser.rb +20 -34
- data/lib/simplecov/configuration/baseline.rb +31 -0
- data/lib/simplecov/configuration/coverage.rb +91 -79
- data/lib/simplecov/configuration/coverage_criteria.rb +43 -54
- data/lib/simplecov/configuration/deprecations.rb +21 -0
- data/lib/simplecov/configuration/eval_coverage.rb +7 -11
- data/lib/simplecov/configuration/filters.rb +40 -73
- data/lib/simplecov/configuration/formatting.rb +74 -83
- data/lib/simplecov/configuration/groups.rb +19 -13
- data/lib/simplecov/configuration/history.rb +44 -0
- data/lib/simplecov/configuration/ignored_entries.rb +31 -22
- data/lib/simplecov/configuration/merging.rb +94 -62
- data/lib/simplecov/configuration/missed_caps.rb +51 -0
- data/lib/simplecov/configuration/production.rb +23 -0
- data/lib/simplecov/configuration/test_tracking.rb +48 -0
- data/lib/simplecov/configuration/thresholds.rb +33 -62
- data/lib/simplecov/configuration/view_coverage.rb +37 -0
- data/lib/simplecov/configuration.rb +70 -76
- data/lib/simplecov/configuration_error.rb +5 -0
- data/lib/simplecov/context_map/union.rb +81 -0
- data/lib/simplecov/context_map.rb +187 -0
- data/lib/simplecov/coverage_json.rb +2 -5
- data/lib/simplecov/coverage_statistics.rb +14 -24
- data/lib/simplecov/coverage_violations.rb +160 -65
- data/lib/simplecov/current_run.rb +61 -0
- data/lib/simplecov/defaults.rb +4 -23
- data/lib/simplecov/deprecation.rb +45 -27
- data/lib/simplecov/directive/erb.rb +44 -0
- data/lib/simplecov/directive/haml.rb +33 -0
- data/lib/simplecov/directive/indented_template.rb +37 -0
- data/lib/simplecov/directive/slim.rb +35 -0
- data/lib/simplecov/directive/template.rb +29 -0
- data/lib/simplecov/directive.rb +47 -62
- data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
- data/lib/simplecov/exit_codes/check.rb +17 -11
- data/lib/simplecov/exit_codes/exit_code_handling.rb +21 -8
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +7 -10
- data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
- data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +7 -12
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +11 -10
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +7 -9
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +16 -17
- data/lib/simplecov/exit_codes.rb +16 -10
- data/lib/simplecov/exit_handling.rb +94 -59
- data/lib/simplecov/file_list.rb +20 -51
- data/lib/simplecov/filter.rb +43 -66
- data/lib/simplecov/formatter/base.rb +37 -55
- data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +29 -31
- data/lib/simplecov/formatter/html_formatter/public/index.html +15 -9
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +74 -17
- data/lib/simplecov/formatter/html_formatter.rb +24 -41
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +50 -16
- data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +43 -29
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +14 -10
- data/lib/simplecov/formatter/json_formatter.rb +7 -13
- data/lib/simplecov/formatter/multi_formatter.rb +5 -11
- data/lib/simplecov/formatter/simple_formatter.rb +2 -8
- data/lib/simplecov/formatter.rb +14 -10
- data/lib/simplecov/group_names.rb +13 -12
- data/lib/simplecov/history.rb +115 -0
- data/lib/simplecov/last_run.rb +6 -9
- data/lib/simplecov/lines_classifier.rb +31 -16
- data/lib/simplecov/load_global_config.rb +7 -7
- data/lib/simplecov/parallel_adapters/base.rb +13 -41
- data/lib/simplecov/parallel_adapters/generic.rb +7 -19
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
- data/lib/simplecov/parallel_adapters.rb +24 -55
- data/lib/simplecov/parallel_coordination.rb +39 -49
- data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
- data/lib/simplecov/parallel_result_merger.rb +67 -126
- data/lib/simplecov/process.rb +14 -32
- data/lib/simplecov/production/error.rb +11 -0
- data/lib/simplecov/production/file_sink.rb +130 -0
- data/lib/simplecov/production.rb +249 -0
- data/lib/simplecov/profiles/rails.rb +4 -15
- data/lib/simplecov/profiles/root_filter.rb +0 -4
- data/lib/simplecov/profiles/strict.rb +11 -21
- data/lib/simplecov/profiles.rb +11 -33
- data/lib/simplecov/report_deferral.rb +11 -15
- data/lib/simplecov/report_stamp.rb +6 -9
- data/lib/simplecov/result/filter_config.rb +19 -0
- data/lib/simplecov/result/missing_source_files_reporter.rb +8 -9
- data/lib/simplecov/result/serialization.rb +43 -0
- data/lib/simplecov/result/source_file_builder.rb +7 -12
- data/lib/simplecov/result.rb +74 -117
- data/lib/simplecov/result_adapter.rb +56 -68
- data/lib/simplecov/result_merger/contexts.rb +22 -0
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +4 -8
- data/lib/simplecov/result_merger/resultset_file.rb +20 -27
- data/lib/simplecov/result_merger/resultset_run_identity.rb +23 -25
- data/lib/simplecov/result_merger/resultset_store.rb +23 -14
- data/lib/simplecov/result_merger/unloaded_files.rb +24 -32
- data/lib/simplecov/result_merger.rb +59 -58
- data/lib/simplecov/result_processing.rb +92 -122
- data/lib/simplecov/run_identity.rb +16 -18
- data/lib/simplecov/simulate_coverage.rb +34 -50
- data/lib/simplecov/source_file/branch.rb +8 -33
- data/lib/simplecov/source_file/branch_builder.rb +28 -47
- data/lib/simplecov/source_file/builder_context.rb +13 -13
- data/lib/simplecov/source_file/line.rb +13 -33
- data/lib/simplecov/source_file/line_builder.rb +9 -15
- data/lib/simplecov/source_file/method.rb +3 -7
- data/lib/simplecov/source_file/method_builder.rb +6 -15
- data/lib/simplecov/source_file/ruby_data_parser.rb +59 -66
- data/lib/simplecov/source_file/skip_chunks.rb +29 -29
- data/lib/simplecov/source_file/source_loader.rb +45 -35
- data/lib/simplecov/source_file/statistics.rb +20 -9
- data/lib/simplecov/source_file.rb +21 -51
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +55 -139
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +9 -26
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +15 -24
- data/lib/simplecov/static_coverage_extractor/value_position.rb +25 -38
- data/lib/simplecov/static_coverage_extractor/visitor.rb +45 -65
- data/lib/simplecov/static_coverage_extractor.rb +43 -75
- data/lib/simplecov/test_tracker/accessors.rb +34 -0
- data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
- data/lib/simplecov/test_tracker/delta.rb +69 -0
- data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
- data/lib/simplecov/test_tracker.rb +162 -0
- data/lib/simplecov/unloaded_file_injector.rb +18 -31
- data/lib/simplecov/useless_results_remover.rb +7 -6
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
- data/lib/simplecov/view_coverage.rb +55 -0
- data/lib/simplecov-html.rb +1 -0
- data/lib/simplecov.rb +57 -113
- data/lib/simplecov_json_formatter.rb +1 -0
- data/man/simplecov.1 +354 -0
- data/sig/simplecov.rbs +528 -595
- metadata +79 -6
|
@@ -2,39 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
module StaticCoverageExtractor
|
|
5
|
-
# Detects the `if` / `unless` / ternary conditions CRuby folds away.
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# failure mode as #1226 / #1233.
|
|
5
|
+
# Detects the `if` / `unless` / ternary conditions CRuby folds away. When a
|
|
6
|
+
# condition is a statically-known-truthy/falsy literal the compiler
|
|
7
|
+
# eliminates the dead arm and Coverage emits no branch, so the extractor
|
|
8
|
+
# must not synthesize one either: the arm would be a phantom no loaded run
|
|
9
|
+
# can hit, the same unmergeable-tuple failure as #1226 / #1233.
|
|
11
10
|
module ConditionFolding
|
|
12
|
-
# CRuby 3.4 rebuilt the fold on the Prism compiler, and the
|
|
13
|
-
#
|
|
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.
|
|
11
|
+
# CRuby 3.4 rebuilt the fold on the Prism compiler, and the parse.y fold
|
|
12
|
+
# it replaced still differs on 3.3, where `__FILE__` folds.
|
|
20
13
|
FOLDS_SOURCE_FILE = Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.4")
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
#
|
|
24
|
-
#
|
|
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?`.
|
|
14
|
+
# Container literals in discarded position are eliminated from 3.3 on, but
|
|
15
|
+
# 3.3's compile.c elides a container whose contents are merely
|
|
16
|
+
# effect-free (`[x]`), while the Prism compiler demands fully static
|
|
17
|
+
# literals (`[1]` goes, `[x]` stays).
|
|
28
18
|
CONTAINER_CONTENTS_NEED_STATIC_LITERALS = !FOLDS_SOURCE_FILE
|
|
29
19
|
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
# the compiler only folds what it can prove at compile time, and a
|
|
37
|
-
# method named `lambda` proves nothing.
|
|
20
|
+
# The literals that fold. `while` / `until` do NOT fold (`while true` is a
|
|
21
|
+
# real branch), so only the if-like visitors consult this. Regexp and
|
|
22
|
+
# Range literals are excluded on purpose: as conditions they mean
|
|
23
|
+
# `=~ $_` / flip-flop, which Coverage does branch on. `[]`, `{}`, and
|
|
24
|
+
# interpolated strings do not fold either, and `->` folds while a
|
|
25
|
+
# `lambda` call does not: a method named `lambda` proves nothing.
|
|
38
26
|
# simplecov:disable branch — which arm runs is fixed by the running Ruby's version
|
|
39
27
|
STATIC_CONDITION_TYPES = [
|
|
40
28
|
::Prism::IntegerNode, ::Prism::FloatNode, ::Prism::RationalNode,
|
|
@@ -45,27 +33,18 @@ module SimpleCov
|
|
|
45
33
|
].freeze
|
|
46
34
|
# simplecov:enable branch
|
|
47
35
|
|
|
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
36
|
FALSY_CONDITION_TYPES = [::Prism::FalseNode, ::Prism::NilNode].freeze
|
|
52
37
|
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
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.
|
|
38
|
+
# CRuby folds `if nil`, `if "x"`, and `if -> {}` but keeps a real branch
|
|
39
|
+
# for `if (nil)`, `if ("x")`, and `if (-> {})`, while every other literal
|
|
40
|
+
# folds parenthesized or not.
|
|
60
41
|
PAREN_OPAQUE_TYPES = [
|
|
61
42
|
::Prism::NilNode, ::Prism::StringNode, ::Prism::LambdaNode, ::Prism::SourceFileNode
|
|
62
43
|
].freeze
|
|
63
44
|
|
|
64
|
-
#
|
|
65
|
-
# multi-statement paren condition (`if (1; 2)`) folds by its last
|
|
45
|
+
# A multi-statement paren condition (`if (1; 2)`) folds by its last
|
|
66
46
|
# expression only when every leading statement is eliminated when
|
|
67
|
-
# discarded, and these
|
|
68
|
-
# always are. Pinned against real Coverage on 3.2 through 4.0.
|
|
47
|
+
# discarded, and these always are, bare or composing an Array/Hash/Range.
|
|
69
48
|
STATIC_LITERAL_LEAF_TYPES = [
|
|
70
49
|
::Prism::IntegerNode, ::Prism::FloatNode, ::Prism::RationalNode,
|
|
71
50
|
::Prism::ImaginaryNode, ::Prism::StringNode, ::Prism::SymbolNode,
|
|
@@ -73,100 +52,53 @@ module SimpleCov
|
|
|
73
52
|
::Prism::SourceLineNode, ::Prism::SourceFileNode, ::Prism::SourceEncodingNode, ::Prism::RegularExpressionNode
|
|
74
53
|
].freeze
|
|
75
54
|
|
|
76
|
-
# Non-literal reads that are also eliminated when discarded.
|
|
77
|
-
#
|
|
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
|
|
55
|
+
# Non-literal reads that are also eliminated when discarded. Anything that
|
|
56
|
+
# can raise or run hooks is never eliminated and keeps the branch real.
|
|
86
57
|
ELIMINABLE_READ_TYPES = [
|
|
87
|
-
::Prism::SelfNode,
|
|
88
|
-
|
|
58
|
+
::Prism::SelfNode, ::Prism::LocalVariableReadNode,
|
|
59
|
+
::Prism::InstanceVariableReadNode, ::Prism::DefinedNode
|
|
89
60
|
].freeze
|
|
90
|
-
# simplecov:enable branch
|
|
91
61
|
|
|
92
|
-
|
|
62
|
+
private
|
|
93
63
|
|
|
94
|
-
#
|
|
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.
|
|
64
|
+
# `visit` is nil-safe, so a missing arm just visits nothing.
|
|
101
65
|
def visit_folded_arms(verdict, truthy_arm, falsy_arm)
|
|
102
|
-
|
|
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
|
|
66
|
+
visit(verdict.equal?(:truthy) ? truthy_arm : falsy_arm)
|
|
107
67
|
end
|
|
108
68
|
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
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.
|
|
69
|
+
# `:truthy` or `:falsy` when the compiler folds, nil when it doesn't. The
|
|
70
|
+
# compiler eliminates the losing arm's entire subtree, so everything
|
|
71
|
+
# inside it must go unvisited too, not just the folded condition's tuple.
|
|
129
72
|
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
#
|
|
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.
|
|
73
|
+
# Compound forms (`!true`, `true || x`) are deliberately not folded: `!`
|
|
74
|
+
# never folds, and `||` / `&&` constant-propagation diverges across Ruby
|
|
75
|
+
# versions, so matching it would trade a rare gain for real risk.
|
|
136
76
|
def folded_condition(node)
|
|
137
77
|
unwrapped = unwrap_parentheses(node)
|
|
138
78
|
return nil unless foldable?(node, unwrapped)
|
|
139
79
|
|
|
140
|
-
FALSY_CONDITION_TYPES.any? { |type| unwrapped.
|
|
80
|
+
(FALSY_CONDITION_TYPES.any? { |type| unwrapped.instance_of?(type) }) ? :falsy : :truthy
|
|
141
81
|
end
|
|
142
82
|
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
# were seen through).
|
|
83
|
+
# `unwrapped` differing from `node` is what says parentheses were seen
|
|
84
|
+
# through.
|
|
146
85
|
def foldable?(node, unwrapped)
|
|
147
|
-
return false unless STATIC_CONDITION_TYPES.any? { |type| unwrapped.
|
|
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
|
|
86
|
+
return false unless STATIC_CONDITION_TYPES.any? { |type| unwrapped.instance_of?(type) }
|
|
154
87
|
|
|
155
|
-
unwrapped.equal?(node) || PAREN_OPAQUE_TYPES.none? { |type| unwrapped.
|
|
156
|
-
# simplecov:enable
|
|
88
|
+
unwrapped.equal?(node) || PAREN_OPAQUE_TYPES.none? { |type| unwrapped.instance_of?(type) }
|
|
157
89
|
end
|
|
158
90
|
|
|
159
|
-
#
|
|
160
|
-
#
|
|
161
|
-
#
|
|
162
|
-
# multi-statement bodies synthesized a phantom then/else pair no
|
|
163
|
-
# real run can ever hit.
|
|
91
|
+
# A multi-statement body (`if (1; 2)`) folds by its LAST expression, but
|
|
92
|
+
# only when the compiler eliminates every leading statement. Stopping at
|
|
93
|
+
# multi-statement bodies synthesized a phantom then/else pair.
|
|
164
94
|
def unwrap_parentheses(node)
|
|
165
95
|
# @type var current: untyped
|
|
166
96
|
current = node
|
|
167
|
-
while current.
|
|
97
|
+
while current.instance_of?(Prism::ParenthesesNode)
|
|
98
|
+
# Empty parentheses carry no statements node at all, so a
|
|
99
|
+
# body that is one always holds at least one statement.
|
|
168
100
|
body = current.body
|
|
169
|
-
break unless body.
|
|
101
|
+
break unless body.instance_of?(Prism::StatementsNode)
|
|
170
102
|
|
|
171
103
|
statements = body.body
|
|
172
104
|
break unless statements.take(statements.size - 1).all? { |leading| eliminable_when_discarded?(leading) }
|
|
@@ -176,40 +108,26 @@ module SimpleCov
|
|
|
176
108
|
current
|
|
177
109
|
end
|
|
178
110
|
|
|
179
|
-
# Whether the compiler compiles `node` to nothing when its value is
|
|
180
|
-
# discarded.
|
|
181
111
|
def eliminable_when_discarded?(node)
|
|
182
112
|
return true if static_container_literal?(node)
|
|
183
|
-
return true if ELIMINABLE_READ_TYPES.any? { |type| node.
|
|
113
|
+
return true if ELIMINABLE_READ_TYPES.any? { |type| node.instance_of?(type) }
|
|
184
114
|
|
|
185
|
-
node.
|
|
186
|
-
node.body.
|
|
115
|
+
node.instance_of?(Prism::ParenthesesNode) &&
|
|
116
|
+
node.body.instance_of?(Prism::StatementsNode) &&
|
|
187
117
|
node.body.body.all? { |statement| eliminable_when_discarded?(statement) }
|
|
188
118
|
end
|
|
189
119
|
|
|
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
120
|
def static_container_literal?(node)
|
|
195
|
-
return true if STATIC_LITERAL_LEAF_TYPES.any? { |type| node.
|
|
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
|
|
121
|
+
return true if STATIC_LITERAL_LEAF_TYPES.any? { |type| node.instance_of?(type) }
|
|
201
122
|
|
|
202
123
|
static_container?(node)
|
|
203
|
-
# simplecov:enable
|
|
204
124
|
end
|
|
205
125
|
|
|
206
|
-
# The container dispatch, version-free: the predicate specs
|
|
207
|
-
# exercise it directly on every supported Ruby, 3.2 included.
|
|
208
126
|
def static_container?(node)
|
|
209
127
|
case node
|
|
210
|
-
when
|
|
211
|
-
when
|
|
212
|
-
when
|
|
128
|
+
when Prism::ArrayNode then static_array_literal?(node)
|
|
129
|
+
when Prism::HashNode then static_hash_literal?(node)
|
|
130
|
+
when Prism::RangeNode then static_range_literal?(node)
|
|
213
131
|
else false
|
|
214
132
|
end
|
|
215
133
|
end
|
|
@@ -220,7 +138,7 @@ module SimpleCov
|
|
|
220
138
|
|
|
221
139
|
def static_hash_literal?(node)
|
|
222
140
|
node.elements.all? do |element|
|
|
223
|
-
element.
|
|
141
|
+
element.instance_of?(Prism::AssocNode) &&
|
|
224
142
|
container_contents_eliminable?(element.key) && container_contents_eliminable?(element.value)
|
|
225
143
|
end
|
|
226
144
|
end
|
|
@@ -230,9 +148,7 @@ module SimpleCov
|
|
|
230
148
|
end
|
|
231
149
|
|
|
232
150
|
def container_contents_eliminable?(node)
|
|
233
|
-
# simplecov:disable branch — which arm runs is fixed by the running Ruby's version
|
|
234
151
|
CONTAINER_CONTENTS_NEED_STATIC_LITERALS ? static_container_literal?(node) : eliminable_when_discarded?(node)
|
|
235
|
-
# simplecov:enable branch
|
|
236
152
|
end
|
|
237
153
|
end
|
|
238
154
|
end
|
|
@@ -4,198 +4,158 @@ require_relative "prism_compat"
|
|
|
4
4
|
|
|
5
5
|
module SimpleCov
|
|
6
6
|
module StaticCoverageExtractor
|
|
7
|
-
# The source ranges Ruby's Coverage assigns to branch conditions and
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
7
|
+
# The source ranges Ruby's Coverage assigns to branch conditions and arms,
|
|
8
|
+
# resolved from Prism nodes. Simulated entries only ever merge with real
|
|
9
|
+
# entries produced by the running Ruby, and CRuby 3.4 changed several of
|
|
10
|
+
# these conventions, so every resolver here emits whichever shape this
|
|
11
|
+
# Ruby's Coverage uses (#1226, #1233). The "runtime tuple equivalence" spec
|
|
12
|
+
# exercises the module against real Coverage output on every CI Ruby, and
|
|
13
|
+
# is the authority on what each resolver should answer.
|
|
12
14
|
#
|
|
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
15
|
module LocationConventions
|
|
17
16
|
LEGACY_COVERAGE_LOCATIONS = Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.4")
|
|
18
17
|
|
|
19
18
|
# A zero-width stand-in for Prism locations, for the arms Coverage
|
|
20
|
-
# anchors to a point rather than a range.
|
|
19
|
+
# anchors to a point rather than a range. Resolvers answer whichever of a
|
|
20
|
+
# node, one of its locations, or this is nearest to hand: all three
|
|
21
|
+
# answer the four position accessors the emitted tuples are built from.
|
|
21
22
|
PointLocation = Data.define(:start_line, :start_column, :end_line, :end_column)
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
private
|
|
24
25
|
|
|
25
|
-
#
|
|
26
|
-
#
|
|
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.
|
|
26
|
+
# Which arm of each conditional below runs is fixed by the running Ruby's
|
|
27
|
+
# version, so no single process can cover both sides.
|
|
31
28
|
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
# end an `elsif` clause's range at its last content instead of the
|
|
35
|
-
# shared `end` keyword the clause doesn't own.
|
|
29
|
+
# 3.2/3.3 end an `elsif` clause's range at its last content rather than
|
|
30
|
+
# at the shared `end` keyword the clause doesn't own.
|
|
36
31
|
def if_like_location(node, type)
|
|
37
|
-
return node
|
|
32
|
+
return node unless LEGACY_COVERAGE_LOCATIONS && type.equal?(:if) && elsif_node?(node)
|
|
38
33
|
|
|
39
|
-
span(node
|
|
34
|
+
span(node, legacy_content_end(node))
|
|
40
35
|
end
|
|
41
36
|
|
|
42
37
|
def elsif_node?(node)
|
|
43
38
|
keyword = node.if_keyword_loc
|
|
44
|
-
!keyword.nil? && keyword.slice
|
|
39
|
+
!keyword.nil? && keyword.slice.eql?("elsif")
|
|
45
40
|
end
|
|
46
41
|
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
# clause's predicate / `else` keyword when its body is empty.
|
|
42
|
+
# The deepest trailing clause's statements, or that clause's predicate /
|
|
43
|
+
# `else` keyword when its body is empty.
|
|
50
44
|
def legacy_content_end(node)
|
|
51
45
|
tail = node
|
|
52
|
-
while tail.
|
|
46
|
+
while tail.instance_of?(Prism::IfNode)
|
|
53
47
|
sub = PrismCompat.subsequent(tail)
|
|
54
48
|
break unless sub
|
|
55
49
|
|
|
56
50
|
tail = sub
|
|
57
51
|
end
|
|
58
|
-
return
|
|
52
|
+
return tail.statements || tail.predicate if tail.instance_of?(Prism::IfNode)
|
|
59
53
|
|
|
60
|
-
tail.statements
|
|
54
|
+
tail.statements || tail.else_keyword_loc
|
|
61
55
|
end
|
|
62
56
|
|
|
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
57
|
def if_like_then_location(node, type)
|
|
70
|
-
return node.statements
|
|
71
|
-
return point_at_end(node.predicate
|
|
58
|
+
return node.statements if node.statements
|
|
59
|
+
return point_at_end(node.predicate) if empty_arm_collapses?(node, type)
|
|
72
60
|
|
|
73
61
|
if_like_location(node, type)
|
|
74
62
|
end
|
|
75
63
|
|
|
76
|
-
#
|
|
77
|
-
#
|
|
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).
|
|
64
|
+
# With no else/elsif present, the synthesized else inherits the
|
|
65
|
+
# condition's range.
|
|
81
66
|
def if_like_else_location(node, type)
|
|
82
67
|
sub = PrismCompat.subsequent(node)
|
|
83
68
|
return if_like_location(node, type) unless sub
|
|
84
|
-
# An `elsif` arrives as a nested IfNode
|
|
85
|
-
# outer else arm to the clause's own range, not its then body
|
|
86
|
-
#
|
|
87
|
-
return if_like_location(sub, :if) if sub.
|
|
88
|
-
return sub.statements
|
|
69
|
+
# An `elsif` arrives as a nested IfNode, and Coverage attributes the
|
|
70
|
+
# outer else arm to the clause's own range, not its then body (which is
|
|
71
|
+
# what created phantom unmergeable arms).
|
|
72
|
+
return if_like_location(sub, :if) if sub.instance_of?(Prism::IfNode)
|
|
73
|
+
return sub.statements if sub.statements
|
|
89
74
|
|
|
90
75
|
empty_else_location(node, sub, type)
|
|
91
76
|
end
|
|
92
77
|
|
|
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
78
|
def empty_else_location(node, sub, type)
|
|
98
|
-
return sub
|
|
99
|
-
|
|
79
|
+
return sub if !LEGACY_COVERAGE_LOCATIONS && type.equal?(:if)
|
|
80
|
+
# Void position is a legacy distinction: a Ruby that does not need the
|
|
81
|
+
# value-position pass answers every node as value.
|
|
82
|
+
return point_at_end(sub.else_keyword_loc) unless value_position?(node)
|
|
100
83
|
|
|
101
84
|
if_like_location(node, type)
|
|
102
85
|
end
|
|
103
86
|
|
|
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
87
|
def case_arm_location(case_node, when_node, when_type)
|
|
110
|
-
return when_node.statements
|
|
111
|
-
return when_node
|
|
112
|
-
return point_at_end(when_node.pattern
|
|
113
|
-
return point_at_end(when_node
|
|
88
|
+
return when_node.statements if when_node.statements
|
|
89
|
+
return when_node unless LEGACY_COVERAGE_LOCATIONS
|
|
90
|
+
return point_at_end(when_node.pattern) if when_type.equal?(:in)
|
|
91
|
+
return point_at_end(when_node) unless value_position?(case_node)
|
|
114
92
|
|
|
115
93
|
legacy_when_value_location(case_node, when_node)
|
|
116
94
|
end
|
|
117
95
|
|
|
118
96
|
def legacy_when_value_location(case_node, when_node)
|
|
119
|
-
span(when_node
|
|
97
|
+
span(when_node, legacy_case_tail_end(case_node, when_node))
|
|
120
98
|
end
|
|
121
99
|
|
|
122
|
-
#
|
|
123
|
-
#
|
|
100
|
+
# Only a `when` reaches here, and a `when` always carries at least one
|
|
101
|
+
# condition, so the fallback is always available.
|
|
124
102
|
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
|
|
103
|
+
following_case_content(case_node, when_node).last || when_node.conditions.last
|
|
127
104
|
end
|
|
128
105
|
|
|
129
106
|
def following_case_content(case_node, when_node)
|
|
130
107
|
clauses = case_node.conditions
|
|
131
|
-
index = clauses.index { |clause| clause.equal?(when_node) } || 0
|
|
132
108
|
# 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
|
|
135
|
-
|
|
109
|
+
# condition when empty), so the whole clause is what extends the range
|
|
110
|
+
# through trailing empty clauses that have no `statements`.
|
|
111
|
+
index = clauses.index { |clause| clause.equal?(when_node) }
|
|
112
|
+
content = clauses.drop(index + 1)
|
|
136
113
|
else_statements = PrismCompat.else_clause(case_node)&.statements
|
|
137
|
-
content
|
|
114
|
+
content += [else_statements] if else_statements
|
|
138
115
|
content
|
|
139
116
|
end
|
|
140
117
|
|
|
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
118
|
def else_arm_location(node)
|
|
147
119
|
else_clause = PrismCompat.else_clause(node)
|
|
148
|
-
return node
|
|
149
|
-
return else_clause.statements
|
|
150
|
-
return else_clause
|
|
151
|
-
# Empty explicit `else`: a point at the `else` keyword's end in void
|
|
152
|
-
# position, the whole case's range in value position.
|
|
120
|
+
return node unless else_clause
|
|
121
|
+
return else_clause.statements if else_clause.statements
|
|
122
|
+
return else_clause unless LEGACY_COVERAGE_LOCATIONS
|
|
153
123
|
return point_at_end(else_clause.else_keyword_loc) unless value_position?(node)
|
|
154
124
|
|
|
155
|
-
node
|
|
125
|
+
node
|
|
156
126
|
end
|
|
157
127
|
|
|
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
128
|
def loop_body_location(node)
|
|
162
129
|
return legacy_do_while_body_location(node) if LEGACY_COVERAGE_LOCATIONS && begin_modifier_loop?(node)
|
|
163
|
-
return node.statements
|
|
164
|
-
return point_at_end(node.predicate
|
|
130
|
+
return node.statements if node.statements
|
|
131
|
+
return point_at_end(node.predicate) if LEGACY_COVERAGE_LOCATIONS
|
|
165
132
|
|
|
166
|
-
node
|
|
133
|
+
node
|
|
167
134
|
end
|
|
168
135
|
|
|
169
|
-
#
|
|
170
|
-
#
|
|
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.
|
|
136
|
+
# The accessor arrived in Prism 0.25, and this gem supports older ones,
|
|
137
|
+
# where nothing answers the question.
|
|
175
138
|
def begin_modifier_loop?(node)
|
|
176
139
|
node.respond_to?(:begin_modifier?) && node.begin_modifier?
|
|
177
140
|
end
|
|
178
141
|
|
|
142
|
+
# `begin ... end while cond` parses as a while whose sole statement is
|
|
143
|
+
# the BeginNode. Modern Coverage attributes the body to that whole
|
|
144
|
+
# `begin ... end` span, but 3.3 uses the begin's inner statements.
|
|
179
145
|
def legacy_do_while_body_location(node)
|
|
180
|
-
begin_node = node.statements.body
|
|
146
|
+
begin_node, = node.statements.body
|
|
181
147
|
inner = begin_node.statements
|
|
182
|
-
inner
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
# Coverage's safe-navigation branch spans the receiver through the
|
|
186
|
-
#
|
|
187
|
-
#
|
|
188
|
-
# `x&.foo
|
|
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.
|
|
148
|
+
inner || point_at_end(begin_node.begin_keyword_loc)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Coverage's safe-navigation branch spans the receiver through the end of
|
|
152
|
+
# the call's arguments (or just the message when there are none), but
|
|
153
|
+
# never includes a trailing block: `x&.foo { ... }` ends exactly where
|
|
154
|
+
# `x&.foo` would. `node.location` would include the block (#1233).
|
|
194
155
|
def safe_navigation_location(node)
|
|
195
|
-
span(node
|
|
156
|
+
span(node, node.closing_loc || node.arguments || node.message_loc)
|
|
196
157
|
end
|
|
197
158
|
|
|
198
|
-
# The range from `from`'s start through `to`'s end.
|
|
199
159
|
def span(from, to)
|
|
200
160
|
PointLocation.new(
|
|
201
161
|
start_line: from.start_line, start_column: from.start_column,
|
|
@@ -210,27 +170,22 @@ module SimpleCov
|
|
|
210
170
|
)
|
|
211
171
|
end
|
|
212
172
|
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
# legacy Coverage does it only in void position, for both.
|
|
173
|
+
# Modern Coverage collapses an empty then arm for every `if` but not
|
|
174
|
+
# `unless`; legacy Coverage does it only in void position, for both.
|
|
216
175
|
def empty_arm_collapses?(node, type)
|
|
217
|
-
return type
|
|
176
|
+
return type.equal?(:if) unless LEGACY_COVERAGE_LOCATIONS
|
|
218
177
|
|
|
219
178
|
!value_position?(node)
|
|
220
179
|
end
|
|
221
180
|
|
|
222
|
-
#
|
|
223
|
-
# legacy Rubies
|
|
224
|
-
#
|
|
225
|
-
# ValuePositions (only on legacy; nil elsewhere, which reads as
|
|
226
|
-
# "value" — the safe, pre-audit default).
|
|
181
|
+
# `@value_positions` is computed once per parse by ValuePositions, and
|
|
182
|
+
# only on legacy Rubies. Nil elsewhere, which reads as "value": the safe,
|
|
183
|
+
# pre-audit default.
|
|
227
184
|
def value_position?(node)
|
|
228
185
|
return true if @value_positions.nil?
|
|
229
186
|
|
|
230
187
|
@value_positions.key?(node)
|
|
231
188
|
end
|
|
232
|
-
# simplecov:enable
|
|
233
189
|
end
|
|
234
|
-
# rubocop:enable Metrics/ModuleLength
|
|
235
190
|
end
|
|
236
191
|
end
|