rigortype 0.1.16 → 0.1.17

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.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +100 -0
  3. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +209 -0
  4. data/lib/rigor/analysis/check_rules.rb +149 -70
  5. data/lib/rigor/analysis/dependency_recorder.rb +122 -0
  6. data/lib/rigor/analysis/diagnostic.rb +18 -0
  7. data/lib/rigor/analysis/incremental.rb +162 -0
  8. data/lib/rigor/analysis/incremental_session.rb +337 -0
  9. data/lib/rigor/analysis/rule_catalog.rb +48 -0
  10. data/lib/rigor/analysis/runner.rb +434 -37
  11. data/lib/rigor/analysis/self_call_resolution_recorder.rb +121 -0
  12. data/lib/rigor/builtins/static_return_refinements.rb +7 -1
  13. data/lib/rigor/cache/descriptor.rb +50 -49
  14. data/lib/rigor/cache/incremental_snapshot.rb +147 -0
  15. data/lib/rigor/cache/rbs_cache_producer.rb +30 -0
  16. data/lib/rigor/cache/rbs_class_ancestor_table.rb +2 -8
  17. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -8
  18. data/lib/rigor/cache/rbs_constant_table.rb +2 -8
  19. data/lib/rigor/cache/rbs_environment.rb +2 -8
  20. data/lib/rigor/cache/rbs_instance_definitions.rb +3 -16
  21. data/lib/rigor/cache/rbs_known_class_names.rb +2 -8
  22. data/lib/rigor/cache/store.rb +99 -1
  23. data/lib/rigor/cli/annotate_command.rb +2 -7
  24. data/lib/rigor/cli/baseline_command.rb +2 -7
  25. data/lib/rigor/cli/command.rb +47 -0
  26. data/lib/rigor/cli/coverage_command.rb +3 -23
  27. data/lib/rigor/cli/coverage_renderer.rb +3 -8
  28. data/lib/rigor/cli/diff_command.rb +3 -7
  29. data/lib/rigor/cli/explain_command.rb +2 -7
  30. data/lib/rigor/cli/lsp_command.rb +3 -7
  31. data/lib/rigor/cli/mcp_command.rb +3 -7
  32. data/lib/rigor/cli/options.rb +57 -0
  33. data/lib/rigor/cli/plugin_command.rb +3 -7
  34. data/lib/rigor/cli/plugins_command.rb +2 -7
  35. data/lib/rigor/cli/renderable.rb +26 -0
  36. data/lib/rigor/cli/sig_gen_command.rb +2 -7
  37. data/lib/rigor/cli/skill_command.rb +3 -7
  38. data/lib/rigor/cli/triage_command.rb +2 -7
  39. data/lib/rigor/cli/type_of_command.rb +5 -38
  40. data/lib/rigor/cli/type_of_renderer.rb +4 -9
  41. data/lib/rigor/cli/type_scan_command.rb +3 -23
  42. data/lib/rigor/cli/type_scan_renderer.rb +4 -9
  43. data/lib/rigor/cli.rb +125 -43
  44. data/lib/rigor/configuration/dependencies.rb +18 -1
  45. data/lib/rigor/configuration/severity_profile.rb +22 -3
  46. data/lib/rigor/configuration.rb +13 -3
  47. data/lib/rigor/environment/rbs_loader.rb +76 -3
  48. data/lib/rigor/inference/block_parameter_binder.rb +1 -2
  49. data/lib/rigor/inference/builtins/array_catalog.rb +2 -5
  50. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -5
  51. data/lib/rigor/inference/builtins/complex_catalog.rb +2 -5
  52. data/lib/rigor/inference/builtins/date_catalog.rb +2 -5
  53. data/lib/rigor/inference/builtins/encoding_catalog.rb +2 -5
  54. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -5
  55. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -5
  56. data/lib/rigor/inference/builtins/hash_catalog.rb +2 -5
  57. data/lib/rigor/inference/builtins/method_catalog.rb +15 -0
  58. data/lib/rigor/inference/builtins/numeric_catalog.rb +21 -93
  59. data/lib/rigor/inference/builtins/pathname_catalog.rb +2 -5
  60. data/lib/rigor/inference/builtins/proc_catalog.rb +2 -5
  61. data/lib/rigor/inference/builtins/random_catalog.rb +2 -5
  62. data/lib/rigor/inference/builtins/range_catalog.rb +2 -5
  63. data/lib/rigor/inference/builtins/rational_catalog.rb +2 -5
  64. data/lib/rigor/inference/builtins/re_catalog.rb +2 -5
  65. data/lib/rigor/inference/builtins/set_catalog.rb +2 -5
  66. data/lib/rigor/inference/builtins/string_catalog.rb +2 -5
  67. data/lib/rigor/inference/builtins/struct_catalog.rb +2 -5
  68. data/lib/rigor/inference/builtins/time_catalog.rb +2 -5
  69. data/lib/rigor/inference/expression_typer.rb +140 -20
  70. data/lib/rigor/inference/method_dispatcher/block_folding.rb +5 -1
  71. data/lib/rigor/inference/method_dispatcher/call_context.rb +65 -0
  72. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +11 -10
  73. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +12 -6
  74. data/lib/rigor/inference/method_dispatcher/data_folding.rb +246 -0
  75. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -2
  76. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  77. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -1
  78. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +4 -1
  79. data/lib/rigor/inference/method_dispatcher/math_folding.rb +6 -6
  80. data/lib/rigor/inference/method_dispatcher/method_folding.rb +12 -7
  81. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +23 -13
  82. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +9 -9
  83. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -6
  84. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +120 -9
  85. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +12 -12
  86. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +49 -0
  87. data/lib/rigor/inference/method_dispatcher/time_folding.rb +6 -6
  88. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -9
  89. data/lib/rigor/inference/method_dispatcher.rb +99 -59
  90. data/lib/rigor/inference/narrowing.rb +202 -5
  91. data/lib/rigor/inference/scope_indexer.rb +134 -7
  92. data/lib/rigor/inference/statement_evaluator.rb +105 -26
  93. data/lib/rigor/language_server/buffer_resolution.rb +33 -0
  94. data/lib/rigor/language_server/completion_provider.rb +4 -4
  95. data/lib/rigor/language_server/document_symbol_provider.rb +4 -4
  96. data/lib/rigor/language_server/folding_range_provider.rb +4 -4
  97. data/lib/rigor/language_server/hover_provider.rb +4 -4
  98. data/lib/rigor/language_server/selection_range_provider.rb +4 -4
  99. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  100. data/lib/rigor/plugin/base.rb +20 -4
  101. data/lib/rigor/plugin/registry.rb +39 -1
  102. data/lib/rigor/rbs_extended/conformance_checker.rb +208 -0
  103. data/lib/rigor/rbs_extended.rb +39 -0
  104. data/lib/rigor/scope.rb +123 -9
  105. data/lib/rigor/type/acceptance_router.rb +19 -0
  106. data/lib/rigor/type/accepts_result.rb +3 -10
  107. data/lib/rigor/type/app.rb +3 -7
  108. data/lib/rigor/type/bot.rb +2 -3
  109. data/lib/rigor/type/bound_method.rb +5 -12
  110. data/lib/rigor/type/combinator.rb +17 -0
  111. data/lib/rigor/type/constant.rb +2 -3
  112. data/lib/rigor/type/data_class.rb +80 -0
  113. data/lib/rigor/type/data_instance.rb +100 -0
  114. data/lib/rigor/type/difference.rb +5 -10
  115. data/lib/rigor/type/dynamic.rb +5 -10
  116. data/lib/rigor/type/hash_shape.rb +5 -15
  117. data/lib/rigor/type/integer_range.rb +5 -10
  118. data/lib/rigor/type/intersection.rb +5 -10
  119. data/lib/rigor/type/nominal.rb +5 -10
  120. data/lib/rigor/type/refined.rb +5 -10
  121. data/lib/rigor/type/singleton.rb +5 -10
  122. data/lib/rigor/type/top.rb +2 -3
  123. data/lib/rigor/type/tuple.rb +5 -10
  124. data/lib/rigor/type/union.rb +5 -10
  125. data/lib/rigor/type.rb +2 -0
  126. data/lib/rigor/value_semantics.rb +77 -0
  127. data/lib/rigor/version.rb +1 -1
  128. data/lib/rigor.rb +1 -0
  129. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +12 -2
  130. data/sig/rigor/cache.rbs +19 -0
  131. data/sig/rigor/inference.rbs +22 -0
  132. data/sig/rigor/rbs_extended.rbs +2 -0
  133. data/sig/rigor/scope.rbs +5 -0
  134. data/sig/rigor/type.rbs +58 -1
  135. data/sig/rigor.rbs +6 -1
  136. metadata +22 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2fb4015697adf7cbc9d65b2ca6ff954c8cac5f784ffb9616153d4af8d2505ccc
4
- data.tar.gz: 52ee5acb7d233c0e86c8cdca45151c5f745b4c13d7155fcb15fafdaaed1d6dc9
3
+ metadata.gz: 9379e94547d874f10fc2fcb79e354b16d0459350bdd68a9025dc5bd0ee6bdc3d
4
+ data.tar.gz: 4b773c4952be32d673358758340d85a34f9839890eede1669ff863a779b30d69
5
5
  SHA512:
6
- metadata.gz: 4f4b64afe3dbca26f6224762a2f1a3dd0bd4cade2ea9a61a30e2fba28ed29d3bb6d68bdfa73fb450536e5c7fef45e07a10a6fd0cec693ad228a828ff30905f8e
7
- data.tar.gz: 4b5faa0d8c7295067f5a0f3d335c1800f22659e47b0428942405b07c1d38649163090f3a461e942a82aecaab6172e774ef88ca44f9a569b1392cdeee0be04086
6
+ metadata.gz: '08680ad9e159ff52c031548a45e862539419f0719a251b0b46dc3dddc5313373a7434806024429c1ebf1c580bafcdb7507fed1b2574139c17cb2394129bc0581'
7
+ data.tar.gz: 4469fa97e287f84772543d0525866a733fd0e075c486e8a8dc650b09e31a1a02e1b91602d61b0678de9c2fb914bd999a4cea998e0ab6c39f3ee4970892e3b39c
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Analysis
7
+ module CheckRules
8
+ # ADR-24 slice 4 — read-side companion to `call.self-undefined-method`.
9
+ # Walks a file's parse tree once and collects the qualified names of
10
+ # declarations that are NOT confidently closed for self-call
11
+ # undefined-method analysis, so the rule can exclude them:
12
+ #
13
+ # - **Modules** — a `module M` is a mixin contract; its methods may be
14
+ # provided by includers (template-method pattern), so an unresolved
15
+ # self-call inside it is not provably a typo.
16
+ # - **Classes with a dynamic `attr_*` accessor** — `attr_reader(*NAMES)`
17
+ # / `attr_accessor(:a, SOME_CONST)` synthesizes readers whose names
18
+ # are not statically decidable, so the class's method surface cannot
19
+ # be enumerated from source.
20
+ #
21
+ # The enclosing declaration of a self-call is always in the same file
22
+ # as the call (even a reopened class restates `class Foo`), so a
23
+ # per-file AST scan suffices — no cross-file plumbing. Names are built
24
+ # to match the engine's qualified `self` type (`Module.nesting`-style
25
+ # `A::B::C`).
26
+ class SelfClosednessScanner
27
+ ATTR_MACROS = %i[attr_reader attr_accessor attr_writer].freeze
28
+ private_constant :ATTR_MACROS
29
+
30
+ def initialize(root)
31
+ @root = root
32
+ end
33
+
34
+ # @return [Set<String>] qualified names to exclude from the rule.
35
+ def open_class_names
36
+ names = Set.new
37
+ walk(@root, [], names)
38
+ names
39
+ end
40
+
41
+ private
42
+
43
+ def walk(node, prefix, names)
44
+ return unless node.is_a?(Prism::Node)
45
+
46
+ case node
47
+ when Prism::ModuleNode
48
+ name = constant_path_name(node.constant_path)
49
+ child_prefix = name ? prefix + [name] : prefix
50
+ names << child_prefix.join("::") if name
51
+ walk(node.body, child_prefix, names) if node.body
52
+ when Prism::ClassNode
53
+ name = constant_path_name(node.constant_path)
54
+ child_prefix = name ? prefix + [name] : prefix
55
+ names << child_prefix.join("::") if name && dynamic_attr_class?(node)
56
+ walk(node.body, child_prefix, names) if node.body
57
+ else
58
+ node.compact_child_nodes.each { |child| walk(child, prefix, names) }
59
+ end
60
+ end
61
+
62
+ # True when the class body directly invokes an `attr_*` macro with a
63
+ # non-literal argument (a splat, a constant, a method call) — the
64
+ # synthesized accessor names are then not statically knowable.
65
+ def dynamic_attr_class?(class_node)
66
+ body = class_node.body
67
+ return false unless body.is_a?(Prism::StatementsNode)
68
+
69
+ body.body.any? do |stmt|
70
+ stmt.is_a?(Prism::CallNode) &&
71
+ stmt.receiver.nil? &&
72
+ ATTR_MACROS.include?(stmt.name) &&
73
+ dynamic_attr_arguments?(stmt)
74
+ end
75
+ end
76
+
77
+ def dynamic_attr_arguments?(call_node)
78
+ args = call_node.arguments&.arguments
79
+ return false if args.nil? || args.empty?
80
+
81
+ args.any? { |arg| !arg.is_a?(Prism::SymbolNode) && !arg.is_a?(Prism::StringNode) }
82
+ end
83
+
84
+ # Renders a class/module path node to its source-qualified name
85
+ # (`Foo`, `Foo::Bar`, leading `::` stripped); nil for a dynamic
86
+ # constant path the scanner cannot name statically.
87
+ def constant_path_name(node)
88
+ case node
89
+ when Prism::ConstantReadNode
90
+ node.name.to_s
91
+ when Prism::ConstantPathNode
92
+ parent = node.parent ? constant_path_name(node.parent) : nil
93
+ child = node.name.to_s
94
+ node.parent.nil? ? child : (parent && "#{parent}::#{child}")
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Analysis
7
+ module CheckRules
8
+ # ADR-47 — collects `case`/`when` clauses proven unreachable. The flow
9
+ # engine already narrows the case subject across `when` branches
10
+ # (`Narrowing.case_when_scopes`), and the per-clause `body_scope` it
11
+ # produces is recorded in `scope_index` keyed on the `WhenNode` (the
12
+ # evaluator enters the clause under that scope). So a clause whose
13
+ # narrowed subject local is `Type::Bot` is one the engine's own
14
+ # narrowing proves no reaching value can match — a stale `when`,
15
+ # mis-ordered clauses, or a type that moved out from under a branch.
16
+ #
17
+ # WD1 (per-clause disjointness) — the narrow, high-value core. Fires
18
+ # ONLY on the safe shape:
19
+ #
20
+ # - the subject is a `case <local>` (a `LocalVariableReadNode`, the
21
+ # only shape `case_when_scopes` narrows — no narrowing ⇒ no firing),
22
+ # - every `when` condition is a class/module constant (`when String` /
23
+ # `when MyClass`), the shape the narrowing recognises — `when nil` /
24
+ # ranges / regexps / arbitrary expressions are out of scope here,
25
+ # - the subject's type at case entry is a concrete carrier, never
26
+ # `Type::Dynamic` (disjointness is never provable under gradual
27
+ # `Dynamic`, preserving the gradual guarantee) and never already
28
+ # `Type::Bot` (dead code, not a clause error),
29
+ # - and the clause's narrowed `body_scope` subject is `Type::Bot`.
30
+ #
31
+ # The false-positive envelope mirrors `flow.always-truthy-condition`:
32
+ # clauses inside loops / blocks are skipped (mutation tracking through
33
+ # those is incomplete), and the rule reads the engine's own narrowing
34
+ # rather than recomputing it, so the diagnostic and the body typing
35
+ # can never diverge.
36
+ class UnreachableClauseCollector
37
+ LOOP_OR_BLOCK_NODE_CLASSES = [
38
+ Prism::WhileNode, Prism::UntilNode, Prism::ForNode, Prism::BlockNode
39
+ ].freeze
40
+
41
+ # A trailing `else` whose body only re-raises / aborts is a
42
+ # deliberate defensive guard ("this should be unreachable — shout
43
+ # if it ever isn't"), NOT dead code the author wants removed.
44
+ # Flagging it would push the author to delete a safety net, which
45
+ # the false-positive discipline forbids. WD1's `when` shapes don't
46
+ # carry this idiom (a stale `when` is a real redundancy), so the
47
+ # skip is `else`-only.
48
+ DEFENSIVE_EXIT_CALLS = %i[raise fail throw abort exit].freeze
49
+
50
+ # @!attribute kind
51
+ # @return [Symbol] `:disjoint` (this clause's type is disjoint
52
+ # from the still-possible subject), `:prior_exhaustion` (an
53
+ # earlier clause already covered the subject), or
54
+ # `:exhausted_else` (every subject value is covered, so the
55
+ # `else` never runs).
56
+ Result = Data.define(:clause, :body, :subject_name, :condition_source, :kind, :keyword)
57
+
58
+ def initialize(scope_index)
59
+ @scope_index = scope_index
60
+ @results = []
61
+ end
62
+
63
+ # @return [Array<Result>] one entry per provably-dead `when` clause.
64
+ def collect(root)
65
+ walk(root, in_loop_or_block: false)
66
+ @results.freeze
67
+ end
68
+
69
+ private
70
+
71
+ def walk(node, in_loop_or_block:)
72
+ return unless node.is_a?(Prism::Node)
73
+
74
+ collect_case(node) if case_like?(node) && !in_loop_or_block
75
+
76
+ child_in_loop_or_block = in_loop_or_block || enters_loop_or_block?(node)
77
+ node.compact_child_nodes.each { |child| walk(child, in_loop_or_block: child_in_loop_or_block) }
78
+ end
79
+
80
+ # `case/when` is a `CaseNode`; `case/in` is a `CaseMatchNode`. Both
81
+ # carry `predicate` + `conditions` + `else_clause` and the engine
82
+ # narrows both through `eval_case_when_branches`.
83
+ def case_like?(node)
84
+ node.is_a?(Prism::CaseNode) || node.is_a?(Prism::CaseMatchNode)
85
+ end
86
+
87
+ def enters_loop_or_block?(node)
88
+ LOOP_OR_BLOCK_NODE_CLASSES.any? { |klass| node.is_a?(klass) }
89
+ end
90
+
91
+ def collect_case(node)
92
+ subject = node.predicate
93
+ return unless subject.is_a?(Prism::LocalVariableReadNode)
94
+
95
+ entry_type = entry_subject_type(node, subject.name)
96
+ # Only meaningful for a concrete subject type: a `Dynamic` subject
97
+ # can never be proven disjoint (the gradual guarantee), and an
98
+ # already-`Bot` subject is dead code, not a clause-ordering error.
99
+ return if entry_type.nil? || entry_type.is_a?(Type::Bot) || entry_type.is_a?(Type::Dynamic)
100
+
101
+ keyword = node.is_a?(Prism::CaseMatchNode) ? "in" : "when"
102
+ node.conditions.each do |clause|
103
+ case clause
104
+ when Prism::WhenNode then collect_when(clause, subject.name)
105
+ when Prism::InNode then collect_in(clause, subject.name)
106
+ end
107
+ end
108
+ collect_else(node.else_clause, subject.name, keyword)
109
+ end
110
+
111
+ def entry_subject_type(case_node, subject_name)
112
+ scope = @scope_index[case_node]
113
+ scope&.local(subject_name)
114
+ end
115
+
116
+ def collect_when(clause, subject_name)
117
+ return if clause.statements.nil? # empty body → no useful location
118
+ return unless all_constant_conditions?(clause)
119
+
120
+ scope = @scope_index[clause]
121
+ return if scope.nil?
122
+ return unless scope.local(subject_name).is_a?(Type::Bot)
123
+
124
+ @results << Result.new(
125
+ clause: clause, body: clause.statements, subject_name: subject_name,
126
+ condition_source: clause.conditions.map(&:slice).join(", "),
127
+ kind: when_clause_kind(clause, subject_name), keyword: "when"
128
+ )
129
+ end
130
+
131
+ # A dead `when` is `:prior_exhaustion` when the subject was
132
+ # already narrowed to `bot` BEFORE this clause (an earlier clause
133
+ # covered it) — read from the entry scope the engine recorded on
134
+ # the clause's first condition node (ADR-47 WD2). Otherwise the
135
+ # subject was still concrete entering the clause and THIS clause's
136
+ # type is disjoint from it.
137
+ def when_clause_kind(clause, subject_name)
138
+ entry = @scope_index[clause.conditions.first]
139
+ return :prior_exhaustion if entry && entry.local(subject_name).is_a?(Type::Bot)
140
+
141
+ :disjoint
142
+ end
143
+
144
+ # ADR-47 WD3a — a dead `case/in` clause. The body scope reaches
145
+ # `bot` either because a bare class pattern (`in C` / `in C => x`)
146
+ # is disjoint from the still-possible subject (the engine narrows
147
+ # those soundly, like `when C`), or because an earlier clause
148
+ # already exhausted the subject (prior-exhaustion — true for ANY
149
+ # pattern downstream of a covering set). Non-bare patterns are not
150
+ # narrowed, so their body is `bot` ONLY under prior-exhaustion;
151
+ # they never produce a spurious `:disjoint`.
152
+ def collect_in(clause, subject_name)
153
+ return if clause.statements.nil?
154
+
155
+ scope = @scope_index[clause]
156
+ return if scope.nil?
157
+ return unless scope.local(subject_name).is_a?(Type::Bot)
158
+
159
+ @results << Result.new(
160
+ clause: clause, body: clause.statements, subject_name: subject_name,
161
+ condition_source: clause.pattern.slice,
162
+ kind: in_clause_kind(clause, subject_name), keyword: "in"
163
+ )
164
+ end
165
+
166
+ def in_clause_kind(clause, subject_name)
167
+ entry = @scope_index[clause.pattern]
168
+ return :prior_exhaustion if entry && entry.local(subject_name).is_a?(Type::Bot)
169
+
170
+ :disjoint
171
+ end
172
+
173
+ # The trailing `else` is dead when every subject value is covered
174
+ # by the `when` clauses — the final falsey scope (recorded on the
175
+ # `else` node) narrows the subject to `bot`. Defensive `else`
176
+ # bodies (a bare `raise` / `fail` / `throw`) are deliberate
177
+ # guards, not removable dead code, so they are skipped.
178
+ def collect_else(else_clause, subject_name, keyword)
179
+ return if else_clause.nil? || else_clause.statements.nil?
180
+ return if defensive_else?(else_clause)
181
+
182
+ scope = @scope_index[else_clause]
183
+ return if scope.nil?
184
+ return unless scope.local(subject_name).is_a?(Type::Bot)
185
+
186
+ @results << Result.new(
187
+ clause: else_clause, body: else_clause.statements, subject_name: subject_name,
188
+ condition_source: nil, kind: :exhausted_else, keyword: keyword
189
+ )
190
+ end
191
+
192
+ def defensive_else?(else_clause)
193
+ body = else_clause.statements.body
194
+ return false unless body.size == 1
195
+
196
+ stmt = body.first
197
+ stmt.is_a?(Prism::CallNode) && stmt.receiver.nil? &&
198
+ DEFENSIVE_EXIT_CALLS.include?(stmt.name)
199
+ end
200
+
201
+ def all_constant_conditions?(clause)
202
+ conditions = clause.conditions
203
+ !conditions.empty? &&
204
+ conditions.all? { |c| c.is_a?(Prism::ConstantReadNode) || c.is_a?(Prism::ConstantPathNode) }
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end