simplecov 0.22.0 → 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/LICENSE +1 -1
- data/README.md +173 -875
- data/exe/simplecov +8 -0
- data/lib/minitest/simplecov_plugin.rb +18 -7
- data/lib/simplecov/atomic_file.rb +67 -0
- data/lib/simplecov/autostart.rb +9 -0
- 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 +86 -0
- data/lib/simplecov/cli/command_helpers.rb +91 -0
- data/lib/simplecov/cli/completions/scripts.rb +114 -0
- data/lib/simplecov/cli/completions.rb +83 -0
- data/lib/simplecov/cli/coverage.rb +83 -0
- data/lib/simplecov/cli/coverage_file.rb +109 -0
- 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 +115 -0
- data/lib/simplecov/cli/dotfile.rb +156 -0
- 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 +115 -0
- data/lib/simplecov/cli/open.rb +45 -0
- 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 +91 -0
- data/lib/simplecov/cli/run.rb +35 -0
- data/lib/simplecov/cli/serve/report_preparer.rb +30 -0
- data/lib/simplecov/cli/serve/static_file_handler.rb +128 -0
- data/lib/simplecov/cli/serve.rb +88 -0
- 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 +127 -0
- 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 +114 -0
- data/lib/simplecov/color.rb +65 -0
- data/lib/simplecov/combine/branches_combiner.rb +44 -21
- data/lib/simplecov/combine/coverage_accumulator.rb +175 -0
- data/lib/simplecov/combine/identity_interner.rb +27 -0
- data/lib/simplecov/combine/interned_counts.rb +28 -0
- data/lib/simplecov/combine/lines_combiner.rb +35 -29
- data/lib/simplecov/combine/methods_combiner.rb +48 -0
- data/lib/simplecov/combine/results_combiner.rb +9 -49
- data/lib/simplecov/combine.rb +0 -24
- data/lib/simplecov/command_guesser.rb +99 -42
- data/lib/simplecov/configuration/baseline.rb +31 -0
- data/lib/simplecov/configuration/coverage.rb +184 -0
- data/lib/simplecov/configuration/coverage_criteria.rb +143 -0
- data/lib/simplecov/configuration/deprecations.rb +21 -0
- data/lib/simplecov/configuration/eval_coverage.rb +37 -0
- data/lib/simplecov/configuration/filters.rb +131 -0
- data/lib/simplecov/configuration/formatting.rb +129 -0
- data/lib/simplecov/configuration/groups.rb +48 -0
- data/lib/simplecov/configuration/history.rb +44 -0
- data/lib/simplecov/configuration/ignored_entries.rb +72 -0
- data/lib/simplecov/configuration/merging.rb +197 -0
- 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 +150 -0
- data/lib/simplecov/configuration/view_coverage.rb +37 -0
- data/lib/simplecov/configuration.rb +100 -443
- 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 +21 -0
- data/lib/simplecov/coverage_statistics.rb +24 -28
- data/lib/simplecov/coverage_violations.rb +254 -0
- data/lib/simplecov/current_run.rb +61 -0
- data/lib/simplecov/defaults.rb +20 -28
- data/lib/simplecov/deprecation.rb +63 -0
- 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 +152 -0
- data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
- data/lib/simplecov/exit_codes/check.rb +39 -0
- data/lib/simplecov/exit_codes/exit_code_handling.rb +24 -5
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +17 -67
- 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 +27 -0
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +20 -38
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +28 -0
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +37 -34
- data/lib/simplecov/exit_codes.rb +26 -0
- data/lib/simplecov/exit_handling.rb +163 -0
- data/lib/simplecov/file_list.rb +63 -48
- data/lib/simplecov/filter.rb +98 -42
- data/lib/simplecov/formatter/base.rb +100 -0
- data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +95 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +93 -0
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +153 -0
- data/lib/simplecov/formatter/html_formatter.rb +88 -0
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +122 -0
- data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +132 -0
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +97 -0
- data/lib/simplecov/formatter/json_formatter.rb +40 -0
- data/lib/simplecov/formatter/multi_formatter.rb +8 -10
- data/lib/simplecov/formatter/simple_formatter.rb +12 -15
- data/lib/simplecov/formatter.rb +21 -1
- data/lib/simplecov/group_names.rb +33 -0
- data/lib/simplecov/history.rb +115 -0
- data/lib/simplecov/last_run.rb +16 -5
- data/lib/simplecov/lines_classifier.rb +67 -19
- data/lib/simplecov/load_global_config.rb +12 -4
- data/lib/simplecov/parallel_adapters/base.rb +50 -0
- data/lib/simplecov/parallel_adapters/generic.rb +33 -0
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +72 -0
- data/lib/simplecov/parallel_adapters.rb +54 -0
- data/lib/simplecov/parallel_coordination.rb +124 -0
- data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
- data/lib/simplecov/parallel_result_merger.rb +171 -0
- data/lib/simplecov/process.rb +34 -12
- 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/bundler_filter.rb +1 -1
- data/lib/simplecov/profiles/hidden_filter.rb +1 -1
- data/lib/simplecov/profiles/rails.rb +13 -10
- data/lib/simplecov/profiles/root_filter.rb +2 -5
- data/lib/simplecov/profiles/strict.rb +22 -0
- data/lib/simplecov/profiles/test_frameworks.rb +1 -4
- data/lib/simplecov/profiles.rb +26 -19
- data/lib/simplecov/report_deferral.rb +45 -0
- data/lib/simplecov/report_stamp.rb +25 -0
- data/lib/simplecov/result/filter_config.rb +19 -0
- data/lib/simplecov/result/missing_source_files_reporter.rb +48 -0
- data/lib/simplecov/result/serialization.rb +43 -0
- data/lib/simplecov/result/source_file_builder.rb +46 -0
- data/lib/simplecov/result.rb +123 -47
- data/lib/simplecov/result_adapter.rb +130 -11
- data/lib/simplecov/result_merger/contexts.rb +22 -0
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +24 -0
- data/lib/simplecov/result_merger/resultset_file.rb +67 -0
- data/lib/simplecov/result_merger/resultset_run_identity.rb +65 -0
- data/lib/simplecov/result_merger/resultset_store.rb +62 -0
- data/lib/simplecov/result_merger/unloaded_files.rb +95 -0
- data/lib/simplecov/result_merger.rb +120 -124
- data/lib/simplecov/result_processing.rb +188 -0
- data/lib/simplecov/run_identity.rb +75 -0
- data/lib/simplecov/simulate_coverage.rb +72 -17
- data/lib/simplecov/source_file/branch.rb +8 -35
- data/lib/simplecov/source_file/branch_builder.rb +95 -0
- data/lib/simplecov/source_file/builder_context.rb +28 -0
- data/lib/simplecov/source_file/line.rb +17 -32
- data/lib/simplecov/source_file/line_builder.rb +37 -0
- data/lib/simplecov/source_file/method.rb +54 -0
- data/lib/simplecov/source_file/method_builder.rb +51 -0
- data/lib/simplecov/source_file/ruby_data_parser.rb +103 -0
- data/lib/simplecov/source_file/skip_chunks.rb +72 -0
- data/lib/simplecov/source_file/source_loader.rb +89 -0
- data/lib/simplecov/source_file/statistics.rb +76 -0
- data/lib/simplecov/source_file.rb +64 -260
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +155 -0
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +191 -0
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +45 -0
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +46 -0
- data/lib/simplecov/static_coverage_extractor/value_position.rb +61 -0
- data/lib/simplecov/static_coverage_extractor/visitor.rb +163 -0
- data/lib/simplecov/static_coverage_extractor.rb +78 -0
- 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 +62 -0
- data/lib/simplecov/useless_results_remover.rb +17 -7
- 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 +5 -0
- data/lib/simplecov.rb +143 -394
- data/lib/simplecov_json_formatter.rb +5 -0
- data/man/simplecov.1 +354 -0
- data/sig/simplecov.rbs +1710 -0
- metadata +183 -61
- 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,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module StaticCoverageExtractor
|
|
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.
|
|
10
|
+
module ConditionFolding
|
|
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.
|
|
13
|
+
FOLDS_SOURCE_FILE = Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.4")
|
|
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).
|
|
18
|
+
CONTAINER_CONTENTS_NEED_STATIC_LITERALS = !FOLDS_SOURCE_FILE
|
|
19
|
+
|
|
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.
|
|
26
|
+
# simplecov:disable branch — which arm runs is fixed by the running Ruby's version
|
|
27
|
+
STATIC_CONDITION_TYPES = [
|
|
28
|
+
::Prism::IntegerNode, ::Prism::FloatNode, ::Prism::RationalNode,
|
|
29
|
+
::Prism::ImaginaryNode, ::Prism::SymbolNode, ::Prism::StringNode,
|
|
30
|
+
::Prism::TrueNode, ::Prism::FalseNode, ::Prism::NilNode,
|
|
31
|
+
::Prism::SourceLineNode, ::Prism::SourceEncodingNode, ::Prism::LambdaNode,
|
|
32
|
+
*(::Prism::SourceFileNode if FOLDS_SOURCE_FILE)
|
|
33
|
+
].freeze
|
|
34
|
+
# simplecov:enable branch
|
|
35
|
+
|
|
36
|
+
FALSY_CONDITION_TYPES = [::Prism::FalseNode, ::Prism::NilNode].freeze
|
|
37
|
+
|
|
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.
|
|
41
|
+
PAREN_OPAQUE_TYPES = [
|
|
42
|
+
::Prism::NilNode, ::Prism::StringNode, ::Prism::LambdaNode, ::Prism::SourceFileNode
|
|
43
|
+
].freeze
|
|
44
|
+
|
|
45
|
+
# A multi-statement paren condition (`if (1; 2)`) folds by its last
|
|
46
|
+
# expression only when every leading statement is eliminated when
|
|
47
|
+
# discarded, and these always are, bare or composing an Array/Hash/Range.
|
|
48
|
+
STATIC_LITERAL_LEAF_TYPES = [
|
|
49
|
+
::Prism::IntegerNode, ::Prism::FloatNode, ::Prism::RationalNode,
|
|
50
|
+
::Prism::ImaginaryNode, ::Prism::StringNode, ::Prism::SymbolNode,
|
|
51
|
+
::Prism::TrueNode, ::Prism::FalseNode, ::Prism::NilNode,
|
|
52
|
+
::Prism::SourceLineNode, ::Prism::SourceFileNode, ::Prism::SourceEncodingNode, ::Prism::RegularExpressionNode
|
|
53
|
+
].freeze
|
|
54
|
+
|
|
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.
|
|
57
|
+
ELIMINABLE_READ_TYPES = [
|
|
58
|
+
::Prism::SelfNode, ::Prism::LocalVariableReadNode,
|
|
59
|
+
::Prism::InstanceVariableReadNode, ::Prism::DefinedNode
|
|
60
|
+
].freeze
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# `visit` is nil-safe, so a missing arm just visits nothing.
|
|
65
|
+
def visit_folded_arms(verdict, truthy_arm, falsy_arm)
|
|
66
|
+
visit(verdict.equal?(:truthy) ? truthy_arm : falsy_arm)
|
|
67
|
+
end
|
|
68
|
+
|
|
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.
|
|
72
|
+
#
|
|
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.
|
|
76
|
+
def folded_condition(node)
|
|
77
|
+
unwrapped = unwrap_parentheses(node)
|
|
78
|
+
return nil unless foldable?(node, unwrapped)
|
|
79
|
+
|
|
80
|
+
(FALSY_CONDITION_TYPES.any? { |type| unwrapped.instance_of?(type) }) ? :falsy : :truthy
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# `unwrapped` differing from `node` is what says parentheses were seen
|
|
84
|
+
# through.
|
|
85
|
+
def foldable?(node, unwrapped)
|
|
86
|
+
return false unless STATIC_CONDITION_TYPES.any? { |type| unwrapped.instance_of?(type) }
|
|
87
|
+
|
|
88
|
+
unwrapped.equal?(node) || PAREN_OPAQUE_TYPES.none? { |type| unwrapped.instance_of?(type) }
|
|
89
|
+
end
|
|
90
|
+
|
|
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.
|
|
94
|
+
def unwrap_parentheses(node)
|
|
95
|
+
# @type var current: untyped
|
|
96
|
+
current = node
|
|
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.
|
|
100
|
+
body = current.body
|
|
101
|
+
break unless body.instance_of?(Prism::StatementsNode)
|
|
102
|
+
|
|
103
|
+
statements = body.body
|
|
104
|
+
break unless statements.take(statements.size - 1).all? { |leading| eliminable_when_discarded?(leading) }
|
|
105
|
+
|
|
106
|
+
current = statements.last
|
|
107
|
+
end
|
|
108
|
+
current
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def eliminable_when_discarded?(node)
|
|
112
|
+
return true if static_container_literal?(node)
|
|
113
|
+
return true if ELIMINABLE_READ_TYPES.any? { |type| node.instance_of?(type) }
|
|
114
|
+
|
|
115
|
+
node.instance_of?(Prism::ParenthesesNode) &&
|
|
116
|
+
node.body.instance_of?(Prism::StatementsNode) &&
|
|
117
|
+
node.body.body.all? { |statement| eliminable_when_discarded?(statement) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def static_container_literal?(node)
|
|
121
|
+
return true if STATIC_LITERAL_LEAF_TYPES.any? { |type| node.instance_of?(type) }
|
|
122
|
+
|
|
123
|
+
static_container?(node)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def static_container?(node)
|
|
127
|
+
case node
|
|
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)
|
|
131
|
+
else false
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def static_array_literal?(node)
|
|
136
|
+
node.elements.all? { |element| container_contents_eliminable?(element) }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def static_hash_literal?(node)
|
|
140
|
+
node.elements.all? do |element|
|
|
141
|
+
element.instance_of?(Prism::AssocNode) &&
|
|
142
|
+
container_contents_eliminable?(element.key) && container_contents_eliminable?(element.value)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def static_range_literal?(node)
|
|
147
|
+
[node.left, node.right].all? { |bound| bound.nil? || container_contents_eliminable?(bound) }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def container_contents_eliminable?(node)
|
|
151
|
+
CONTAINER_CONTENTS_NEED_STATIC_LITERALS ? static_container_literal?(node) : eliminable_when_discarded?(node)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,191 @@
|
|
|
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 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.
|
|
14
|
+
#
|
|
15
|
+
module LocationConventions
|
|
16
|
+
LEGACY_COVERAGE_LOCATIONS = Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.4")
|
|
17
|
+
|
|
18
|
+
# A zero-width stand-in for Prism locations, for the arms Coverage
|
|
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.
|
|
22
|
+
PointLocation = Data.define(:start_line, :start_column, :end_line, :end_column)
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
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.
|
|
28
|
+
|
|
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.
|
|
31
|
+
def if_like_location(node, type)
|
|
32
|
+
return node unless LEGACY_COVERAGE_LOCATIONS && type.equal?(:if) && elsif_node?(node)
|
|
33
|
+
|
|
34
|
+
span(node, legacy_content_end(node))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def elsif_node?(node)
|
|
38
|
+
keyword = node.if_keyword_loc
|
|
39
|
+
!keyword.nil? && keyword.slice.eql?("elsif")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The deepest trailing clause's statements, or that clause's predicate /
|
|
43
|
+
# `else` keyword when its body is empty.
|
|
44
|
+
def legacy_content_end(node)
|
|
45
|
+
tail = node
|
|
46
|
+
while tail.instance_of?(Prism::IfNode)
|
|
47
|
+
sub = PrismCompat.subsequent(tail)
|
|
48
|
+
break unless sub
|
|
49
|
+
|
|
50
|
+
tail = sub
|
|
51
|
+
end
|
|
52
|
+
return tail.statements || tail.predicate if tail.instance_of?(Prism::IfNode)
|
|
53
|
+
|
|
54
|
+
tail.statements || tail.else_keyword_loc
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def if_like_then_location(node, type)
|
|
58
|
+
return node.statements if node.statements
|
|
59
|
+
return point_at_end(node.predicate) if empty_arm_collapses?(node, type)
|
|
60
|
+
|
|
61
|
+
if_like_location(node, type)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# With no else/elsif present, the synthesized else inherits the
|
|
65
|
+
# condition's range.
|
|
66
|
+
def if_like_else_location(node, type)
|
|
67
|
+
sub = PrismCompat.subsequent(node)
|
|
68
|
+
return if_like_location(node, type) unless sub
|
|
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
|
|
74
|
+
|
|
75
|
+
empty_else_location(node, sub, type)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def empty_else_location(node, sub, type)
|
|
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)
|
|
83
|
+
|
|
84
|
+
if_like_location(node, type)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def case_arm_location(case_node, when_node, when_type)
|
|
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)
|
|
92
|
+
|
|
93
|
+
legacy_when_value_location(case_node, when_node)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def legacy_when_value_location(case_node, when_node)
|
|
97
|
+
span(when_node, legacy_case_tail_end(case_node, when_node))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Only a `when` reaches here, and a `when` always carries at least one
|
|
101
|
+
# condition, so the fallback is always available.
|
|
102
|
+
def legacy_case_tail_end(case_node, when_node)
|
|
103
|
+
following_case_content(case_node, when_node).last || when_node.conditions.last
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def following_case_content(case_node, when_node)
|
|
107
|
+
clauses = case_node.conditions
|
|
108
|
+
# A when-clause's own location ends where its body ends (or at its
|
|
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)
|
|
113
|
+
else_statements = PrismCompat.else_clause(case_node)&.statements
|
|
114
|
+
content += [else_statements] if else_statements
|
|
115
|
+
content
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def else_arm_location(node)
|
|
119
|
+
else_clause = PrismCompat.else_clause(node)
|
|
120
|
+
return node unless else_clause
|
|
121
|
+
return else_clause.statements if else_clause.statements
|
|
122
|
+
return else_clause unless LEGACY_COVERAGE_LOCATIONS
|
|
123
|
+
return point_at_end(else_clause.else_keyword_loc) unless value_position?(node)
|
|
124
|
+
|
|
125
|
+
node
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def loop_body_location(node)
|
|
129
|
+
return legacy_do_while_body_location(node) if LEGACY_COVERAGE_LOCATIONS && begin_modifier_loop?(node)
|
|
130
|
+
return node.statements if node.statements
|
|
131
|
+
return point_at_end(node.predicate) if LEGACY_COVERAGE_LOCATIONS
|
|
132
|
+
|
|
133
|
+
node
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# The accessor arrived in Prism 0.25, and this gem supports older ones,
|
|
137
|
+
# where nothing answers the question.
|
|
138
|
+
def begin_modifier_loop?(node)
|
|
139
|
+
node.respond_to?(:begin_modifier?) && node.begin_modifier?
|
|
140
|
+
end
|
|
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.
|
|
145
|
+
def legacy_do_while_body_location(node)
|
|
146
|
+
begin_node, = node.statements.body
|
|
147
|
+
inner = begin_node.statements
|
|
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).
|
|
155
|
+
def safe_navigation_location(node)
|
|
156
|
+
span(node, node.closing_loc || node.arguments || node.message_loc)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def span(from, to)
|
|
160
|
+
PointLocation.new(
|
|
161
|
+
start_line: from.start_line, start_column: from.start_column,
|
|
162
|
+
end_line: to.end_line, end_column: to.end_column
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def point_at_end(location)
|
|
167
|
+
PointLocation.new(
|
|
168
|
+
start_line: location.end_line, start_column: location.end_column,
|
|
169
|
+
end_line: location.end_line, end_column: location.end_column
|
|
170
|
+
)
|
|
171
|
+
end
|
|
172
|
+
|
|
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.
|
|
175
|
+
def empty_arm_collapses?(node, type)
|
|
176
|
+
return type.equal?(:if) unless LEGACY_COVERAGE_LOCATIONS
|
|
177
|
+
|
|
178
|
+
!value_position?(node)
|
|
179
|
+
end
|
|
180
|
+
|
|
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.
|
|
184
|
+
def value_position?(node)
|
|
185
|
+
return true if @value_positions.nil?
|
|
186
|
+
|
|
187
|
+
@value_positions.key?(node)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module StaticCoverageExtractor
|
|
5
|
+
# Visitor mixin that collects method tuples and tracks the lexical class /
|
|
6
|
+
# module nesting that names them, in the shape Ruby's `Coverage` reports
|
|
7
|
+
# methods. Module and Class are both namespaces here, since `Coverage` reports
|
|
8
|
+
# both as the constant.
|
|
9
|
+
module MethodCollector
|
|
10
|
+
def visit_class_node(node)
|
|
11
|
+
with_class(constant_name(node.constant_path)) { super }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def visit_module_node(node)
|
|
15
|
+
with_class(constant_name(node.constant_path)) { super }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# `def name(...)` and `def self.name(...)` both produce DefNode. The class
|
|
19
|
+
# context is the surrounding lexical class or module, or `Object` at the top
|
|
20
|
+
# level, matching `Coverage`'s convention.
|
|
21
|
+
def visit_def_node(node)
|
|
22
|
+
class_name = @class_stack.last || "Object"
|
|
23
|
+
key = [class_name, node.name, node.start_line, node.start_column, node.end_line, node.end_column]
|
|
24
|
+
@methods[key] = 0
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def constant_name(node)
|
|
31
|
+
return "<anonymous>" if node.nil?
|
|
32
|
+
return node.slice if node.respond_to?(:slice)
|
|
33
|
+
|
|
34
|
+
node.to_s
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def with_class(name)
|
|
38
|
+
@class_stack.push(name)
|
|
39
|
+
yield
|
|
40
|
+
ensure
|
|
41
|
+
@class_stack.pop
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module StaticCoverageExtractor
|
|
5
|
+
# The Prism 1.3 accessor renames for the trailing clause of conditional
|
|
6
|
+
# nodes, resolved once at load so the per-node hot paths stay branch-free.
|
|
7
|
+
# Ruby 3.3's stdlib Prism predates the renames; 3.4+ and any installed prism
|
|
8
|
+
# gem post-date them, and reaching for the modern name on the old one raised
|
|
9
|
+
# NoMethodError inside the extractor, which `call` swallowed, silently
|
|
10
|
+
# falling back to no simulated data.
|
|
11
|
+
#
|
|
12
|
+
# This lives in its own file, required by every consumer, because the
|
|
13
|
+
# constants are referenced from several extractor files and defining them
|
|
14
|
+
# after those files were loaded worked only while the references happened at
|
|
15
|
+
# call time: a load-order trap for the next editor.
|
|
16
|
+
module PrismCompat
|
|
17
|
+
extend self
|
|
18
|
+
|
|
19
|
+
# simplecov:disable
|
|
20
|
+
IF_NODE_SUBSEQUENT_METHOD =
|
|
21
|
+
if Prism::IfNode.method_defined?(:subsequent)
|
|
22
|
+
:subsequent
|
|
23
|
+
else
|
|
24
|
+
:consequent
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
ELSE_CLAUSE_METHOD =
|
|
28
|
+
if Prism::CaseNode.method_defined?(:else_clause)
|
|
29
|
+
:else_clause
|
|
30
|
+
else
|
|
31
|
+
:consequent
|
|
32
|
+
end
|
|
33
|
+
# simplecov:enable
|
|
34
|
+
|
|
35
|
+
def subsequent(node)
|
|
36
|
+
node.instance_of?(Prism::IfNode) ? node.public_send(IF_NODE_SUBSEQUENT_METHOD) : else_clause(node)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The rename hit `UnlessNode`, `CaseNode`, and `CaseMatchNode` together, so
|
|
40
|
+
# one constant probed off CaseNode covers all three.
|
|
41
|
+
def else_clause(node)
|
|
42
|
+
node.public_send(ELSE_CLAUSE_METHOD)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
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 (#1233). On 3.3 the source range Coverage assigns to an empty
|
|
9
|
+
# branch arm depends on whether its construct is in value position, where its
|
|
10
|
+
# result is the method's return value, or void position, where the result is
|
|
11
|
+
# discarded. Value position keeps the whole-construct range; void collapses
|
|
12
|
+
# the arm to a point at its header's end. Ruby 3.4 dropped the distinction.
|
|
13
|
+
#
|
|
14
|
+
# "Value position" here is strictly method-return (tail) position. It reaches
|
|
15
|
+
# a node only through statement tails and `if`/`unless`/`when` arms.
|
|
16
|
+
# Assignments, blocks, lambdas, method arguments, `case/in` arms, and loop
|
|
17
|
+
# bodies all discard it, so `tail_children` names the constructs that forward
|
|
18
|
+
# tail position and everything else falls through to the void default.
|
|
19
|
+
module ValuePositions
|
|
20
|
+
extend self
|
|
21
|
+
|
|
22
|
+
# An identity set of the Prism nodes Coverage treats as being in value
|
|
23
|
+
# position. This pass runs natively only on legacy Rubies; elsewhere the
|
|
24
|
+
# specs drive it directly, pinned by the differential tuple-equivalence spec
|
|
25
|
+
# against real Coverage on 3.3.
|
|
26
|
+
def call(root)
|
|
27
|
+
positions = {} #: Hash[untyped, bool]
|
|
28
|
+
positions.compare_by_identity
|
|
29
|
+
mark(root, true, positions)
|
|
30
|
+
positions
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def mark(node, in_value, positions)
|
|
34
|
+
return unless node.is_a?(Prism::Node)
|
|
35
|
+
|
|
36
|
+
positions[node] = true if in_value
|
|
37
|
+
children = tail_children(node, in_value)
|
|
38
|
+
node.compact_child_nodes.each do |child|
|
|
39
|
+
mark(child, children.any? { |c| c.equal?(child) }, positions)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A method body is a tail context even when the `def` itself is not, since
|
|
44
|
+
# the method still returns its last expression, so it is included regardless
|
|
45
|
+
# of `in_value`. `case/in` is intentionally absent: its `in` arms and `else`
|
|
46
|
+
# both discard tail position.
|
|
47
|
+
def tail_children(node, in_value)
|
|
48
|
+
return [node.body] if node.instance_of?(Prism::DefNode)
|
|
49
|
+
return [] unless in_value
|
|
50
|
+
|
|
51
|
+
case node
|
|
52
|
+
when Prism::StatementsNode then [node.body.last]
|
|
53
|
+
when Prism::IfNode, Prism::UnlessNode then [node.statements, PrismCompat.subsequent(node)]
|
|
54
|
+
when Prism::CaseNode then [*node.conditions, PrismCompat.else_clause(node)]
|
|
55
|
+
when Prism::ElseNode, Prism::WhenNode, Prism::BeginNode, Prism::ProgramNode then [node.statements]
|
|
56
|
+
else []
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "prism_compat"
|
|
4
|
+
require_relative "condition_folding"
|
|
5
|
+
require_relative "location_conventions"
|
|
6
|
+
require_relative "method_collector"
|
|
7
|
+
require_relative "value_position"
|
|
8
|
+
|
|
9
|
+
module SimpleCov
|
|
10
|
+
module StaticCoverageExtractor
|
|
11
|
+
# Prism visitor that accumulates branch and method tuples in the shape
|
|
12
|
+
# Ruby's `Coverage` reports. Tuple ids are sequential across the file like
|
|
13
|
+
# `Coverage`'s, but the numbering order can differ. That's fine: the
|
|
14
|
+
# combiners intern on source span and the report output drops ids, so
|
|
15
|
+
# nothing downstream compares them.
|
|
16
|
+
class Visitor < Prism::Visitor
|
|
17
|
+
include MethodCollector
|
|
18
|
+
include LocationConventions
|
|
19
|
+
include ConditionFolding
|
|
20
|
+
|
|
21
|
+
attr_reader :branches, :methods
|
|
22
|
+
|
|
23
|
+
# Prism's Visitor is a stateless dispatch table whose initializer is
|
|
24
|
+
# Object's, so no mutation of the `super` call can be told apart. It
|
|
25
|
+
# stays for the day that stops being true.
|
|
26
|
+
# mutant:disable
|
|
27
|
+
def initialize
|
|
28
|
+
super
|
|
29
|
+
@branches = {}
|
|
30
|
+
@methods = {}
|
|
31
|
+
@next_id = 0
|
|
32
|
+
@class_stack = []
|
|
33
|
+
@value_positions = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# On legacy Rubies the location of an empty branch arm depends on whether
|
|
37
|
+
# its construct is in value (tail) position, so precompute that once for
|
|
38
|
+
# the whole tree before emitting anything.
|
|
39
|
+
def visit_program_node(node)
|
|
40
|
+
@value_positions = ValuePositions.call(node) if LEGACY_COVERAGE_LOCATIONS
|
|
41
|
+
super
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# `if` / `unless` / postfix / ternary all parse as IfNode (or UnlessNode).
|
|
45
|
+
# Both carry a `then` arm and an optional `subsequent` (an ElseNode for
|
|
46
|
+
# `else`, another IfNode for `elsif`). When the subsequent is missing,
|
|
47
|
+
# Coverage synthesizes a `:else` arm attributed to the whole condition's
|
|
48
|
+
# range, and so do we.
|
|
49
|
+
#
|
|
50
|
+
# A folded condition emits no tuple, and only its live arm is descended
|
|
51
|
+
# into: the compiler eliminates the dead arm's entire subtree, so a branch
|
|
52
|
+
# or method nested there would be a phantom no loaded run can produce.
|
|
53
|
+
def visit_if_node(node)
|
|
54
|
+
verdict = folded_condition(node.predicate)
|
|
55
|
+
return visit_folded_arms(verdict, node.statements, PrismCompat.subsequent(node)) if verdict
|
|
56
|
+
|
|
57
|
+
emit_if_like(node, :if)
|
|
58
|
+
super
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def visit_unless_node(node)
|
|
62
|
+
verdict = folded_condition(node.predicate)
|
|
63
|
+
return visit_folded_arms(verdict, PrismCompat.else_clause(node), node.statements) if verdict
|
|
64
|
+
|
|
65
|
+
emit_if_like(node, :unless)
|
|
66
|
+
super
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def visit_call_node(node)
|
|
70
|
+
emit_safe_navigation(node) if node.respond_to?(:safe_navigation?) && node.safe_navigation?
|
|
71
|
+
super
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# When there's no explicit `else`, Coverage synthesizes one at the case's
|
|
75
|
+
# range.
|
|
76
|
+
def visit_case_node(node)
|
|
77
|
+
emit_case_like(node, :when)
|
|
78
|
+
super
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def visit_case_match_node(node)
|
|
82
|
+
emit_case_like(node, :in)
|
|
83
|
+
super
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# One-line pattern matching: `x => pattern` and `x in pattern`. Ruby 3.3's
|
|
87
|
+
# Coverage reports these as a `:case` with an `:in` and an `:else` arm;
|
|
88
|
+
# 3.4 dropped them entirely, so this is legacy-only. The two forms differ
|
|
89
|
+
# only in where Coverage anchors the synthesized `:else`: `=>` uses the
|
|
90
|
+
# whole expression, `in` uses just the pattern.
|
|
91
|
+
def visit_match_required_node(node)
|
|
92
|
+
emit_oneline_pattern(node, node) if LEGACY_COVERAGE_LOCATIONS
|
|
93
|
+
super
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def visit_match_predicate_node(node)
|
|
97
|
+
emit_oneline_pattern(node, node.pattern) if LEGACY_COVERAGE_LOCATIONS
|
|
98
|
+
super
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# A loop gets a single `:body` arm and no synthetic else.
|
|
102
|
+
def visit_while_node(node)
|
|
103
|
+
emit_loop(node, :while)
|
|
104
|
+
super
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def visit_until_node(node)
|
|
108
|
+
emit_loop(node, :until)
|
|
109
|
+
super
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
# IfNode and UnlessNode share a shape but expose the trailing arm under
|
|
115
|
+
# different accessors, which `if_like_else_location` hides.
|
|
116
|
+
def emit_if_like(node, type)
|
|
117
|
+
then_loc = if_like_then_location(node, type)
|
|
118
|
+
else_loc = if_like_else_location(node, type)
|
|
119
|
+
@branches[build_tuple(type, if_like_location(node, type))] = {
|
|
120
|
+
build_tuple(:then, then_loc) => 0,
|
|
121
|
+
build_tuple(:else, else_loc) => 0
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def emit_safe_navigation(node)
|
|
126
|
+
loc = safe_navigation_location(node)
|
|
127
|
+
@branches[build_tuple(:"&.", loc)] = {
|
|
128
|
+
build_tuple(:then, loc) => 0,
|
|
129
|
+
build_tuple(:else, loc) => 0
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def emit_oneline_pattern(node, else_span)
|
|
134
|
+
@branches[build_tuple(:case, node)] = {
|
|
135
|
+
build_tuple(:in, node.pattern) => 0,
|
|
136
|
+
build_tuple(:else, else_span) => 0
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def emit_case_like(node, when_type)
|
|
141
|
+
arms = node.conditions.to_h do |when_node|
|
|
142
|
+
[build_tuple(when_type, case_arm_location(node, when_node, when_type)), 0]
|
|
143
|
+
end
|
|
144
|
+
arms[build_tuple(:else, else_arm_location(node))] = 0
|
|
145
|
+
@branches[build_tuple(:case, node)] = arms
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def emit_loop(node, type)
|
|
149
|
+
cond_tuple = build_tuple(type, node)
|
|
150
|
+
@branches[cond_tuple] = {build_tuple(:body, loop_body_location(node)) => 0}
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# `span` is anything that answers the four position accessors: a location
|
|
154
|
+
# from LocationConventions, or the node itself where the range wanted is
|
|
155
|
+
# the node's own.
|
|
156
|
+
def build_tuple(type, span)
|
|
157
|
+
id = @next_id
|
|
158
|
+
@next_id += 1
|
|
159
|
+
[type, id, span.start_line, span.start_column, span.end_line, span.end_column]
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|