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
@@ -85,6 +85,7 @@ module Rigor
85
85
  Prism::EnsureNode => :eval_ensure,
86
86
  Prism::WhileNode => :eval_loop,
87
87
  Prism::UntilNode => :eval_loop,
88
+ Prism::ForNode => :eval_for,
88
89
  Prism::AndNode => :eval_and_or,
89
90
  Prism::OrNode => :eval_and_or,
90
91
  Prism::ParenthesesNode => :eval_parentheses,
@@ -93,7 +94,8 @@ module Rigor
93
94
  Prism::ModuleNode => :eval_class_or_module,
94
95
  Prism::SingletonClassNode => :eval_singleton_class,
95
96
  Prism::CallNode => :eval_call,
96
- Prism::BlockNode => :eval_block
97
+ Prism::BlockNode => :eval_block,
98
+ Prism::MatchWriteNode => :eval_match_write
97
99
  }.freeze
98
100
  private_constant :HANDLERS
99
101
 
@@ -452,13 +454,26 @@ module Rigor
452
454
  results = []
453
455
  falsey_scope = entry_scope
454
456
  conditions.each do |branch|
455
- when_conditions = branch.respond_to?(:conditions) ? branch.conditions : []
456
- body_scope, falsey_scope = Narrowing.case_when_scopes(subject, when_conditions, falsey_scope)
457
+ body_scope, falsey_scope = branch_body_and_falsey_scopes(subject, branch, falsey_scope)
457
458
  results << sub_eval(branch, body_scope)
458
459
  end
459
460
  [results, falsey_scope]
460
461
  end
461
462
 
463
+ # Returns `[body_scope, updated_falsey_scope]` for a single branch.
464
+ # `InNode` branches apply pattern bindings; `WhenNode` branches
465
+ # narrow through `Narrowing.case_when_scopes`. The falsey scope is
466
+ # unchanged for `in` branches (conservative: no exhaustiveness
467
+ # tracking yet).
468
+ def branch_body_and_falsey_scopes(subject, branch, falsey_scope)
469
+ if branch.is_a?(Prism::InNode)
470
+ [apply_in_pattern_bindings(subject, branch.pattern, falsey_scope), falsey_scope]
471
+ else
472
+ when_conditions = branch.respond_to?(:conditions) ? branch.conditions : []
473
+ Narrowing.case_when_scopes(subject, when_conditions, falsey_scope)
474
+ end
475
+ end
476
+
462
477
  def eval_case_else(else_clause, falsey_scope)
463
478
  return sub_eval(else_clause, falsey_scope) if else_clause
464
479
 
@@ -524,7 +539,8 @@ module Rigor
524
539
  results = []
525
540
  current = rescue_node
526
541
  while current
527
- results << eval_branch_or_nil(current.statements, entry_scope)
542
+ rescue_scope = bind_rescue_reference(current, entry_scope)
543
+ results << eval_branch_or_nil(current.statements, rescue_scope)
528
544
  current = current.subsequent
529
545
  end
530
546
  results
@@ -554,6 +570,122 @@ module Rigor
554
570
  ]
555
571
  end
556
572
 
573
+ # `for index in collection; body; end`. Unlike `each {}` blocks,
574
+ # `for` does NOT create a new variable scope: the index variable
575
+ # AND every local written in the body leak to the surrounding
576
+ # scope. The collection is evaluated once; the body runs zero or
577
+ # more times, so the post-loop scope is the join of the
578
+ # no-iteration scope (just `post_collection`) and the body scope,
579
+ # with half-bound names degraded to `T | nil` via nil-injection.
580
+ # The loop expression itself types as `Constant[nil]` (the common
581
+ # case where no `break VALUE` is observed), matching the policy
582
+ # `eval_loop` uses for `while` / `until`.
583
+ def eval_for(node)
584
+ coll_type, post_coll = sub_eval(node.collection, scope)
585
+ element_type = for_iteration_element_type(coll_type)
586
+ body_entry = bind_for_index(node.index, element_type, post_coll)
587
+
588
+ body_scope = node.statements ? sub_eval(node.statements, body_entry).last : body_entry
589
+ [
590
+ Type::Combinator.constant_of(nil),
591
+ join_with_nil_injection(post_coll, body_scope)
592
+ ]
593
+ end
594
+
595
+ # `for x in coll` is semantically `coll.each { |x| ... }`. We
596
+ # ask the method dispatcher for `coll.each`'s expected block
597
+ # parameter types — that path consults RBS and the iterator
598
+ # dispatch table, which is more precise than the structural
599
+ # `collection_element_type` fallback (it knows, e.g., that
600
+ # `Hash[K, V]#each` yields `[K, V]` even when the receiver is
601
+ # not a literal Hash carrier in our local lattice). When the
602
+ # dispatcher returns nothing (no signature, unknown receiver)
603
+ # we fall back to the structural extractor.
604
+ def for_iteration_element_type(coll_type)
605
+ structural = collection_element_type(coll_type)
606
+ return structural unless structural.equal?(Type::Combinator.untyped)
607
+
608
+ block_params = MethodDispatcher.expected_block_param_types(
609
+ receiver_type: coll_type,
610
+ method_name: :each,
611
+ arg_types: [],
612
+ environment: scope.environment
613
+ )
614
+ return structural if block_params.nil? || block_params.empty?
615
+
616
+ block_params.size == 1 ? block_params.first : Type::Combinator.tuple_of(*block_params)
617
+ rescue StandardError
618
+ Type::Combinator.untyped
619
+ end
620
+
621
+ # Binds the `for` index variable(s) into `scope`. A single
622
+ # `LocalVariableTargetNode` is bound to `element_type` (the
623
+ # per-iteration value the collection yields). A `MultiTargetNode`
624
+ # (`for a, b in pairs`) delegates to {MultiTargetBinder}, which
625
+ # decomposes a tuple-shaped element into the inner slots.
626
+ def bind_for_index(index_node, element_type, scope)
627
+ case index_node
628
+ when Prism::LocalVariableTargetNode
629
+ scope.with_local(index_node.name, element_type)
630
+ when Prism::MultiTargetNode
631
+ MultiTargetBinder.bind(index_node, element_type)
632
+ .reduce(scope) { |s, (name, type)| s.with_local(name, type) }
633
+ else
634
+ scope
635
+ end
636
+ end
637
+
638
+ # Extracts the per-iteration element type from a collection
639
+ # carrier. `Tuple[T1..Tn]` yields the union of its elements;
640
+ # `Nominal[Array, [T]]` and `Nominal[Range, [T]]` yield `T`;
641
+ # `Nominal[Hash, [K, V]]` yields `Tuple[K, V]` (Hash#each yields
642
+ # `[key, value]` pairs); `IntegerRange` yields `Integer`;
643
+ # `Constant<Range>` reads the literal range's element class.
644
+ # Anything else falls back to `untyped`.
645
+ def collection_element_type(type)
646
+ case type
647
+ when Type::Tuple
648
+ type.elements.empty? ? Type::Combinator.untyped : Type::Combinator.union(*type.elements)
649
+ when Type::Nominal
650
+ nominal_element_type(type)
651
+ when Type::IntegerRange
652
+ Type::Combinator.nominal_of("Integer")
653
+ when Type::Constant
654
+ constant_element_type(type)
655
+ else
656
+ Type::Combinator.untyped
657
+ end
658
+ end
659
+
660
+ def constant_element_type(constant)
661
+ value = constant.value
662
+ case value
663
+ when Range
664
+ first = value.first
665
+ first.nil? ? Type::Combinator.untyped : Type::Combinator.nominal_of(first.class.name)
666
+ when Array
667
+ return Type::Combinator.untyped if value.empty?
668
+
669
+ Type::Combinator.union(*value.map { |v| Type::Combinator.constant_of(v) })
670
+ else
671
+ Type::Combinator.untyped
672
+ end
673
+ rescue StandardError
674
+ Type::Combinator.untyped
675
+ end
676
+
677
+ def nominal_element_type(nominal)
678
+ args = nominal.type_args
679
+ case nominal.class_name
680
+ when "Array", "Range", "Set", "Enumerator" then args[0] || Type::Combinator.untyped
681
+ when "Hash"
682
+ k = args[0] || Type::Combinator.untyped
683
+ v = args[1] || Type::Combinator.untyped
684
+ Type::Combinator.tuple_of(k, v)
685
+ else Type::Combinator.untyped
686
+ end
687
+ end
688
+
557
689
  # `a && b` / `a || b`. The LHS always runs, the RHS only
558
690
  # sometimes runs. Slice 6 phase 1 narrows the RHS evaluation:
559
691
  # `a && b` evaluates `b` under the truthy edge of `a`, and
@@ -819,23 +951,30 @@ module Rigor
819
951
  parts.join("::")
820
952
  end
821
953
 
822
- # v0.0.2 applies `RBS::Extended` `assert <target> is T`
823
- # directives to the post-call scope. The conditional
824
- # variants (`assert-if-true` / `assert-if-false`) are
825
- # NOT applied here — they refine the scope only when the
826
- # call is observed as a truthy / falsey predicate, which
827
- # `Narrowing.predicate_scopes` handles separately.
954
+ # Slice 4b-2 (ADR-7 § "Slice 4-A/4-B") applies the
955
+ # post-return facts the merger produces for an
956
+ # `RBS::Extended`-annotated call. Reads through
957
+ # `RbsExtended.read_flow_contribution` so the bundle
958
+ # carries the canonical `Rigor::FlowContribution::Fact`
959
+ # rows for `:always` assert directives (the slice-4a
960
+ # routing places conditional asserts on `truthy_facts` /
961
+ # `falsey_facts`, which `Narrowing.predicate_scopes`
962
+ # consumes). Future plugin contributions that add
963
+ # `:always` assertions at the same call site flow through
964
+ # the same merger and land here.
828
965
  def apply_rbs_extended_assertions(call_node, current_scope)
829
966
  method_def = resolve_call_method(call_node, current_scope)
830
967
  return current_scope if method_def.nil?
831
968
 
832
- effects = RbsExtended.read_assert_effects(method_def)
833
- return current_scope if effects.empty?
969
+ contribution = RbsExtended.read_flow_contribution(method_def)
970
+ return current_scope if contribution.nil?
834
971
 
835
- effects.reduce(current_scope) do |scope_acc, effect|
836
- next scope_acc unless effect.always?
972
+ result = Rigor::FlowContribution::Merger.merge([contribution])
973
+ post_return = result.post_return_facts
974
+ return current_scope if post_return.empty?
837
975
 
838
- apply_assert_effect(effect, call_node, scope_acc, method_def)
976
+ post_return.reduce(current_scope) do |scope_acc, fact|
977
+ apply_post_return_fact(fact, call_node, scope_acc, method_def)
839
978
  end
840
979
  end
841
980
 
@@ -868,44 +1007,64 @@ module Rigor
868
1007
  end
869
1008
  end
870
1009
 
871
- def apply_assert_effect(effect, call_node, current_scope, method_def)
872
- target_node = assert_effect_target_node(effect, call_node, method_def)
1010
+ # Slice 4b-2 applies a single post-return Fact to the
1011
+ # scope. Mirrors `Narrowing#apply_fact_to_scope` (Fact
1012
+ # variant of the v0.0.2 `apply_assert_effect`); shares the
1013
+ # narrowing logic via `Narrowing.narrow_for_fact` so the
1014
+ # predicate / assert / plugin paths all converge on the
1015
+ # same hierarchy-aware narrowing rules.
1016
+ def apply_post_return_fact(fact, call_node, current_scope, method_def)
1017
+ target_node = fact_target_node(fact, call_node, method_def)
1018
+ return apply_self_post_return_fact(fact, target_node, current_scope) if fact.target_kind == :self
873
1019
  return current_scope unless target_node.is_a?(Prism::LocalVariableReadNode)
874
1020
 
875
1021
  local_name = target_node.name
876
1022
  current_type = current_scope.local(local_name)
877
1023
  return current_scope if current_type.nil?
878
1024
 
879
- narrowed = narrow_for_assert_effect(current_type, effect, current_scope.environment)
1025
+ narrowed = Narrowing.narrow_for_fact(current_type, fact, current_scope.environment)
880
1026
  current_scope.with_local(local_name, narrowed)
881
1027
  end
882
1028
 
883
- # v0.0.2 #3 — same `target: self` accommodation as
884
- # `Narrowing.effect_target_node`: the call's receiver
885
- # serves as the target for self-targeted directives.
886
- def assert_effect_target_node(effect, call_node, method_def)
887
- if effect.target_kind == :self
888
- call_node.receiver
1029
+ # v0.1.1 Track 1 slice 3 — `assert self is T` post-return
1030
+ # narrowing for the four supported receiver shapes (mirrors
1031
+ # `Narrowing#apply_self_fact`).
1032
+ def apply_self_post_return_fact(fact, receiver_node, current_scope)
1033
+ case receiver_node
1034
+ when nil, Prism::SelfNode
1035
+ current = current_scope.self_type
1036
+ return current_scope if current.nil?
1037
+
1038
+ narrowed = Narrowing.narrow_for_fact(current, fact, current_scope.environment)
1039
+ current_scope.with_self_type(narrowed)
1040
+ when Prism::LocalVariableReadNode
1041
+ current = current_scope.local(receiver_node.name)
1042
+ return current_scope if current.nil?
1043
+
1044
+ narrowed = Narrowing.narrow_for_fact(current, fact, current_scope.environment)
1045
+ current_scope.with_local(receiver_node.name, narrowed)
1046
+ when Prism::InstanceVariableReadNode
1047
+ current = current_scope.ivar(receiver_node.name)
1048
+ return current_scope if current.nil?
1049
+
1050
+ narrowed = Narrowing.narrow_for_fact(current, fact, current_scope.environment)
1051
+ current_scope.with_ivar(receiver_node.name, narrowed)
889
1052
  else
890
- lookup_assert_arg(call_node, method_def, effect.target_name)
1053
+ current_scope
891
1054
  end
892
1055
  end
893
1056
 
894
- def narrow_for_assert_effect(current_type, effect, environment)
895
- if effect.refinement?
896
- return Narrowing.narrow_not_refinement(current_type, effect.refinement_type) if effect.negative?
897
-
898
- return effect.refinement_type
899
- end
900
-
901
- if effect.negative?
902
- Narrowing.narrow_not_class(current_type, effect.class_name, exact: false, environment: environment)
1057
+ # `:self` routes to the call receiver; otherwise we look
1058
+ # up the matching positional argument by parameter name.
1059
+ def fact_target_node(fact, call_node, method_def)
1060
+ if fact.target_kind == :self
1061
+ call_node.receiver
903
1062
  else
904
- Narrowing.narrow_class(current_type, effect.class_name, exact: false, environment: environment)
1063
+ lookup_post_return_arg(call_node, method_def, fact.target_name)
905
1064
  end
906
1065
  end
907
1066
 
908
- def lookup_assert_arg(call_node, method_def, target_name)
1067
+ def lookup_post_return_arg(call_node, method_def, target_name)
909
1068
  arguments = call_node.arguments&.arguments || []
910
1069
  method_def.method_types.each do |mt|
911
1070
  params = mt.type.required_positionals + mt.type.optional_positionals
@@ -1031,10 +1190,29 @@ module Rigor
1031
1190
  # bindings for every named block parameter on top. Parameter
1032
1191
  # types come from the receiving method's RBS signature when
1033
1192
  # one is available; the rest default to `Dynamic[Top]`.
1193
+ #
1194
+ # `;`-prefixed block-locals (`do |i; x|`) are bound to
1195
+ # `Constant[nil]` so the inner read shadows any outer
1196
+ # `x` per Ruby's semantics — at runtime the block-local is
1197
+ # a fresh nil-valued variable on every block invocation.
1198
+ # Without this shadow, an inner `x.even?` before the first
1199
+ # write would type-check against the OUTER `x` (e.g.
1200
+ # `Integer`) when the runtime would actually `NoMethodError`
1201
+ # on `nil`.
1034
1202
  def build_block_entry_scope(call_node, block_node)
1035
1203
  expected = expected_block_param_types_for(call_node)
1036
1204
  bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_node)
1037
- bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
1205
+ scope_with_params = bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
1206
+ block_local_names(block_node).reduce(scope_with_params) do |acc, name|
1207
+ acc.with_local(name, Type::Combinator.constant_of(nil))
1208
+ end
1209
+ end
1210
+
1211
+ def block_local_names(block_node)
1212
+ params_root = block_node.parameters
1213
+ return [] unless params_root.is_a?(Prism::BlockParametersNode)
1214
+
1215
+ params_root.locals.map(&:name)
1038
1216
  end
1039
1217
 
1040
1218
  def expected_block_param_types_for(call_node)
@@ -1318,6 +1496,197 @@ module Rigor
1318
1496
  def reduce_scopes_with_nil_injection(scopes)
1319
1497
  scopes.reduce { |a, b| join_with_nil_injection(a, b) }
1320
1498
  end
1499
+
1500
+ # ---------------------------------------------------------------
1501
+ # rescue variable binding helpers
1502
+ # ---------------------------------------------------------------
1503
+
1504
+ # Returns `scope` extended with the rescue reference variable bound
1505
+ # to the exception instance type. Leaves scope unchanged when the
1506
+ # node carries no reference (bare `rescue` without `=> var`).
1507
+ def bind_rescue_reference(rescue_node, scope)
1508
+ ref = rescue_node.reference
1509
+ return scope unless ref.is_a?(Prism::LocalVariableTargetNode)
1510
+
1511
+ scope.with_local(ref.name, rescue_exception_type(rescue_node, scope))
1512
+ end
1513
+
1514
+ # Derives the exception instance type for a `RescueNode`. When the
1515
+ # exceptions list is empty (bare `rescue`) the type is
1516
+ # `StandardError`. When one or more exception classes are named the
1517
+ # types are unioned. Falls back to `StandardError` for any class
1518
+ # that cannot be resolved to a `Singleton` type.
1519
+ def rescue_exception_type(rescue_node, scope)
1520
+ exceptions = rescue_node.exceptions
1521
+ if exceptions.empty?
1522
+ Type::Combinator.nominal_of("StandardError")
1523
+ else
1524
+ types = exceptions.map do |exc_node|
1525
+ singleton_type, = sub_eval(exc_node, scope)
1526
+ singleton_to_nominal(singleton_type)
1527
+ end
1528
+ Type::Combinator.union(*types)
1529
+ end
1530
+ end
1531
+
1532
+ # ---------------------------------------------------------------
1533
+ # `case/in` pattern variable binding helpers
1534
+ # ---------------------------------------------------------------
1535
+
1536
+ # Builds the entry scope for an `in` branch by injecting every
1537
+ # variable captured by the pattern as a local binding.
1538
+ def apply_in_pattern_bindings(subject, pattern, scope)
1539
+ bindings = collect_in_pattern_bindings(subject, pattern, scope)
1540
+ bindings.reduce(scope) { |s, (name, type)| s.with_local(name, type) }
1541
+ end
1542
+
1543
+ # Returns an array of `[Symbol, Rigor::Type]` pairs for every
1544
+ # variable captured by `pattern`. Unrecognised pattern nodes
1545
+ # contribute no bindings (fail-soft).
1546
+ # rubocop:disable Metrics/CyclomaticComplexity
1547
+ def collect_in_pattern_bindings(subject, pattern, scope)
1548
+ case pattern
1549
+ when Prism::CapturePatternNode
1550
+ [[pattern.target.name, pattern_capture_type(pattern.value, scope)]]
1551
+ when Prism::LocalVariableTargetNode
1552
+ subject_type = subject.is_a?(Prism::LocalVariableReadNode) ? scope.local(subject.name) : nil
1553
+ [[pattern.name, subject_type || Type::Combinator.untyped]]
1554
+ when Prism::ImplicitNode
1555
+ collect_in_pattern_bindings(subject, pattern.value, scope)
1556
+ when Prism::ArrayPatternNode
1557
+ collect_array_pattern_bindings(pattern, scope)
1558
+ when Prism::FindPatternNode
1559
+ collect_find_pattern_bindings(pattern, scope)
1560
+ when Prism::HashPatternNode
1561
+ collect_hash_pattern_bindings(pattern, scope)
1562
+ when Prism::AlternationPatternNode
1563
+ collect_alternation_pattern_bindings(subject, pattern, scope)
1564
+ else
1565
+ []
1566
+ end
1567
+ end
1568
+ # rubocop:enable Metrics/CyclomaticComplexity
1569
+
1570
+ def collect_array_pattern_bindings(pattern, scope)
1571
+ bindings = [*pattern.requireds, *pattern.posts].flat_map do |elem|
1572
+ collect_in_pattern_bindings(nil, elem, scope)
1573
+ end
1574
+ append_array_splat_binding(bindings, pattern.rest)
1575
+ bindings
1576
+ end
1577
+
1578
+ def collect_hash_pattern_bindings(pattern, scope)
1579
+ bindings = pattern.elements.flat_map do |assoc|
1580
+ next [] unless assoc.is_a?(Prism::AssocNode) && assoc.value
1581
+
1582
+ collect_in_pattern_bindings(nil, assoc.value, scope)
1583
+ end
1584
+ rest = pattern.rest
1585
+ if rest.is_a?(Prism::AssocSplatNode)
1586
+ val = rest.value
1587
+ bindings << [val.name, hash_pattern_rest_type] if val.is_a?(Prism::LocalVariableTargetNode)
1588
+ end
1589
+ bindings
1590
+ end
1591
+
1592
+ def collect_find_pattern_bindings(pattern, scope)
1593
+ bindings = pattern.requireds.flat_map do |elem|
1594
+ collect_in_pattern_bindings(nil, elem, scope)
1595
+ end
1596
+ [pattern.left, pattern.right].each { |splat| append_array_splat_binding(bindings, splat) }
1597
+ bindings
1598
+ end
1599
+
1600
+ # `[..., *rest, ...]` / `[*pre, x, *post]` capture an Array of
1601
+ # the unmatched elements; bind `rest` to `Array[untyped]` rather
1602
+ # than the previous bare `untyped`. Per-element typing waits on
1603
+ # subject-aware element-type extraction (the binder doesn't see
1604
+ # the case subject).
1605
+ def append_array_splat_binding(bindings, splat)
1606
+ return unless splat.is_a?(Prism::SplatNode)
1607
+
1608
+ target = splat.expression
1609
+ return unless target.is_a?(Prism::LocalVariableTargetNode)
1610
+
1611
+ bindings << [target.name, Type::Combinator.nominal_of("Array", type_args: [Type::Combinator.untyped])]
1612
+ end
1613
+
1614
+ # `{ key:, **rest }` binds `rest` to a Hash whose keys are
1615
+ # Symbols (the only legal key shape for a hash pattern) and
1616
+ # whose values are untyped (the binder can't see the subject's
1617
+ # value type).
1618
+ def hash_pattern_rest_type
1619
+ Type::Combinator.nominal_of(
1620
+ "Hash",
1621
+ type_args: [Type::Combinator.nominal_of("Symbol"), Type::Combinator.untyped]
1622
+ )
1623
+ end
1624
+
1625
+ # ---------------------------------------------------------------
1626
+ # named-capture regex binding (`MatchWriteNode`)
1627
+ # ---------------------------------------------------------------
1628
+
1629
+ # `/(?<year>\d+)/ =~ str` — Prism emits a `MatchWriteNode` that
1630
+ # wraps the `=~` call and lists the named-capture targets. Each
1631
+ # target is bound to `String | nil` (the capture is absent as nil
1632
+ # when the pattern doesn't match or the group didn't participate).
1633
+ def eval_match_write(node)
1634
+ match_type, post_scope = sub_eval(node.call, scope)
1635
+ string_or_nil = Type::Combinator.union(
1636
+ Type::Combinator.nominal_of("String"),
1637
+ Type::Combinator.constant_of(nil)
1638
+ )
1639
+ bound_scope = node.targets.reduce(post_scope) do |s, target|
1640
+ next s unless target.is_a?(Prism::LocalVariableTargetNode)
1641
+
1642
+ s.with_local(target.name, string_or_nil)
1643
+ end
1644
+ [match_type, bound_scope]
1645
+ end
1646
+
1647
+ # ---------------------------------------------------------------
1648
+ # shared type conversion helper
1649
+ # ---------------------------------------------------------------
1650
+
1651
+ # Converts a `Singleton[ClassName]` (the class object) to the
1652
+ # corresponding `Nominal[ClassName]` (an instance). Falls back to
1653
+ # `untyped` for carriers that are not Singleton (e.g. Dynamic[Top]
1654
+ # when the class could not be resolved).
1655
+ def singleton_to_nominal(type)
1656
+ type.is_a?(Type::Singleton) ? Type::Combinator.nominal_of(type.class_name) : Type::Combinator.untyped
1657
+ end
1658
+
1659
+ # Returns the type to bind for a `CapturePatternNode`'s target.
1660
+ # Plain class references collapse to the matching `Nominal[T]`;
1661
+ # `AlternationPatternNode` (`Integer | String => x`) unions every
1662
+ # alternate's resolved type. Anything else falls back to
1663
+ # `untyped` (the conservative legacy behaviour).
1664
+ def pattern_capture_type(value_node, scope)
1665
+ if value_node.is_a?(Prism::AlternationPatternNode)
1666
+ left = pattern_capture_type(value_node.left, scope)
1667
+ right = pattern_capture_type(value_node.right, scope)
1668
+ Type::Combinator.union(left, right)
1669
+ else
1670
+ singleton_to_nominal(sub_eval(value_node, scope).first)
1671
+ end
1672
+ end
1673
+
1674
+ # `in PatternA | PatternB` — Ruby requires both alternates to
1675
+ # bind the same names, but the binder runs against the AST and
1676
+ # cannot enforce that. We collect bindings from each side and
1677
+ # merge by name, unioning types when both alternates contribute.
1678
+ # Names that only one alternate contributes still surface (the
1679
+ # parser would have rejected the case at compile time, so by the
1680
+ # time we see it the user's intent is the merged set).
1681
+ def collect_alternation_pattern_bindings(subject, pattern, scope)
1682
+ left = collect_in_pattern_bindings(subject, pattern.left, scope)
1683
+ right = collect_in_pattern_bindings(subject, pattern.right, scope)
1684
+ merged = {}
1685
+ (left + right).each do |name, type|
1686
+ merged[name] = merged.key?(name) ? Type::Combinator.union(merged[name], type) : type
1687
+ end
1688
+ merged.to_a
1689
+ end
1321
1690
  end
1322
1691
  # rubocop:enable Metrics/ClassLength
1323
1692
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Plugin
5
+ # Raised when a plugin's I/O attempt fails the active
6
+ # {TrustPolicy}. Surfaced through {IoBoundary} so the analyzer
7
+ # can convert the exception into a `:plugin_loader` diagnostic
8
+ # without crashing `rigor check`.
9
+ #
10
+ # The policy is documented in [ADR-2 § "Plugin Trust and I/O
11
+ # Policy"](../../../docs/adr/2-extension-api.md). Slice 2's
12
+ # surface lists `:read_outside_scope` and `:network_disabled`
13
+ # as the two reason codes; future slices may extend the set.
14
+ class AccessDeniedError < StandardError
15
+ attr_reader :reason, :resource
16
+
17
+ def initialize(message, reason:, resource: nil)
18
+ super(message)
19
+ @reason = reason
20
+ @resource = resource
21
+ end
22
+ end
23
+ end
24
+ end