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.
- checksums.yaml +4 -4
- data/README.md +45 -2
- data/data/builtins/ruby_core/array.yml +6 -6
- data/data/builtins/ruby_core/hash.yml +1 -1
- data/data/builtins/ruby_core/io.yml +3 -3
- data/data/builtins/ruby_core/numeric.yml +1 -1
- data/data/builtins/ruby_core/pathname.yml +100 -100
- data/data/builtins/ruby_core/proc.yml +1 -1
- data/data/builtins/ruby_core/time.yml +3 -3
- data/lib/rigor/analysis/check_rules.rb +228 -40
- data/lib/rigor/analysis/diagnostic.rb +15 -1
- data/lib/rigor/analysis/runner.rb +269 -7
- data/lib/rigor/builtins/regex_refinement.rb +104 -0
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
- data/lib/rigor/cache/rbs_class_type_param_names.rb +1 -1
- data/lib/rigor/cache/rbs_constant_table.rb +2 -2
- data/lib/rigor/cache/rbs_descriptor.rb +2 -0
- data/lib/rigor/cache/rbs_instance_definitions.rb +79 -0
- data/lib/rigor/cache/store.rb +2 -0
- data/lib/rigor/cli/type_of_command.rb +3 -3
- data/lib/rigor/cli/type_scan_command.rb +4 -4
- data/lib/rigor/cli.rb +20 -7
- data/lib/rigor/configuration/severity_profile.rb +109 -0
- data/lib/rigor/configuration.rb +286 -15
- data/lib/rigor/environment/rbs_loader.rb +89 -13
- data/lib/rigor/environment.rb +12 -4
- data/lib/rigor/flow_contribution/conflict.rb +81 -0
- data/lib/rigor/flow_contribution/element.rb +53 -0
- data/lib/rigor/flow_contribution/fact.rb +88 -0
- data/lib/rigor/flow_contribution/merge_result.rb +67 -0
- data/lib/rigor/flow_contribution/merger.rb +275 -0
- data/lib/rigor/flow_contribution.rb +51 -0
- data/lib/rigor/inference/block_parameter_binder.rb +15 -0
- data/lib/rigor/inference/expression_typer.rb +87 -6
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +31 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +136 -9
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +21 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +68 -2
- data/lib/rigor/inference/method_dispatcher.rb +50 -1
- data/lib/rigor/inference/multi_target_binder.rb +2 -0
- data/lib/rigor/inference/narrowing.rb +246 -127
- data/lib/rigor/inference/scope_indexer.rb +124 -16
- data/lib/rigor/inference/statement_evaluator.rb +406 -37
- data/lib/rigor/plugin/access_denied_error.rb +24 -0
- data/lib/rigor/plugin/base.rb +284 -0
- data/lib/rigor/plugin/fact_store.rb +92 -0
- data/lib/rigor/plugin/io_boundary.rb +102 -0
- data/lib/rigor/plugin/load_error.rb +35 -0
- data/lib/rigor/plugin/loader.rb +307 -0
- data/lib/rigor/plugin/manifest.rb +203 -0
- data/lib/rigor/plugin/registry.rb +50 -0
- data/lib/rigor/plugin/services.rb +77 -0
- data/lib/rigor/plugin/trust_policy.rb +99 -0
- data/lib/rigor/plugin.rb +62 -0
- data/lib/rigor/rbs_extended.rb +57 -9
- data/lib/rigor/reflection.rb +2 -2
- data/lib/rigor/trinary.rb +1 -1
- data/lib/rigor/type/integer_range.rb +6 -2
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +7 -0
- data/sig/rigor/environment.rbs +10 -3
- data/sig/rigor/inference.rbs +1 -0
- data/sig/rigor/rbs_extended.rbs +2 -0
- data/sig/rigor/scope.rbs +1 -0
- data/sig/rigor/type.rbs +7 -0
- data/sig/rigor.rbs +8 -2
- 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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
823
|
-
#
|
|
824
|
-
#
|
|
825
|
-
#
|
|
826
|
-
#
|
|
827
|
-
# `
|
|
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
|
-
|
|
833
|
-
return current_scope if
|
|
969
|
+
contribution = RbsExtended.read_flow_contribution(method_def)
|
|
970
|
+
return current_scope if contribution.nil?
|
|
834
971
|
|
|
835
|
-
|
|
836
|
-
|
|
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
|
-
|
|
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
|
-
|
|
872
|
-
|
|
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 =
|
|
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.
|
|
884
|
-
#
|
|
885
|
-
#
|
|
886
|
-
def
|
|
887
|
-
|
|
888
|
-
|
|
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
|
-
|
|
1053
|
+
current_scope
|
|
891
1054
|
end
|
|
892
1055
|
end
|
|
893
1056
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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
|
-
|
|
1063
|
+
lookup_post_return_arg(call_node, method_def, fact.target_name)
|
|
905
1064
|
end
|
|
906
1065
|
end
|
|
907
1066
|
|
|
908
|
-
def
|
|
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
|