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
@@ -7,6 +7,7 @@ require_relative "../type"
7
7
  require_relative "../environment"
8
8
  require_relative "../rbs_extended"
9
9
  require_relative "../analysis/fact_store"
10
+ require_relative "../builtins/regex_refinement"
10
11
 
11
12
  module Rigor
12
13
  module Inference
@@ -293,6 +294,29 @@ module Rigor
293
294
  end
294
295
  end
295
296
 
297
+ # ADR-7 § "Slice 4-A" — public Fact-shaped narrowing
298
+ # entry. Distinguishes a `Nominal[<class>]`-typed Fact
299
+ # (uses `narrow_class` / `narrow_not_class` for
300
+ # hierarchy-aware narrowing) from a refinement-shaped
301
+ # Fact (refined types, IntegerRange, Difference, …).
302
+ # The implementation lives next to its sibling helpers
303
+ # `narrow_class` and `narrow_not_refinement`; consumers
304
+ # outside `Narrowing` (today: `StatementEvaluator`'s
305
+ # post-return assertion path) reach for it via
306
+ # `Rigor::Inference::Narrowing.narrow_for_fact`.
307
+ def narrow_for_fact(current, fact, environment)
308
+ if fact.type.is_a?(Type::Nominal) && fact.type.type_args.empty?
309
+ class_name = fact.type.class_name
310
+ return narrow_not_class(current, class_name, exact: false, environment: environment) if fact.negative?
311
+
312
+ return narrow_class(current, class_name, exact: false, environment: environment)
313
+ end
314
+
315
+ return narrow_not_refinement(current, fact.type) if fact.negative?
316
+
317
+ fact.type
318
+ end
319
+
296
320
  # Public predicate analyser. Returns `[truthy_scope, falsey_scope]`,
297
321
  # always; when no narrowing rule matches the predicate node both
298
322
  # entries are the receiver scope unchanged.
@@ -352,7 +376,7 @@ module Rigor
352
376
  # the predicate shape is recognised, or `nil` to signal "no
353
377
  # narrowing" so the public surface can fall back to the entry
354
378
  # scope.
355
- def analyse(node, scope)
379
+ def analyse(node, scope) # rubocop:disable Metrics/CyclomaticComplexity
356
380
  case node
357
381
  when Prism::ParenthesesNode
358
382
  analyse_parentheses(node, scope)
@@ -366,6 +390,8 @@ module Rigor
366
390
  analyse_and(node, scope)
367
391
  when Prism::OrNode
368
392
  analyse_or(node, scope)
393
+ when Prism::MatchWriteNode
394
+ analyse_match_write(node, scope)
369
395
  end
370
396
  end
371
397
 
@@ -712,6 +738,60 @@ module Rigor
712
738
  ]
713
739
  end
714
740
 
741
+ # `if /(?<x>...)/ =~ str` — Prism wraps the `=~` call in a
742
+ # `MatchWriteNode` listing the named-capture targets. The
743
+ # parent `eval_match_write` has already bound each target
744
+ # to `String | nil`; in the truthy branch (the regex
745
+ # matched) every named capture is guaranteed `String`,
746
+ # and in the falsey branch (no match) every capture is
747
+ # `nil`. Subtract the dead half on each edge so callers
748
+ # like `year.upcase` inside the truthy branch no longer
749
+ # fire `possible-nil-receiver`.
750
+ #
751
+ # v0.1.1 Track 1 slice 1 — when the regex source is a
752
+ # statically known `RegularExpressionNode` and a named
753
+ # capture's body matches one of the curated shapes in
754
+ # {Rigor::Builtins::RegexRefinement::RULES}, the truthy
755
+ # branch narrows further than `String` to the matching
756
+ # imported refinement (e.g. `decimal-int-string` for
757
+ # `\d+`). Bodies outside the table fall back to the
758
+ # v0.1.0 baseline (plain `String`).
759
+ def analyse_match_write(node, scope)
760
+ string_t = Type::Combinator.nominal_of("String")
761
+ nil_t = Type::Combinator.constant_of(nil)
762
+ refinements = match_write_capture_refinements(node)
763
+ truthy = scope
764
+ falsey = scope
765
+ node.targets.each do |target|
766
+ next unless target.is_a?(Prism::LocalVariableTargetNode)
767
+
768
+ truthy = truthy.with_local(target.name, refinements[target.name] || string_t)
769
+ falsey = falsey.with_local(target.name, nil_t)
770
+ end
771
+ [truthy, falsey]
772
+ end
773
+
774
+ # Extracts `{ capture_name => Refinement }` for every named
775
+ # capture group in the `MatchWriteNode`'s wrapped `=~` call
776
+ # whose body the recogniser table accepts. Bodies that
777
+ # contain nested groups, anchors, alternation, or anything
778
+ # else outside the curated forms drop out and the caller
779
+ # falls back to plain `String`.
780
+ NAMED_CAPTURE_BODY_RE = /\(\?<([A-Za-z_][A-Za-z0-9_]*)>([^()|]*)\)/
781
+ private_constant :NAMED_CAPTURE_BODY_RE
782
+
783
+ def match_write_capture_refinements(node)
784
+ regex = node.call.is_a?(Prism::CallNode) ? node.call.receiver : nil
785
+ return {} unless regex.is_a?(Prism::RegularExpressionNode)
786
+
787
+ refinements = {}
788
+ regex.unescaped.scan(NAMED_CAPTURE_BODY_RE) do |name, body|
789
+ type = ::Rigor::Builtins::RegexRefinement.for_capture_body(body)
790
+ refinements[name.to_sym] = type if type
791
+ end
792
+ refinements
793
+ end
794
+
715
795
  # Recognised CallNode predicates:
716
796
  # - `recv.nil?` (Slice 6 phase 1, no args, no block)
717
797
  # - unary `!recv` (`name == :!`, no args, no block)
@@ -724,42 +804,80 @@ module Rigor
724
804
  # through to the no-narrowing fallback.
725
805
  def analyse_call(node, scope)
726
806
  return nil if node.block
727
- return nil if node.receiver.nil?
728
807
 
729
- shape_result = dispatch_call(node, scope, node.name)
730
- return shape_result if shape_result
808
+ unless node.receiver.nil?
809
+ shape_result = dispatch_call(node, scope, node.name)
810
+ return shape_result if shape_result
811
+
812
+ # v0.1.1 Track 1 slice 4 — String predicate flow facts.
813
+ string_predicate_result = analyse_string_predicate(node, scope)
814
+ return string_predicate_result if string_predicate_result
815
+ end
731
816
 
732
817
  # Slice 7 phase 15 — RBS::Extended predicate
733
818
  # effects. When the method's RBS signature carries
734
819
  # `rigor:v1:predicate-if-true` / `predicate-if-false`
735
820
  # annotations, apply them to narrow the corresponding
736
- # local-variable arguments on each edge.
737
- predicate_result = analyse_rbs_extended_predicate(node, scope)
738
- assert_result = analyse_rbs_extended_assert_if(node, scope)
739
- merge_extended_results(predicate_result, assert_result, scope)
740
- end
741
-
742
- # Combines two `[truthy_scope, falsey_scope]` pair
743
- # results from sibling RBS::Extended analysers
744
- # (`predicate-if-*` and `assert-if-*`). When only one
745
- # side fires, return it directly; when both fire the
746
- # right side's per-local deltas are applied on top of
747
- # the left side's edges so the rules compose.
748
- def merge_extended_results(left, right, base_scope)
749
- return left if right.nil?
750
- return right if left.nil?
821
+ # local-variable arguments on each edge. v0.1.1 Track
822
+ # 1 slice 3 — implicit-self calls (`recv == nil`,
823
+ # e.g. `admin?` inside an instance method body) flow
824
+ # through here too so `self`-targeted facts can edit
825
+ # `scope.self_type`.
826
+ analyse_rbs_extended_contribution(node, scope)
827
+ end
828
+
829
+ # v0.1.1 Track 1 slice 4 — `String#start_with?` /
830
+ # `#end_with?` / `#include?` against a `Constant<String>`
831
+ # needle attaches a relational `FactStore::Fact` to the
832
+ # local on each edge. The receiver's type does NOT
833
+ # change Rigor has no "starts-with-X" carrier today —
834
+ # so the fact carries the predicate semantics for any
835
+ # downstream consumer that wants to read it (e.g. a
836
+ # plugin's `prepare(services)` hook in v0.1.x).
837
+ # Truthy edge: positive polarity. Falsey edge: negative
838
+ # polarity. Mirrors the equality-predicate fact pattern
839
+ # already used by `analyse_equality_predicate`.
840
+ STRING_PREDICATE_NAMES = %i[start_with? end_with? include?].freeze
841
+ private_constant :STRING_PREDICATE_NAMES
842
+
843
+ def analyse_string_predicate(node, scope)
844
+ return nil unless string_predicate_call?(node)
845
+
846
+ needle = string_predicate_needle(node, scope)
847
+ return nil if needle.nil?
848
+
849
+ local_name = node.receiver.name
850
+ return nil if scope.local(local_name).nil?
751
851
 
752
852
  [
753
- merge_scope_pair(left[0], right[0], base_scope),
754
- merge_scope_pair(left[1], right[1], base_scope)
853
+ scope.with_fact(string_predicate_fact(local_name, node.name, needle, polarity: :positive)),
854
+ scope.with_fact(string_predicate_fact(local_name, node.name, needle, polarity: :negative))
755
855
  ]
756
856
  end
757
857
 
758
- def merge_scope_pair(left_scope, right_scope, base_scope)
759
- right_scope.locals.reduce(left_scope) do |acc, (name, type)|
760
- base_type = base_scope.local(name)
761
- type.equal?(base_type) ? acc : acc.with_local(name, type)
762
- end
858
+ def string_predicate_call?(node)
859
+ STRING_PREDICATE_NAMES.include?(node.name) &&
860
+ node.receiver.is_a?(Prism::LocalVariableReadNode) &&
861
+ !node.arguments.nil? &&
862
+ node.arguments.arguments.size == 1
863
+ end
864
+
865
+ def string_predicate_needle(node, scope)
866
+ needle_type = static_literal_type(node.arguments.arguments.first, scope)
867
+ return nil unless needle_type.is_a?(Type::Constant) && needle_type.value.is_a?(String)
868
+
869
+ needle_type.value
870
+ end
871
+
872
+ def string_predicate_fact(name, predicate, needle, polarity:)
873
+ Analysis::FactStore::Fact.new(
874
+ bucket: :relational,
875
+ target: Analysis::FactStore::Target.local(name),
876
+ predicate: predicate,
877
+ payload: needle,
878
+ polarity: polarity,
879
+ stability: :local_binding
880
+ )
763
881
  end
764
882
 
765
883
  ZERO_CLASS_PREDICATES = %i[positive? negative? zero? nonzero?].freeze
@@ -1187,118 +1305,130 @@ module Rigor
1187
1305
  node
1188
1306
  end
1189
1307
 
1190
- # Slice 7 phase 15RBS::Extended predicate-effect
1191
- # analyser. Resolves the called method through the
1192
- # RBS environment, reads any `rigor:v1:predicate-if-*`
1193
- # annotations, and applies them to the call's
1194
- # local-variable arguments.
1308
+ # Slice 4b-1 (ADR-7 § "Slice 4-A/4-B") single-point
1309
+ # RBS::Extended contribution analyser. Replaces the
1310
+ # earlier sibling pair (`analyse_rbs_extended_predicate`
1311
+ # + `analyse_rbs_extended_assert_if`) with one path that
1312
+ # routes through `RbsExtended.read_flow_contribution` and
1313
+ # `Rigor::FlowContribution::Merger.merge`. The bundle's
1314
+ # `truthy_facts` slot already includes both the
1315
+ # `predicate-if-true` and `assert-if-true` Facts (slice
1316
+ # 4a routing closed the v0.0.9 imperfection); the
1317
+ # `falsey_facts` slot mirrors that. The merger composes
1318
+ # any future plugin contribution at the same site
1319
+ # alongside the RBS::Extended bundle without changing
1320
+ # this analyser.
1195
1321
  #
1196
- # Conservative envelope:
1322
+ # Conservative envelope (carried over from the previous
1323
+ # implementation):
1197
1324
  # - Receiver type must be `Type::Nominal`,
1198
1325
  # `Type::Singleton`, or `Type::Constant`.
1199
1326
  # - The method must be present in the loader.
1200
- # - For each predicate effect, the corresponding
1201
- # positional argument (matched by parameter name in
1202
- # the selected overload) MUST be a
1203
- # `Prism::LocalVariableReadNode` for narrowing to
1204
- # apply.
1205
- # - When the target is `self`, narrowing applies to
1206
- # the receiver but the engine does not yet narrow
1207
- # `self` itself (Slice A-engine self-typing is
1208
- # read-only), so `self`-targeted effects are
1209
- # accepted by the parser but currently produce no
1327
+ # - For each fact, the corresponding positional
1328
+ # argument (matched by parameter name in the selected
1329
+ # overload) MUST be a `Prism::LocalVariableReadNode`
1330
+ # for narrowing to apply.
1331
+ # - When the target is `self`, narrowing applies to the
1332
+ # receiver but the engine does not yet narrow
1333
+ # `self` itself, so `self`-targeted facts are
1334
+ # accepted by the merger but currently produce no
1210
1335
  # scope edits.
1211
- def analyse_rbs_extended_predicate(node, scope)
1336
+ def analyse_rbs_extended_contribution(node, scope)
1212
1337
  method_def = resolve_rbs_extended_method(node, scope)
1213
1338
  return nil if method_def.nil?
1214
1339
 
1215
- effects = RbsExtended.read_predicate_effects(method_def)
1216
- return nil if effects.empty?
1340
+ contribution = RbsExtended.read_flow_contribution(method_def)
1341
+ return nil if contribution.nil?
1217
1342
 
1218
- truthy_scope = scope
1219
- falsey_scope = scope
1220
- effects.each do |effect|
1221
- truthy_scope, falsey_scope =
1222
- apply_predicate_effect(effect, node, scope, truthy_scope, falsey_scope, method_def)
1223
- end
1224
- [truthy_scope, falsey_scope]
1225
- end
1226
-
1227
- # v0.0.2 — `assert-if-true` / `assert-if-false`. Reads
1228
- # the conditional assertion effects off the called
1229
- # method and narrows the matching argument on the
1230
- # corresponding edge. The unconditional `assert`
1231
- # variant is NOT applied here; `StatementEvaluator`
1232
- # applies it directly to the post-call scope.
1233
- def analyse_rbs_extended_assert_if(node, scope)
1234
- method_def = resolve_rbs_extended_method(node, scope)
1235
- return nil if method_def.nil?
1343
+ result = Rigor::FlowContribution::Merger.merge([contribution])
1344
+ truthy_facts = result.truthy_facts
1345
+ falsey_facts = result.falsey_facts
1346
+ return nil if truthy_facts.empty? && falsey_facts.empty?
1236
1347
 
1237
- effects = RbsExtended.read_assert_effects(method_def).reject(&:always?)
1238
- return nil if effects.empty?
1348
+ apply_facts_to_scope_pair(node, scope, truthy_facts, falsey_facts, method_def)
1349
+ end
1239
1350
 
1240
- truthy_scope = scope
1241
- falsey_scope = scope
1242
- effects.each do |effect|
1243
- truthy_scope, falsey_scope =
1244
- apply_assert_if_effect(effect, node, scope, truthy_scope, falsey_scope, method_def)
1351
+ def apply_facts_to_scope_pair(call_node, entry_scope, truthy_facts, falsey_facts, method_def)
1352
+ truthy_scope = entry_scope
1353
+ falsey_scope = entry_scope
1354
+ truthy_facts.each do |fact|
1355
+ truthy_scope = apply_fact_to_scope(fact, call_node, entry_scope, truthy_scope, method_def)
1356
+ end
1357
+ falsey_facts.each do |fact|
1358
+ falsey_scope = apply_fact_to_scope(fact, call_node, entry_scope, falsey_scope, method_def)
1245
1359
  end
1246
1360
  [truthy_scope, falsey_scope]
1247
1361
  end
1248
1362
 
1249
- # rubocop:disable Metrics/ParameterLists
1250
- def apply_assert_if_effect(effect, call_node, entry_scope, truthy_scope, falsey_scope, method_def)
1251
- target_node = effect_target_node(effect, call_node, method_def)
1252
- return [truthy_scope, falsey_scope] unless target_node.is_a?(Prism::LocalVariableReadNode)
1363
+ def apply_fact_to_scope(fact, call_node, entry_scope, target_scope, method_def)
1364
+ target_node = fact_target_node(fact, call_node, method_def)
1365
+ return apply_self_fact(fact, target_node, entry_scope, target_scope) if fact.target_kind == :self
1366
+ return target_scope unless target_node.is_a?(Prism::LocalVariableReadNode)
1253
1367
 
1254
1368
  local_name = target_node.name
1255
1369
  current = entry_scope.local(local_name)
1256
- return [truthy_scope, falsey_scope] if current.nil?
1370
+ return target_scope if current.nil?
1257
1371
 
1258
- narrowed = narrow_for_effect(current, effect, entry_scope.environment)
1259
- if effect.if_truthy_return?
1260
- [truthy_scope.with_local(local_name, narrowed), falsey_scope]
1261
- else
1262
- [truthy_scope, falsey_scope.with_local(local_name, narrowed)]
1263
- end
1372
+ narrowed = narrow_for_fact(current, fact, entry_scope.environment)
1373
+ target_scope.with_local(local_name, narrowed)
1264
1374
  end
1265
- # rubocop:enable Metrics/ParameterLists
1266
-
1267
- # v0.0.2 #3 resolves an effect's target node. For
1268
- # `target: <param>` we look up the matching positional
1269
- # argument; for `target: self` we use the call's
1270
- # receiver. In both cases the caller still requires a
1271
- # `Prism::LocalVariableReadNode` for narrowing to
1272
- # actually fire (the engine's narrowing surface only
1273
- # rebinds locals).
1274
- def effect_target_node(effect, call_node, method_def)
1275
- if effect.target_kind == :self
1276
- call_node.receiver
1375
+
1376
+ # v0.1.1 Track 1 slice 3 — `target_kind == :self` facts
1377
+ # (`predicate-if-true self is T`, `predicate-if-false
1378
+ # self is T`) narrow whichever scope binding is bound to
1379
+ # the call's receiver. Four receiver shapes participate:
1380
+ #
1381
+ # `recv.method?` where recv is...
1382
+ # - `LocalVariableReadNode` -> narrow that local
1383
+ # - `InstanceVariableReadNode` -> narrow that ivar
1384
+ # - `Prism::SelfNode` (explicit self) -> narrow `self_type`
1385
+ # - nil (implicit self call inside a method body)
1386
+ # -> narrow `self_type`
1387
+ #
1388
+ # Other receiver shapes (method chains, expressions) have
1389
+ # no scope binding to narrow against and fall through.
1390
+ def apply_self_fact(fact, receiver_node, entry_scope, target_scope)
1391
+ case receiver_node
1392
+ when nil, Prism::SelfNode
1393
+ current = entry_scope.self_type
1394
+ return target_scope if current.nil?
1395
+
1396
+ narrowed = narrow_for_fact(current, fact, entry_scope.environment)
1397
+ target_scope.with_self_type(narrowed)
1398
+ when Prism::LocalVariableReadNode
1399
+ current = entry_scope.local(receiver_node.name)
1400
+ return target_scope if current.nil?
1401
+
1402
+ narrowed = narrow_for_fact(current, fact, entry_scope.environment)
1403
+ target_scope.with_local(receiver_node.name, narrowed)
1404
+ when Prism::InstanceVariableReadNode
1405
+ current = entry_scope.ivar(receiver_node.name)
1406
+ return target_scope if current.nil?
1407
+
1408
+ narrowed = narrow_for_fact(current, fact, entry_scope.environment)
1409
+ target_scope.with_ivar(receiver_node.name, narrowed)
1277
1410
  else
1278
- lookup_positional_arg(call_node, method_def, effect.target_name)
1411
+ target_scope
1279
1412
  end
1280
1413
  end
1281
1414
 
1282
- # v0.0.2 selects `narrow_class` (positive) or
1283
- # `narrow_not_class` (negative `~T` form) based on
1284
- # the effect's `negative?` flag. Shared between
1285
- # predicate-if-* and assert-if-* application paths.
1286
- def narrow_for_effect(current, effect, environment)
1287
- if effect.respond_to?(:refinement?) && effect.refinement?
1288
- return narrow_not_refinement(current, effect.refinement_type) if effect.negative?
1289
-
1290
- return effect.refinement_type
1291
- end
1292
-
1293
- if effect.negative?
1294
- narrow_not_class(current, effect.class_name, exact: false, environment: environment)
1415
+ # Resolves a Fact's target node. Mirrors the earlier
1416
+ # `effect_target_node` helper but reads from the
1417
+ # canonical Fact carrier; `:self` routes to the call
1418
+ # receiver, otherwise we look up the matching
1419
+ # positional argument by parameter name.
1420
+ def fact_target_node(fact, call_node, method_def)
1421
+ if fact.target_kind == :self
1422
+ call_node.receiver
1295
1423
  else
1296
- narrow_class(current, effect.class_name, exact: false, environment: environment)
1424
+ lookup_positional_arg(call_node, method_def, fact.target_name)
1297
1425
  end
1298
1426
  end
1299
1427
 
1300
1428
  def resolve_rbs_extended_method(node, scope)
1301
- receiver_type = scope.type_of(node.receiver)
1429
+ receiver_type = receiver_type_for_resolve(node, scope)
1430
+ return nil if receiver_type.nil?
1431
+
1302
1432
  class_name = rbs_extended_class_name(receiver_type)
1303
1433
  return nil if class_name.nil?
1304
1434
  return nil unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
@@ -1312,6 +1442,13 @@ module Rigor
1312
1442
  nil
1313
1443
  end
1314
1444
 
1445
+ # Implicit self call (`admin?` with no receiver inside an
1446
+ # instance method body) — read the method definition from
1447
+ # `scope.self_type` per v0.1.1 Track 1 slice 3.
1448
+ def receiver_type_for_resolve(node, scope)
1449
+ node.receiver.nil? ? scope.self_type : scope.type_of(node.receiver)
1450
+ end
1451
+
1315
1452
  def rbs_extended_class_name(receiver_type)
1316
1453
  case receiver_type
1317
1454
  when Type::Nominal, Type::Singleton then receiver_type.class_name
@@ -1332,24 +1469,6 @@ module Rigor
1332
1469
  nil
1333
1470
  end
1334
1471
 
1335
- # rubocop:disable Metrics/ParameterLists
1336
- def apply_predicate_effect(effect, call_node, entry_scope, truthy_scope, falsey_scope, method_def)
1337
- target_node = effect_target_node(effect, call_node, method_def)
1338
- return [truthy_scope, falsey_scope] unless target_node.is_a?(Prism::LocalVariableReadNode)
1339
-
1340
- local_name = target_node.name
1341
- current = entry_scope.local(local_name)
1342
- return [truthy_scope, falsey_scope] if current.nil?
1343
-
1344
- narrowed = narrow_for_effect(current, effect, entry_scope.environment)
1345
- if effect.truthy_only?
1346
- [truthy_scope.with_local(local_name, narrowed), falsey_scope]
1347
- else
1348
- [truthy_scope, falsey_scope.with_local(local_name, narrowed)]
1349
- end
1350
- end
1351
- # rubocop:enable Metrics/ParameterLists
1352
-
1353
1472
  # Maps the effect's target parameter name to the call
1354
1473
  # site argument by inspecting the selected overload's
1355
1474
  # required-positional parameter list. Returns the Prism