rigortype 0.0.9 → 0.1.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -2
  3. data/data/builtins/ruby_core/array.yml +6 -6
  4. data/data/builtins/ruby_core/hash.yml +1 -1
  5. data/data/builtins/ruby_core/io.yml +3 -3
  6. data/data/builtins/ruby_core/numeric.yml +1 -1
  7. data/data/builtins/ruby_core/pathname.yml +100 -100
  8. data/data/builtins/ruby_core/proc.yml +1 -1
  9. data/data/builtins/ruby_core/time.yml +3 -3
  10. data/lib/rigor/analysis/check_rules.rb +228 -40
  11. data/lib/rigor/analysis/diagnostic.rb +15 -1
  12. data/lib/rigor/analysis/runner.rb +269 -7
  13. data/lib/rigor/builtins/regex_refinement.rb +104 -0
  14. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  15. data/lib/rigor/cache/rbs_class_type_param_names.rb +1 -1
  16. data/lib/rigor/cache/rbs_constant_table.rb +2 -2
  17. data/lib/rigor/cache/rbs_descriptor.rb +2 -0
  18. data/lib/rigor/cache/rbs_instance_definitions.rb +79 -0
  19. data/lib/rigor/cache/store.rb +2 -0
  20. data/lib/rigor/cli/type_of_command.rb +3 -3
  21. data/lib/rigor/cli/type_scan_command.rb +4 -4
  22. data/lib/rigor/cli.rb +20 -7
  23. data/lib/rigor/configuration/severity_profile.rb +109 -0
  24. data/lib/rigor/configuration.rb +286 -15
  25. data/lib/rigor/environment/rbs_loader.rb +89 -13
  26. data/lib/rigor/environment.rb +12 -4
  27. data/lib/rigor/flow_contribution/conflict.rb +81 -0
  28. data/lib/rigor/flow_contribution/element.rb +53 -0
  29. data/lib/rigor/flow_contribution/fact.rb +88 -0
  30. data/lib/rigor/flow_contribution/merge_result.rb +67 -0
  31. data/lib/rigor/flow_contribution/merger.rb +275 -0
  32. data/lib/rigor/flow_contribution.rb +51 -0
  33. data/lib/rigor/inference/block_parameter_binder.rb +15 -0
  34. data/lib/rigor/inference/expression_typer.rb +87 -6
  35. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +31 -0
  36. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +136 -9
  37. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +21 -1
  38. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +68 -2
  39. data/lib/rigor/inference/method_dispatcher.rb +50 -1
  40. data/lib/rigor/inference/multi_target_binder.rb +2 -0
  41. data/lib/rigor/inference/narrowing.rb +246 -127
  42. data/lib/rigor/inference/scope_indexer.rb +124 -16
  43. data/lib/rigor/inference/statement_evaluator.rb +406 -37
  44. data/lib/rigor/plugin/access_denied_error.rb +24 -0
  45. data/lib/rigor/plugin/base.rb +284 -0
  46. data/lib/rigor/plugin/fact_store.rb +92 -0
  47. data/lib/rigor/plugin/io_boundary.rb +102 -0
  48. data/lib/rigor/plugin/load_error.rb +35 -0
  49. data/lib/rigor/plugin/loader.rb +307 -0
  50. data/lib/rigor/plugin/manifest.rb +203 -0
  51. data/lib/rigor/plugin/registry.rb +50 -0
  52. data/lib/rigor/plugin/services.rb +77 -0
  53. data/lib/rigor/plugin/trust_policy.rb +99 -0
  54. data/lib/rigor/plugin.rb +62 -0
  55. data/lib/rigor/rbs_extended.rb +57 -9
  56. data/lib/rigor/reflection.rb +2 -2
  57. data/lib/rigor/trinary.rb +1 -1
  58. data/lib/rigor/type/integer_range.rb +6 -2
  59. data/lib/rigor/version.rb +1 -1
  60. data/lib/rigor.rb +7 -0
  61. data/sig/rigor/environment.rbs +10 -3
  62. data/sig/rigor/inference.rbs +1 -0
  63. data/sig/rigor/rbs_extended.rbs +2 -0
  64. data/sig/rigor/scope.rbs +1 -0
  65. data/sig/rigor/type.rbs +7 -0
  66. data/sig/rigor.rbs +8 -2
  67. metadata +20 -1
@@ -8,12 +8,21 @@ module Rigor
8
8
  # Dispatcher tier that lifts string-composition results into
9
9
  # the `literal-string` carrier when every operand is itself
10
10
  # literal-bearing. Sits between {ConstantFolding} (which
11
- # handles all-Constant cases) and {ShapeDispatch}; runs only
12
- # for `String#+` / `String#*` / `String#<<` / `String#concat`
13
- # calls whose inputs the ConstantFolding tier could not fold
14
- # to a precise `Constant<String>` (e.g. one operand is
15
- # `literal-string` rather than `Constant<String>`, or the
16
- # multiplication exceeds the constant-fold size cap).
11
+ # handles all-Constant cases) and {ShapeDispatch}; runs for:
12
+ #
13
+ # - `String#+` / `String#*` / `String#<<` / `String#concat`
14
+ # on string-typed receivers whose inputs the
15
+ # ConstantFolding tier could not fold to a precise
16
+ # `Constant<String>` (e.g. one operand is `literal-string`
17
+ # rather than `Constant<String>`, or the multiplication
18
+ # exceeds the constant-fold size cap).
19
+ # - `Array#join` on `Tuple[…]` receivers whose every element
20
+ # plus the separator argument (when given) is
21
+ # literal-bearing.
22
+ # - `Kernel#format` / `Kernel#sprintf` (any receiver) and
23
+ # `String#%` (literal-bearing receiver) when every value
24
+ # argument is literal-bearing or a Type::Constant of any
25
+ # value.
17
26
  #
18
27
  # Result rule:
19
28
  #
@@ -27,6 +36,12 @@ module Rigor
27
36
  # literal-bearing too.
28
37
  # - `*`: receiver MUST be literal-bearing; argument MUST be
29
38
  # integer-typed. The result is `literal-string`.
39
+ # - `join`: receiver MUST be `Tuple[…]` with every element
40
+ # literal-string-compatible; the optional separator
41
+ # argument MUST also be literal-string-compatible.
42
+ # Result: `literal-string`. Empty `Tuple[]` lifts too —
43
+ # `[].join` is the empty string at runtime, which is
44
+ # literal-bearing trivially.
30
45
  #
31
46
  # Other receiver / argument shapes decline so the next tier
32
47
  # (ShapeDispatch / FileFolding / RbsDispatch) takes over and
@@ -36,10 +51,40 @@ module Rigor
36
51
  module_function
37
52
 
38
53
  CONCAT_METHODS = %i[+ << concat].freeze
39
- private_constant :CONCAT_METHODS
54
+ FORMAT_METHODS = %i[format sprintf].freeze
55
+ # v0.1.1 Track 1 slice 5a — methods that, called with no
56
+ # arguments on a literal-bearing receiver, return a value
57
+ # that is also literal-bearing. `#strip` / `#lstrip` /
58
+ # `#rstrip` / `#chomp` (no-arg) / `#chop` strip a known
59
+ # subset of characters from the ends, so the survivors
60
+ # are always a substring of an already-literal value.
61
+ # `#scrub` (no-arg) replaces invalid bytes; a literal-string
62
+ # value comes from source code and is always valid UTF-8,
63
+ # so the result is identical to the receiver. None of
64
+ # these preserve `non-empty-string`-ness (e.g. `" ".strip
65
+ # == ""`); the carrier collapses from `non-empty-literal-string`
66
+ # down to plain `literal-string`.
67
+ LITERAL_PRESERVING_METHODS = %i[strip lstrip rstrip chomp chop scrub].freeze
68
+ # v0.1.1 Track 1 slice 5c — width-padding methods. `center`
69
+ # / `ljust` / `rjust` take a `width` Integer plus an
70
+ # optional literal padding `String`. When the receiver
71
+ # and the (default or supplied) padding are both
72
+ # literal-bearing, the result is literal-bearing too.
73
+ WIDTH_PADDING_METHODS = %i[center ljust rjust].freeze
74
+ private_constant :CONCAT_METHODS, :FORMAT_METHODS,
75
+ :LITERAL_PRESERVING_METHODS, :WIDTH_PADDING_METHODS
76
+
77
+ def try_dispatch(receiver:, method_name:, args:, **) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
78
+ return fold_array_join(receiver, args) if method_name == :join
79
+ return fold_format(args) if FORMAT_METHODS.include?(method_name)
40
80
 
41
- def try_dispatch(receiver:, method_name:, args:, **)
42
81
  return nil unless Type::Combinator.literal_string_compatible?(receiver)
82
+
83
+ return fold_string_percent(args) if method_name == :%
84
+ if args.empty?
85
+ return LITERAL_PRESERVING_METHODS.include?(method_name) ? Type::Combinator.literal_string : nil
86
+ end
87
+ return fold_width_pad(args) if WIDTH_PADDING_METHODS.include?(method_name)
43
88
  return nil unless args.size == 1
44
89
 
45
90
  if CONCAT_METHODS.include?(method_name)
@@ -49,6 +94,23 @@ module Rigor
49
94
  end
50
95
  end
51
96
 
97
+ # `String#center` / `#ljust` / `#rjust` — first argument is
98
+ # the target width (Integer-typed), optional second
99
+ # argument is the padding string (must be literal-bearing
100
+ # for the result to stay literal). The default padding
101
+ # (a space) is always literal so the no-second-arg form
102
+ # passes through. Width is allowed to be any Integer
103
+ # because Ruby's runtime accepts negative widths and
104
+ # widths smaller than the receiver's length without
105
+ # raising.
106
+ def fold_width_pad(args)
107
+ return nil unless [1, 2].include?(args.size)
108
+ return nil unless integer_typed?(args[0])
109
+ return nil if args.size == 2 && !Type::Combinator.literal_string_compatible?(args[1])
110
+
111
+ Type::Combinator.literal_string
112
+ end
113
+
52
114
  def fold_concat(arg_type)
53
115
  return nil unless Type::Combinator.literal_string_compatible?(arg_type)
54
116
 
@@ -62,6 +124,69 @@ module Rigor
62
124
  Type::Combinator.literal_string
63
125
  end
64
126
 
127
+ # `[lit, lit].join(sep)` — receiver must be a Tuple
128
+ # whose every element is literal-bearing; separator
129
+ # (when given) must be literal-bearing too. Multi-arg
130
+ # forms / `Array#join(*args)` splat shapes don't reach
131
+ # here because the dispatcher only routes through this
132
+ # tier when the call resolves to a single named method.
133
+ def fold_array_join(receiver, args)
134
+ return nil unless receiver.is_a?(Type::Tuple)
135
+ return nil unless receiver.elements.all? { |el| Type::Combinator.literal_string_compatible?(el) }
136
+ return nil unless args.size <= 1
137
+ return nil if args.size == 1 && !Type::Combinator.literal_string_compatible?(args.first)
138
+
139
+ Type::Combinator.literal_string
140
+ end
141
+
142
+ # `format("hello %s", lit)` / `sprintf(...)` — template
143
+ # plus every value argument must be literal-bearing
144
+ # ({Type::Combinator.literal_string_compatible?}) or a
145
+ # `Type::Constant` of any value (Constants are always
146
+ # provably literal). The template arg specifically must
147
+ # be literal-bearing — a Constant<Integer> first arg
148
+ # would not be a valid format template, so the
149
+ # `Type::Constant` allowance applies only to subsequent
150
+ # value args.
151
+ def fold_format(args)
152
+ return nil if args.empty?
153
+ return nil unless Type::Combinator.literal_string_compatible?(args.first)
154
+ return nil unless args.drop(1).all? { |arg| literal_or_constant?(arg) }
155
+
156
+ Type::Combinator.literal_string
157
+ end
158
+
159
+ # `"foo %s" % "x"` / `"foo %s" % ["x", "y"]` — receiver
160
+ # is the template (already verified literal-bearing by
161
+ # the caller); arg is either:
162
+ #
163
+ # - a single literal-bearing string / Constant value, or
164
+ # - a Tuple whose every element is literal-bearing or a
165
+ # Constant.
166
+ #
167
+ # Hash-form `%` (e.g. `"%{name}" % {name: "x"}`) is not
168
+ # yet folded — the analyzer's HashShape carrier could
169
+ # support this, but the v0.0.x catalogue declines and
170
+ # widens to Nominal[String].
171
+ def fold_string_percent(args)
172
+ return nil unless args.size == 1
173
+
174
+ arg = args.first
175
+ if arg.is_a?(Type::Tuple)
176
+ return nil unless arg.elements.all? { |el| literal_or_constant?(el) }
177
+
178
+ return Type::Combinator.literal_string
179
+ end
180
+
181
+ return nil unless literal_or_constant?(arg)
182
+
183
+ Type::Combinator.literal_string
184
+ end
185
+
186
+ def literal_or_constant?(type)
187
+ Type::Combinator.literal_string_compatible?(type) || type.is_a?(Type::Constant)
188
+ end
189
+
65
190
  def integer_typed?(type)
66
191
  case type
67
192
  when Type::Constant then type.value.is_a?(Integer)
@@ -80,7 +205,9 @@ module Rigor
80
205
  type.is_a?(Type::Constant) && type.value.is_a?(Integer) && type.value.negative?
81
206
  end
82
207
 
83
- private_class_method :fold_concat, :fold_repeat, :integer_typed?
208
+ private_class_method :fold_concat, :fold_repeat, :fold_array_join,
209
+ :fold_format, :fold_string_percent, :fold_width_pad,
210
+ :literal_or_constant?, :integer_typed?
84
211
  end
85
212
  end
86
213
  end
@@ -243,7 +243,13 @@ module Rigor
243
243
 
244
244
  # rubocop:disable Metrics/ParameterLists
245
245
  def translate_return_type(method_definition, class_name:, kind:, args:, type_vars:, block_type:)
246
- override = RbsExtended.read_return_type_override(method_definition)
246
+ # Slice 4b-3 (ADR-7 § "Slice 4-A/4-B") — read the
247
+ # return-type override through the merger so future
248
+ # plugin / `:rbs_extended` bundles that also assert a
249
+ # `return_type` slot at this call site compose with
250
+ # the RBS::Extended directive instead of silently
251
+ # racing it.
252
+ override = merged_return_type(method_definition)
247
253
  return override if override
248
254
 
249
255
  instance_type = Type::Combinator.nominal_of(class_name)
@@ -274,6 +280,20 @@ module Rigor
274
280
  end
275
281
  # rubocop:enable Metrics/ParameterLists
276
282
 
283
+ # ADR-7 § "Slice 4-A/4-B" — folds the
284
+ # `RBS::Extended` `return:` directive (and any
285
+ # other `return_type`-bearing contribution future
286
+ # slices add at this call site) through the merger
287
+ # before consuming. Returns the merged return type
288
+ # or nil when no contribution overrides the
289
+ # RBS-declared return.
290
+ def merged_return_type(method_definition)
291
+ contribution = RbsExtended.read_flow_contribution(method_definition)
292
+ return nil if contribution.nil?
293
+
294
+ Rigor::FlowContribution::Merger.merge([contribution]).return_type
295
+ end
296
+
277
297
  # When a block type is supplied, locate the method-level
278
298
  # type parameter that the selected overload's block return
279
299
  # type references and bind it to `block_type`. The
@@ -122,10 +122,25 @@ module Rigor
122
122
  Type::Nominal => :dispatch_nominal_size,
123
123
  Type::Difference => :dispatch_difference,
124
124
  Type::Refined => :dispatch_refined,
125
- Type::Intersection => :dispatch_intersection
125
+ Type::Intersection => :dispatch_intersection,
126
+ Type::IntegerRange => :dispatch_integer_range
126
127
  }.freeze
127
128
  private_constant :RECEIVER_HANDLERS
128
129
 
130
+ # v0.1.1 Track 1 slice 5b — `Integer#to_s(base)` on a
131
+ # non-negative `IntegerRange` receiver. The output of
132
+ # `n.to_s(b)` for `n >= 0` is digit-string-only (no
133
+ # leading sign), so when the base is in this table the
134
+ # result lifts to the matching imported refinement.
135
+ # Bases not listed (2, 36, ...) keep the v0.1.0 baseline
136
+ # since Rigor has no carrier for the resulting alphabet.
137
+ TO_S_BASE_REFINEMENTS = {
138
+ 10 => :decimal_int_string,
139
+ 8 => :octal_int_string,
140
+ 16 => :hex_int_string
141
+ }.freeze
142
+ private_constant :TO_S_BASE_REFINEMENTS
143
+
129
144
  def try_dispatch(receiver:, method_name:, args:)
130
145
  args ||= []
131
146
  handler = RECEIVER_HANDLERS[receiver.class]
@@ -184,6 +199,42 @@ module Rigor
184
199
  Type::Combinator.non_negative_int
185
200
  end
186
201
 
202
+ # `IntegerRange#to_s` precision (v0.1.1 Track 1 slice 5b).
203
+ # When the range's lower bound is `>= 0`, every member is
204
+ # a non-negative integer and `to_s(base)` returns a
205
+ # digit-string with no leading sign. The result lifts to
206
+ # the matching imported refinement (`decimal-int-string`
207
+ # for base 10, `octal-int-string` for 8, `hex-int-string`
208
+ # for 16). Signed ranges fall through (the result could
209
+ # carry a `-` sign that no Rigor refinement currently
210
+ # captures), as do bases without a digit-only refinement.
211
+ def dispatch_integer_range(range, method_name, args)
212
+ return nil unless method_name == :to_s
213
+ return nil unless range.lower >= 0
214
+
215
+ base = base_argument(args)
216
+ return nil if base.nil?
217
+
218
+ refinement = TO_S_BASE_REFINEMENTS[base]
219
+ return nil if refinement.nil?
220
+
221
+ Type::Combinator.public_send(refinement)
222
+ end
223
+
224
+ # `to_s` with no argument defaults to base 10. With one
225
+ # argument, the value MUST be a `Constant<Integer>` to
226
+ # be statically known. Anything else (Nominal[Integer]
227
+ # arg, multi-arg, etc.) declines.
228
+ def base_argument(args)
229
+ return 10 if args.empty?
230
+ return nil unless args.size == 1
231
+
232
+ arg = args.first
233
+ return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(Integer)
234
+
235
+ arg.value
236
+ end
237
+
187
238
  # Refinement-aware projections over a `Difference[base,
188
239
  # removed]` receiver. When the removed value is the
189
240
  # empty witness of the base (`Constant[""]` for
@@ -289,7 +340,21 @@ module Rigor
289
340
  %i[octal_int downcase] => :refined_self,
290
341
  %i[octal_int upcase] => :refined_self,
291
342
  %i[hex_int downcase] => :refined_self,
292
- %i[hex_int upcase] => :refined_self
343
+ %i[hex_int upcase] => :refined_self,
344
+ # v0.1.1 Track 1 slice 2 — `to_i` / `to_int` on a
345
+ # known digit-only string. `decimal-int-string`
346
+ # (`/\A\d+\z/`) and `numeric-string` (Rigor's
347
+ # numeric-string predicate, ASCII digits) are
348
+ # predicates over digit-only strings, so the parse
349
+ # is total over the carrier domain and the result
350
+ # is always `>= 0`. `non-negative-int` is the
351
+ # tightest carrier that captures both the lower
352
+ # bound and the integer-ness without inventing a
353
+ # narrower carrier.
354
+ %i[decimal_int to_i] => :non_negative_int,
355
+ %i[decimal_int to_int] => :non_negative_int,
356
+ %i[numeric to_i] => :non_negative_int,
357
+ %i[numeric to_int] => :non_negative_int
293
358
  }.freeze
294
359
  private_constant :REFINED_STRING_PROJECTIONS
295
360
 
@@ -313,6 +378,7 @@ module Rigor
313
378
  when :refined_self then refined
314
379
  when :uppercase_string then Type::Combinator.uppercase_string
315
380
  when :lowercase_string then Type::Combinator.lowercase_string
381
+ when :non_negative_int then Type::Combinator.non_negative_int
316
382
  end
317
383
  end
318
384
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  require_relative "../reflection"
4
4
  require_relative "../type"
5
+ require_relative "../flow_contribution"
6
+ require_relative "../flow_contribution/merger"
5
7
  require_relative "method_dispatcher/constant_folding"
6
8
  require_relative "method_dispatcher/literal_string_folding"
7
9
  require_relative "method_dispatcher/shape_dispatch"
@@ -59,12 +61,25 @@ module Rigor
59
61
  # @param environment [Rigor::Environment, nil] required for
60
62
  # RBS-backed dispatch; when nil only constant folding can fire.
61
63
  # @return [Rigor::Type, nil] inferred result type, or `nil` for "no rule".
62
- def dispatch(receiver_type:, method_name:, arg_types:, block_type: nil, environment: nil)
64
+ def dispatch(receiver_type:, method_name:, arg_types:, # rubocop:disable Metrics/ParameterLists
65
+ block_type: nil, environment: nil,
66
+ call_node: nil, scope: nil)
63
67
  return nil if receiver_type.nil?
64
68
 
65
69
  precise = dispatch_precise_tiers(receiver_type, method_name, arg_types, block_type)
66
70
  return precise if precise
67
71
 
72
+ # v0.1.1 Track 2 slice 7 — plugin return-type contribution
73
+ # tier. Sits ahead of `RbsDispatch` so a plugin that
74
+ # understands a domain-specific dispatch (e.g. an
75
+ # `ActiveRecord::Base.find` returning `Nominal[<resolved
76
+ # model>]`) wins over the RBS-projected envelope. Only
77
+ # consults the registry when both `call_node` and `scope`
78
+ # are supplied — the dispatcher's own internal callers
79
+ # (per-element block fold, etc.) skip this tier.
80
+ plugin_result = try_plugin_contribution(call_node, scope)
81
+ return plugin_result if plugin_result
82
+
68
83
  rbs_result = RbsDispatch.try_dispatch(
69
84
  receiver: receiver_type, method_name: method_name, args: arg_types,
70
85
  environment: environment, block_type: block_type
@@ -85,6 +100,40 @@ module Rigor
85
100
  try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type)
86
101
  end
87
102
 
103
+ # ADR-2 § "Flow Contribution Bundle" / v0.1.1 Track 2
104
+ # slice 7. Walks every loaded plugin's
105
+ # `#flow_contribution_for(call_node:, scope:)` hook,
106
+ # collects the non-nil `FlowContribution` bundles, merges
107
+ # them through `FlowContribution::Merger`, and returns
108
+ # the merged `return_type` slot (or nil when no plugin
109
+ # contributed a return type).
110
+ #
111
+ # Plugins whose hook raises have their contribution
112
+ # silently dropped for this call so the dispatch chain
113
+ # keeps moving — the run-level diagnostic envelope (per
114
+ # ADR-2 § "Plugin Trust and I/O Policy") is owned by
115
+ # `Analysis::Runner#plugin_emitted_diagnostics`.
116
+ def try_plugin_contribution(call_node, scope)
117
+ return nil if call_node.nil? || scope.nil?
118
+
119
+ registry = scope.environment&.plugin_registry
120
+ return nil if registry.nil? || registry.empty?
121
+
122
+ contributions = collect_plugin_contributions(registry, call_node, scope)
123
+ return nil if contributions.empty?
124
+
125
+ FlowContribution::Merger.merge(contributions).return_type
126
+ end
127
+
128
+ def collect_plugin_contributions(registry, call_node, scope)
129
+ registry.plugins.filter_map do |plugin|
130
+ contribution = plugin.flow_contribution_for(call_node: call_node, scope: scope)
131
+ contribution.is_a?(FlowContribution) ? contribution : nil
132
+ rescue StandardError
133
+ nil
134
+ end
135
+ end
136
+
88
137
  # Runs the precision tiers (constant fold, shape dispatch,
89
138
  # file-path fold, block fold) in order and returns the first
90
139
  # non-nil answer. Each tier owns its own receiver/argument
@@ -131,6 +131,8 @@ module Rigor
131
131
  end
132
132
 
133
133
  def bind_rest_target(splat_node, type, bindings)
134
+ return unless splat_node.is_a?(Prism::SplatNode)
135
+
134
136
  expression = splat_node.expression
135
137
  case expression
136
138
  when Prism::LocalVariableTargetNode, Prism::RequiredParameterNode