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.
- checksums.yaml +4 -4
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +100 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +209 -0
- data/lib/rigor/analysis/check_rules.rb +149 -70
- data/lib/rigor/analysis/dependency_recorder.rb +122 -0
- data/lib/rigor/analysis/diagnostic.rb +18 -0
- data/lib/rigor/analysis/incremental.rb +162 -0
- data/lib/rigor/analysis/incremental_session.rb +337 -0
- data/lib/rigor/analysis/rule_catalog.rb +48 -0
- data/lib/rigor/analysis/runner.rb +434 -37
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +121 -0
- data/lib/rigor/builtins/static_return_refinements.rb +7 -1
- data/lib/rigor/cache/descriptor.rb +50 -49
- data/lib/rigor/cache/incremental_snapshot.rb +147 -0
- data/lib/rigor/cache/rbs_cache_producer.rb +30 -0
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +2 -8
- data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -8
- data/lib/rigor/cache/rbs_constant_table.rb +2 -8
- data/lib/rigor/cache/rbs_environment.rb +2 -8
- data/lib/rigor/cache/rbs_instance_definitions.rb +3 -16
- data/lib/rigor/cache/rbs_known_class_names.rb +2 -8
- data/lib/rigor/cache/store.rb +99 -1
- data/lib/rigor/cli/annotate_command.rb +2 -7
- data/lib/rigor/cli/baseline_command.rb +2 -7
- data/lib/rigor/cli/command.rb +47 -0
- data/lib/rigor/cli/coverage_command.rb +3 -23
- data/lib/rigor/cli/coverage_renderer.rb +3 -8
- data/lib/rigor/cli/diff_command.rb +3 -7
- data/lib/rigor/cli/explain_command.rb +2 -7
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mcp_command.rb +3 -7
- data/lib/rigor/cli/options.rb +57 -0
- data/lib/rigor/cli/plugin_command.rb +3 -7
- data/lib/rigor/cli/plugins_command.rb +2 -7
- data/lib/rigor/cli/renderable.rb +26 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -7
- data/lib/rigor/cli/skill_command.rb +3 -7
- data/lib/rigor/cli/triage_command.rb +2 -7
- data/lib/rigor/cli/type_of_command.rb +5 -38
- data/lib/rigor/cli/type_of_renderer.rb +4 -9
- data/lib/rigor/cli/type_scan_command.rb +3 -23
- data/lib/rigor/cli/type_scan_renderer.rb +4 -9
- data/lib/rigor/cli.rb +125 -43
- data/lib/rigor/configuration/dependencies.rb +18 -1
- data/lib/rigor/configuration/severity_profile.rb +22 -3
- data/lib/rigor/configuration.rb +13 -3
- data/lib/rigor/environment/rbs_loader.rb +76 -3
- data/lib/rigor/inference/block_parameter_binder.rb +1 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/complex_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/date_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/encoding_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/exception_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/hash_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/method_catalog.rb +15 -0
- data/lib/rigor/inference/builtins/numeric_catalog.rb +21 -93
- data/lib/rigor/inference/builtins/pathname_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/proc_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/random_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/range_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/rational_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/re_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/set_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/string_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/struct_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/time_catalog.rb +2 -5
- data/lib/rigor/inference/expression_typer.rb +140 -20
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +5 -1
- data/lib/rigor/inference/method_dispatcher/call_context.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +11 -10
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +12 -6
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +246 -0
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -1
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +4 -1
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +12 -7
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +23 -13
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +9 -9
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +120 -9
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +12 -12
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +49 -0
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -9
- data/lib/rigor/inference/method_dispatcher.rb +99 -59
- data/lib/rigor/inference/narrowing.rb +202 -5
- data/lib/rigor/inference/scope_indexer.rb +134 -7
- data/lib/rigor/inference/statement_evaluator.rb +105 -26
- data/lib/rigor/language_server/buffer_resolution.rb +33 -0
- data/lib/rigor/language_server/completion_provider.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +4 -4
- data/lib/rigor/language_server/folding_range_provider.rb +4 -4
- data/lib/rigor/language_server/hover_provider.rb +4 -4
- data/lib/rigor/language_server/selection_range_provider.rb +4 -4
- data/lib/rigor/language_server/signature_help_provider.rb +4 -4
- data/lib/rigor/plugin/base.rb +20 -4
- data/lib/rigor/plugin/registry.rb +39 -1
- data/lib/rigor/rbs_extended/conformance_checker.rb +208 -0
- data/lib/rigor/rbs_extended.rb +39 -0
- data/lib/rigor/scope.rb +123 -9
- data/lib/rigor/type/acceptance_router.rb +19 -0
- data/lib/rigor/type/accepts_result.rb +3 -10
- data/lib/rigor/type/app.rb +3 -7
- data/lib/rigor/type/bot.rb +2 -3
- data/lib/rigor/type/bound_method.rb +5 -12
- data/lib/rigor/type/combinator.rb +17 -0
- data/lib/rigor/type/constant.rb +2 -3
- data/lib/rigor/type/data_class.rb +80 -0
- data/lib/rigor/type/data_instance.rb +100 -0
- data/lib/rigor/type/difference.rb +5 -10
- data/lib/rigor/type/dynamic.rb +5 -10
- data/lib/rigor/type/hash_shape.rb +5 -15
- data/lib/rigor/type/integer_range.rb +5 -10
- data/lib/rigor/type/intersection.rb +5 -10
- data/lib/rigor/type/nominal.rb +5 -10
- data/lib/rigor/type/refined.rb +5 -10
- data/lib/rigor/type/singleton.rb +5 -10
- data/lib/rigor/type/top.rb +2 -3
- data/lib/rigor/type/tuple.rb +5 -10
- data/lib/rigor/type/union.rb +5 -10
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/value_semantics.rb +77 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +12 -2
- data/sig/rigor/cache.rbs +19 -0
- data/sig/rigor/inference.rbs +22 -0
- data/sig/rigor/rbs_extended.rbs +2 -0
- data/sig/rigor/scope.rbs +5 -0
- data/sig/rigor/type.rbs +58 -1
- data/sig/rigor.rbs +6 -1
- metadata +22 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9379e94547d874f10fc2fcb79e354b16d0459350bdd68a9025dc5bd0ee6bdc3d
|
|
4
|
+
data.tar.gz: 4b773c4952be32d673358758340d85a34f9839890eede1669ff863a779b30d69
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|