rigortype 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +373 -0
  3. data/README.md +152 -0
  4. data/exe/rigor +9 -0
  5. data/lib/rigor/analysis/check_rules.rb +503 -0
  6. data/lib/rigor/analysis/diagnostic.rb +35 -0
  7. data/lib/rigor/analysis/fact_store.rb +133 -0
  8. data/lib/rigor/analysis/result.rb +29 -0
  9. data/lib/rigor/analysis/runner.rb +119 -0
  10. data/lib/rigor/ast/type_node.rb +41 -0
  11. data/lib/rigor/ast.rb +22 -0
  12. data/lib/rigor/cli/type_of_command.rb +160 -0
  13. data/lib/rigor/cli/type_of_renderer.rb +88 -0
  14. data/lib/rigor/cli/type_scan_command.rb +160 -0
  15. data/lib/rigor/cli/type_scan_renderer.rb +165 -0
  16. data/lib/rigor/cli/type_scan_report.rb +32 -0
  17. data/lib/rigor/cli.rb +195 -0
  18. data/lib/rigor/configuration.rb +49 -0
  19. data/lib/rigor/environment/class_registry.rb +141 -0
  20. data/lib/rigor/environment/rbs_hierarchy.rb +64 -0
  21. data/lib/rigor/environment/rbs_loader.rb +244 -0
  22. data/lib/rigor/environment.rb +177 -0
  23. data/lib/rigor/inference/acceptance.rb +444 -0
  24. data/lib/rigor/inference/block_parameter_binder.rb +198 -0
  25. data/lib/rigor/inference/closure_escape_analyzer.rb +191 -0
  26. data/lib/rigor/inference/coverage_scanner.rb +85 -0
  27. data/lib/rigor/inference/expression_typer.rb +831 -0
  28. data/lib/rigor/inference/fallback.rb +35 -0
  29. data/lib/rigor/inference/fallback_tracer.rb +64 -0
  30. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +102 -0
  31. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +169 -0
  32. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +421 -0
  33. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +336 -0
  34. data/lib/rigor/inference/method_dispatcher.rb +213 -0
  35. data/lib/rigor/inference/method_parameter_binder.rb +257 -0
  36. data/lib/rigor/inference/multi_target_binder.rb +143 -0
  37. data/lib/rigor/inference/narrowing.rb +1008 -0
  38. data/lib/rigor/inference/rbs_type_translator.rb +219 -0
  39. data/lib/rigor/inference/scope_indexer.rb +468 -0
  40. data/lib/rigor/inference/statement_evaluator.rb +1017 -0
  41. data/lib/rigor/rbs_extended.rb +98 -0
  42. data/lib/rigor/scope.rb +340 -0
  43. data/lib/rigor/source/node_locator.rb +104 -0
  44. data/lib/rigor/source/node_walker.rb +37 -0
  45. data/lib/rigor/source.rb +15 -0
  46. data/lib/rigor/testing.rb +65 -0
  47. data/lib/rigor/trinary.rb +108 -0
  48. data/lib/rigor/type/accepts_result.rb +109 -0
  49. data/lib/rigor/type/bot.rb +57 -0
  50. data/lib/rigor/type/combinator.rb +148 -0
  51. data/lib/rigor/type/constant.rb +90 -0
  52. data/lib/rigor/type/dynamic.rb +60 -0
  53. data/lib/rigor/type/hash_shape.rb +246 -0
  54. data/lib/rigor/type/nominal.rb +83 -0
  55. data/lib/rigor/type/singleton.rb +65 -0
  56. data/lib/rigor/type/top.rb +56 -0
  57. data/lib/rigor/type/tuple.rb +84 -0
  58. data/lib/rigor/type/union.rb +65 -0
  59. data/lib/rigor/type.rb +23 -0
  60. data/lib/rigor/version.rb +5 -0
  61. data/lib/rigor.rb +29 -0
  62. data/sig/rigor/analysis/fact_store.rbs +51 -0
  63. data/sig/rigor/ast.rbs +11 -0
  64. data/sig/rigor/environment.rbs +59 -0
  65. data/sig/rigor/inference.rbs +151 -0
  66. data/sig/rigor/rbs_extended.rbs +22 -0
  67. data/sig/rigor/scope.rbs +49 -0
  68. data/sig/rigor/source.rbs +20 -0
  69. data/sig/rigor/testing.rbs +9 -0
  70. data/sig/rigor/trinary.rbs +29 -0
  71. data/sig/rigor/type.rbs +171 -0
  72. data/sig/rigor.rbs +70 -0
  73. metadata +260 -0
@@ -0,0 +1,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../type"
6
+ require_relative "rbs_type_translator"
7
+
8
+ module Rigor
9
+ module Inference
10
+ # Builds the entry scope of a method body by translating the method's
11
+ # parameter list into a `name -> Rigor::Type` map.
12
+ #
13
+ # Parameter types come from the surrounding class's RBS signature
14
+ # when one is available; otherwise every parameter defaults to
15
+ # `Dynamic[Top]`. The default is the Slice 1 fail-soft answer for
16
+ # unknown values, so a method whose RBS signature is missing or
17
+ # whose parameters cannot be matched still binds every name into
18
+ # the scope (a method body whose `Local x` reads return
19
+ # `Dynamic[Top]` instead of falling through to the unbound-local
20
+ # `Dynamic[Top]` event is the same observable type, but the
21
+ # binding presence is what later slices need to attach narrowing
22
+ # facts to).
23
+ #
24
+ # The class context (`class_path:`) and `singleton:` flag are
25
+ # supplied by the caller (the StatementEvaluator) which threads
26
+ # them as the lexical class scope. The binder makes no assumption
27
+ # about how that context was computed; it only uses it to build
28
+ # the `(class_name, method_name)` lookup key for
29
+ # `Rigor::Environment::RbsLoader#instance_method` /
30
+ # `#singleton_method`.
31
+ #
32
+ # See docs/internal-spec/inference-engine.md for the binding contract.
33
+ # rubocop:disable Metrics/ClassLength
34
+ class MethodParameterBinder
35
+ # @param environment [Rigor::Environment]
36
+ # @param class_path [String, nil] the qualified name of the class
37
+ # the method is defined in (e.g., `"Foo::Bar"`), or `nil` for a
38
+ # top-level `def` outside any class. When `nil` (or when the
39
+ # class is unknown to RBS), every parameter falls back to
40
+ # `Dynamic[Top]`.
41
+ # @param singleton [Boolean] `true` when the def is a singleton
42
+ # method (either `def self.foo` or a `def foo` inside
43
+ # `class << self`); routes the lookup through
44
+ # `RbsLoader#singleton_method`.
45
+ def initialize(environment:, class_path:, singleton:)
46
+ @environment = environment
47
+ @class_path = class_path
48
+ @singleton = singleton
49
+ end
50
+
51
+ # @param def_node [Prism::DefNode]
52
+ # @return [Hash{Symbol => Rigor::Type}] ordered map from parameter
53
+ # name to bound type. Anonymous parameters (`*` and `**` without
54
+ # a name) are skipped.
55
+ def bind(def_node)
56
+ slots = collect_slots(def_node.parameters)
57
+ types = default_types_for(slots)
58
+
59
+ rbs_method = lookup_rbs_method(def_node)
60
+ return types unless rbs_method
61
+
62
+ method_types = rbs_method.method_types
63
+ return types if method_types.empty?
64
+
65
+ apply_rbs_overloads(types, slots, method_types)
66
+ types
67
+ end
68
+
69
+ private
70
+
71
+ ParamSlot = Data.define(:kind, :name, :index)
72
+ private_constant :ParamSlot
73
+
74
+ # Walk the Prism `ParametersNode` and emit one slot per named
75
+ # parameter, in declaration order. Anonymous slots (rest /
76
+ # keyword-rest with no name) are skipped because we have no
77
+ # local name to bind. The slot's `:index` is the positional
78
+ # index for required/optional/trailing positionals (used to look
79
+ # up the matching RBS function param) and is `nil` for the
80
+ # singleton kinds (`:rest_positional`, `:rest_keyword`,
81
+ # `:block`).
82
+ def collect_slots(params_node)
83
+ return [] if params_node.nil?
84
+
85
+ slots = []
86
+ slots.concat(positional_slots(params_node))
87
+ slots.concat(keyword_slots(params_node))
88
+ append_rest_keyword_slot(slots, params_node)
89
+ append_block_slot(slots, params_node)
90
+ slots
91
+ end
92
+
93
+ def positional_slots(params_node)
94
+ slots = []
95
+ params_node.requireds.each_with_index { |p, i| slots << ParamSlot.new(:required_positional, p.name, i) }
96
+ params_node.optionals.each_with_index { |p, i| slots << ParamSlot.new(:optional_positional, p.name, i) }
97
+ rest = params_node.rest
98
+ slots << ParamSlot.new(:rest_positional, rest.name, nil) if rest.respond_to?(:name) && rest&.name
99
+ params_node.posts.each_with_index { |p, i| slots << ParamSlot.new(:trailing_positional, p.name, i) }
100
+ slots
101
+ end
102
+
103
+ def keyword_slots(params_node)
104
+ params_node.keywords.filter_map do |kw|
105
+ case kw
106
+ when Prism::RequiredKeywordParameterNode
107
+ ParamSlot.new(:required_keyword, kw.name, kw.name)
108
+ when Prism::OptionalKeywordParameterNode
109
+ ParamSlot.new(:optional_keyword, kw.name, kw.name)
110
+ end
111
+ end
112
+ end
113
+
114
+ def append_rest_keyword_slot(slots, params_node)
115
+ kw_rest = params_node.keyword_rest
116
+ return unless kw_rest.respond_to?(:name) && kw_rest&.name
117
+
118
+ slots << ParamSlot.new(:rest_keyword, kw_rest.name, nil)
119
+ end
120
+
121
+ def append_block_slot(slots, params_node)
122
+ block = params_node.block
123
+ return unless block.respond_to?(:name) && block&.name
124
+
125
+ slots << ParamSlot.new(:block, block.name, nil)
126
+ end
127
+
128
+ def default_types_for(slots)
129
+ slots.to_h { |slot| [slot.name, Type::Combinator.untyped] }
130
+ end
131
+
132
+ def lookup_rbs_method(def_node)
133
+ return nil if @class_path.nil?
134
+
135
+ loader = @environment.rbs_loader
136
+ return nil if loader.nil?
137
+
138
+ method_name = def_node.name
139
+ # `def self.foo` always means a singleton method on the
140
+ # immediate enclosing class. `def foo` inside `class << self`
141
+ # is also a singleton method (the StatementEvaluator threads
142
+ # the `singleton:` flag through this case).
143
+ if def_node.receiver.is_a?(Prism::SelfNode) || @singleton
144
+ loader.singleton_method(class_name: @class_path, method_name: method_name)
145
+ else
146
+ loader.instance_method(class_name: @class_path, method_name: method_name)
147
+ end
148
+ end
149
+
150
+ # Bind each parameter slot to the union of the matching parameter
151
+ # types across every overload that *has* that slot. Overloads
152
+ # that omit the slot (e.g., `Array#first` has both `()` and
153
+ # `(int)` overloads — only the second matches a `def first(n)`
154
+ # redefinition) are silently skipped, so the binder defaults to
155
+ # the most informative type the RBS signature provides without
156
+ # having to know which overload the runtime will pick.
157
+ def apply_rbs_overloads(types, slots, method_types)
158
+ slots.each do |slot|
159
+ next if slot.name.nil?
160
+
161
+ translated = collect_translated_types(method_types, slot)
162
+ next if translated.empty?
163
+
164
+ types[slot.name] = build_slot_type(translated, slot.kind)
165
+ end
166
+ end
167
+
168
+ def collect_translated_types(method_types, slot)
169
+ rbs_types = method_types.flat_map do |mt|
170
+ t = rbs_type_for_slot(mt.type, slot)
171
+ t ? [t] : []
172
+ end
173
+ rbs_types.map { |t| translate_with_self(t) }.uniq
174
+ end
175
+
176
+ def build_slot_type(translated, kind)
177
+ bound = translated.size == 1 ? translated.first : Type::Combinator.union(*translated)
178
+ wrap_for_kind(bound, kind)
179
+ end
180
+
181
+ # Dispatch table from slot kind to a small lambda that pulls the
182
+ # matching RBS parameter type out of an `RBS::Types::Function`.
183
+ # The hash keeps `rbs_type_for_slot` linear (one lookup, one
184
+ # call) so the cyclomatic-complexity budget does not balloon as
185
+ # future slices add more parameter kinds (e.g., `**Symbol kw` is
186
+ # a candidate for a stricter route in Slice 5+).
187
+ # Match keyword parameters by name across both required and
188
+ # optional keyword maps. RBS may declare a keyword as optional
189
+ # (`?by:`) while the Ruby `def` lists it as required (or vice
190
+ # versa); the binding is by-name regardless of which side
191
+ # defines it.
192
+ KEYWORD_PROVIDER = lambda do |fn, slot|
193
+ fn.required_keywords[slot.name]&.type || fn.optional_keywords[slot.name]&.type
194
+ end
195
+ private_constant :KEYWORD_PROVIDER
196
+
197
+ RBS_TYPE_PROVIDERS = {
198
+ required_positional: ->(fn, slot) { fn.required_positionals[slot.index]&.type },
199
+ optional_positional: ->(fn, slot) { fn.optional_positionals[slot.index]&.type },
200
+ rest_positional: ->(fn, _slot) { fn.rest_positionals&.type },
201
+ trailing_positional: ->(fn, slot) { fn.trailing_positionals[slot.index]&.type },
202
+ required_keyword: KEYWORD_PROVIDER,
203
+ optional_keyword: KEYWORD_PROVIDER,
204
+ rest_keyword: ->(fn, _slot) { fn.rest_keywords&.type }
205
+ }.freeze
206
+ private_constant :RBS_TYPE_PROVIDERS
207
+
208
+ def rbs_type_for_slot(function, slot)
209
+ provider = RBS_TYPE_PROVIDERS[slot.kind]
210
+ return nil unless provider
211
+
212
+ provider.call(function, slot)
213
+ end
214
+
215
+ # The variable bound to a `*rest` parameter is the *Array* of
216
+ # rest-positional arguments, not a single element. Likewise
217
+ # `**kw` is bound to a `Hash[Symbol, V]`. Wrap the translated
218
+ # element/value type accordingly so `rest` reads as
219
+ # `Array[Integer]` rather than `Integer`.
220
+ def wrap_for_kind(translated, kind)
221
+ case kind
222
+ when :rest_positional
223
+ Type::Combinator.nominal_of("Array", type_args: [translated])
224
+ when :rest_keyword
225
+ symbol_nominal = Type::Combinator.nominal_of("Symbol")
226
+ Type::Combinator.nominal_of("Hash", type_args: [symbol_nominal, translated])
227
+ else
228
+ translated
229
+ end
230
+ end
231
+
232
+ def translate_with_self(rbs_type)
233
+ self_type, instance_type = self_and_instance_type
234
+ RbsTypeTranslator.translate(
235
+ rbs_type,
236
+ self_type: self_type,
237
+ instance_type: instance_type
238
+ )
239
+ rescue StandardError
240
+ Type::Combinator.untyped
241
+ end
242
+
243
+ def self_and_instance_type
244
+ return [nil, nil] if @class_path.nil?
245
+
246
+ instance = @environment.nominal_for_name(@class_path)
247
+ if @singleton
248
+ singleton = @environment.singleton_for_name(@class_path)
249
+ [singleton, instance]
250
+ else
251
+ [instance, instance]
252
+ end
253
+ end
254
+ end
255
+ # rubocop:enable Metrics/ClassLength
256
+ end
257
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../type"
6
+
7
+ module Rigor
8
+ module Inference
9
+ # Slice 5 phase 2 sub-phase 2 destructuring binder.
10
+ #
11
+ # `Rigor::Inference::MultiTargetBinder` decomposes a tuple-shaped
12
+ # right-hand side type against a Prism multi-target tree and
13
+ # produces a `name -> Rigor::Type` binding map. The binder is
14
+ # shared between two surfaces:
15
+ #
16
+ # 1. `Rigor::Inference::StatementEvaluator#eval_multi_write` for
17
+ # the statement-level `a, b = rhs` form (`Prism::MultiWriteNode`).
18
+ # 2. `Rigor::Inference::BlockParameterBinder` for nested
19
+ # destructuring inside block parameter lists
20
+ # (`Prism::MultiTargetNode` under `BlockParametersNode#requireds`).
21
+ #
22
+ # Both Prism nodes share the same `lefts` / `rest` (a
23
+ # `Prism::SplatNode`) / `rights` triple, so the binder treats them
24
+ # uniformly. The binder is pure: it MUST NOT mutate its inputs and
25
+ # MUST return a fresh `Hash` on every call.
26
+ #
27
+ # The binder threads `Type::Tuple` decompositions when the
28
+ # right-hand side carrier is a known-arity tuple. Other carriers
29
+ # (`Nominal[Array]`, `Dynamic[Top]`, `Top`, `Bot`, ...) collapse
30
+ # to `Dynamic[Top]` per slot — Slice 5 phase 2 sub-phase 2 stays
31
+ # conservative on dynamic-arity right-hand sides until the
32
+ # narrower receiver-shape lattice lands.
33
+ #
34
+ # Targets the binder recognises:
35
+ #
36
+ # - `Prism::LocalVariableTargetNode` — used by the statement-level
37
+ # `a, b = rhs` form. Binds `target.name` to its slice of the
38
+ # right-hand side.
39
+ # - `Prism::RequiredParameterNode` — used by block-parameter
40
+ # destructuring (`|(a, b), c|`). Prism encodes the inner names
41
+ # of a block-side `MultiTargetNode` as parameter nodes rather
42
+ # than target nodes; the binder treats them uniformly with
43
+ # their `LocalVariableTargetNode` cousins because they carry
44
+ # the same `name:` field and the same observable semantics
45
+ # (binding a fresh local in the block-entry scope).
46
+ # - `Prism::MultiTargetNode` — recurses with the slot's type as
47
+ # the new right-hand side.
48
+ # - `Prism::SplatNode` (used for `rest`) — its `expression` MUST
49
+ # be a `Prism::LocalVariableTargetNode` or a
50
+ # `Prism::RequiredParameterNode` to be observable; an anonymous
51
+ # `*` splat or a non-local target is skipped.
52
+ #
53
+ # Other target kinds (`InstanceVariableTargetNode`,
54
+ # `ConstantTargetNode`, `IndexTargetNode`, `CallTargetNode`,
55
+ # `ConstantPathTargetNode`, `ImplicitRestNode`, ...) MUST be
56
+ # silently skipped: they have no observable contribution to the
57
+ # local-variable scope the StatementEvaluator threads.
58
+ #
59
+ # See docs/internal-spec/inference-engine.md for the binding
60
+ # contract and docs/adr/4-type-inference-engine.md for the slice
61
+ # rationale.
62
+ module MultiTargetBinder
63
+ module_function
64
+
65
+ # @param target_node [Prism::MultiWriteNode, Prism::MultiTargetNode]
66
+ # @param rhs_type [Rigor::Type] type of the right-hand side
67
+ # @return [Hash{Symbol => Rigor::Type}]
68
+ def bind(target_node, rhs_type)
69
+ bindings = {}
70
+ visit(target_node, rhs_type, bindings)
71
+ bindings
72
+ end
73
+
74
+ class << self
75
+ private
76
+
77
+ def visit(node, rhs_type, bindings)
78
+ lefts = node.lefts || []
79
+ rest = node.rest
80
+ rights = node.rights || []
81
+
82
+ fronts, rest_type, backs = decompose(rhs_type, lefts.size, rights.size, rest_present: !rest.nil?)
83
+ lefts.each_with_index { |t, i| bind_target(t, fronts[i], bindings) }
84
+ bind_rest_target(rest, rest_type, bindings) if rest
85
+ rights.each_with_index { |t, i| bind_target(t, backs[i], bindings) }
86
+ end
87
+
88
+ # Decomposes the right-hand side type into the per-slot
89
+ # types. Returns a `[fronts, rest_type, backs]` triple, with
90
+ # `fronts` and `backs` each an ordered array of length
91
+ # `front_count`/`back_count`, and `rest_type` either a
92
+ # `Rigor::Type` (when `rest_present:` is true) or `nil`.
93
+ def decompose(rhs_type, front_count, back_count, rest_present:)
94
+ if rhs_type.is_a?(Type::Tuple)
95
+ decompose_tuple(rhs_type, front_count, back_count, rest_present: rest_present)
96
+ else
97
+ decompose_default(front_count, back_count, rest_present: rest_present)
98
+ end
99
+ end
100
+
101
+ def decompose_tuple(tuple, front_count, back_count, rest_present:)
102
+ elements = tuple.elements
103
+ fronts = Array.new(front_count) { |i| elements[i] || Type::Combinator.constant_of(nil) }
104
+ if rest_present
105
+ middle_end = [elements.size - back_count, front_count].max
106
+ middle = elements[front_count...middle_end] || []
107
+ rest_type = Type::Combinator.tuple_of(*middle)
108
+ backs = Array.new(back_count) { |i| elements[middle_end + i] || Type::Combinator.constant_of(nil) }
109
+ else
110
+ rest_type = nil
111
+ backs = Array.new(back_count) { |i| elements[front_count + i] || Type::Combinator.constant_of(nil) }
112
+ end
113
+ [fronts, rest_type, backs]
114
+ end
115
+
116
+ def decompose_default(front_count, back_count, rest_present:)
117
+ [
118
+ Array.new(front_count) { Type::Combinator.untyped },
119
+ rest_present ? Type::Combinator.untyped : nil,
120
+ Array.new(back_count) { Type::Combinator.untyped }
121
+ ]
122
+ end
123
+
124
+ def bind_target(target, type, bindings)
125
+ case target
126
+ when Prism::LocalVariableTargetNode, Prism::RequiredParameterNode
127
+ bindings[target.name] = type
128
+ when Prism::MultiTargetNode
129
+ visit(target, type, bindings)
130
+ end
131
+ end
132
+
133
+ def bind_rest_target(splat_node, type, bindings)
134
+ expression = splat_node.expression
135
+ case expression
136
+ when Prism::LocalVariableTargetNode, Prism::RequiredParameterNode
137
+ bindings[expression.name] = type
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end