rigortype 0.0.9 → 0.1.0
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 +40 -2
- data/lib/rigor/analysis/check_rules.rb +228 -40
- data/lib/rigor/analysis/diagnostic.rb +15 -1
- data/lib/rigor/analysis/runner.rb +183 -4
- 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.rb +9 -3
- data/lib/rigor/configuration/severity_profile.rb +109 -0
- data/lib/rigor/configuration.rb +110 -6
- data/lib/rigor/environment/rbs_loader.rb +89 -13
- 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 +84 -5
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +95 -9
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +21 -1
- data/lib/rigor/inference/multi_target_binder.rb +2 -0
- data/lib/rigor/inference/narrowing.rb +105 -130
- data/lib/rigor/inference/scope_indexer.rb +75 -1
- data/lib/rigor/inference/statement_evaluator.rb +380 -40
- data/lib/rigor/plugin/access_denied_error.rb +24 -0
- data/lib/rigor/plugin/base.rb +241 -0
- data/lib/rigor/plugin/io_boundary.rb +102 -0
- data/lib/rigor/plugin/load_error.rb +23 -0
- data/lib/rigor/plugin/loader.rb +191 -0
- data/lib/rigor/plugin/manifest.rb +134 -0
- data/lib/rigor/plugin/registry.rb +50 -0
- data/lib/rigor/plugin/services.rb +65 -0
- data/lib/rigor/plugin/trust_policy.rb +99 -0
- data/lib/rigor/plugin.rb +61 -0
- data/lib/rigor/rbs_extended.rb +57 -9
- data/lib/rigor/reflection.rb +2 -2
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +7 -0
- data/sig/rigor/environment.rbs +7 -1
- 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
- metadata +18 -1
|
@@ -340,7 +340,7 @@ module Rigor
|
|
|
340
340
|
accumulator.transform_values(&:freeze).freeze
|
|
341
341
|
end
|
|
342
342
|
|
|
343
|
-
def walk_methods(node, qualified_prefix, in_singleton_class, accumulator) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
343
|
+
def walk_methods(node, qualified_prefix, in_singleton_class, accumulator) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
344
344
|
return unless node.is_a?(Prism::Node)
|
|
345
345
|
|
|
346
346
|
case node
|
|
@@ -359,6 +359,9 @@ module Rigor
|
|
|
359
359
|
when Prism::DefNode
|
|
360
360
|
record_def_method(node, qualified_prefix, in_singleton_class, accumulator)
|
|
361
361
|
return
|
|
362
|
+
when Prism::AliasMethodNode
|
|
363
|
+
record_alias_method(node, qualified_prefix, in_singleton_class, accumulator)
|
|
364
|
+
return
|
|
362
365
|
when Prism::CallNode
|
|
363
366
|
record_define_method(node, qualified_prefix, in_singleton_class, accumulator) if node.name == :define_method
|
|
364
367
|
end
|
|
@@ -390,6 +393,7 @@ module Rigor
|
|
|
390
393
|
def build_discovered_def_nodes(root)
|
|
391
394
|
accumulator = {}
|
|
392
395
|
walk_def_nodes(root, [], false, accumulator)
|
|
396
|
+
apply_alias_def_nodes(root, accumulator)
|
|
393
397
|
accumulator.transform_values(&:freeze).freeze
|
|
394
398
|
end
|
|
395
399
|
|
|
@@ -436,6 +440,76 @@ module Rigor
|
|
|
436
440
|
accumulator[class_name][def_node.name] = def_node
|
|
437
441
|
end
|
|
438
442
|
|
|
443
|
+
# Registers the alias name in the `discovered_methods` table so
|
|
444
|
+
# `undefined-method` diagnostics are not emitted for calls to the
|
|
445
|
+
# aliased name. The kind mirrors the surrounding class context
|
|
446
|
+
# (instance inside a regular class body, singleton inside
|
|
447
|
+
# `class << self`).
|
|
448
|
+
def record_alias_method(alias_node, qualified_prefix, in_singleton_class, accumulator)
|
|
449
|
+
return if qualified_prefix.empty?
|
|
450
|
+
return unless alias_node.new_name.is_a?(Prism::SymbolNode)
|
|
451
|
+
|
|
452
|
+
class_name = qualified_prefix.join("::")
|
|
453
|
+
new_name = alias_node.new_name.unescaped.to_sym
|
|
454
|
+
kind = in_singleton_class ? :singleton : :instance
|
|
455
|
+
(accumulator[class_name] ||= {})[new_name] = kind
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Post-pass over the `def_nodes` accumulator: for every `alias`
|
|
459
|
+
# declaration inside a class body, if the original method name
|
|
460
|
+
# maps to a `Prism::DefNode`, register the new name pointing to
|
|
461
|
+
# the same node so inter-procedural return-type inference works
|
|
462
|
+
# for the aliased name.
|
|
463
|
+
def apply_alias_def_nodes(root, accumulator)
|
|
464
|
+
alias_map = collect_class_alias_map(root, [], {})
|
|
465
|
+
alias_map.each do |class_name, aliases|
|
|
466
|
+
class_defs = accumulator[class_name]
|
|
467
|
+
next unless class_defs
|
|
468
|
+
|
|
469
|
+
aliases.each do |new_name, old_name|
|
|
470
|
+
def_node = class_defs[old_name]
|
|
471
|
+
next unless def_node.is_a?(Prism::DefNode)
|
|
472
|
+
|
|
473
|
+
(accumulator[class_name] ||= {})[new_name] = def_node
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Builds a map `{class_name => {new_name_sym => old_name_sym}}` by
|
|
479
|
+
# walking the tree for `AliasMethodNode` nodes inside class bodies.
|
|
480
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
481
|
+
def collect_class_alias_map(node, qualified_prefix, accumulator)
|
|
482
|
+
return accumulator unless node.is_a?(Prism::Node)
|
|
483
|
+
|
|
484
|
+
case node
|
|
485
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
486
|
+
name = qualified_name_for(node.constant_path)
|
|
487
|
+
if name
|
|
488
|
+
collect_class_alias_map(node.body, qualified_prefix + [name], accumulator) if node.body
|
|
489
|
+
return accumulator
|
|
490
|
+
end
|
|
491
|
+
when Prism::SingletonClassNode
|
|
492
|
+
return accumulator
|
|
493
|
+
when Prism::AliasMethodNode
|
|
494
|
+
record_alias_map_entry(node, qualified_prefix, accumulator)
|
|
495
|
+
return accumulator
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
node.compact_child_nodes.each { |child| collect_class_alias_map(child, qualified_prefix, accumulator) }
|
|
499
|
+
accumulator
|
|
500
|
+
end
|
|
501
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
502
|
+
|
|
503
|
+
def record_alias_map_entry(alias_node, qualified_prefix, accumulator)
|
|
504
|
+
return if qualified_prefix.empty?
|
|
505
|
+
return unless alias_node.new_name.is_a?(Prism::SymbolNode) && alias_node.old_name.is_a?(Prism::SymbolNode)
|
|
506
|
+
|
|
507
|
+
class_name = qualified_prefix.join("::")
|
|
508
|
+
new_name = alias_node.new_name.unescaped.to_sym
|
|
509
|
+
old_name = alias_node.old_name.unescaped.to_sym
|
|
510
|
+
(accumulator[class_name] ||= {})[new_name] = old_name
|
|
511
|
+
end
|
|
512
|
+
|
|
439
513
|
def record_define_method(call_node, qualified_prefix, in_singleton_class, accumulator)
|
|
440
514
|
return if qualified_prefix.empty?
|
|
441
515
|
return if call_node.arguments.nil? || call_node.arguments.arguments.empty?
|
|
@@ -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,35 @@ 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)
|
|
873
1018
|
return current_scope unless target_node.is_a?(Prism::LocalVariableReadNode)
|
|
874
1019
|
|
|
875
1020
|
local_name = target_node.name
|
|
876
1021
|
current_type = current_scope.local(local_name)
|
|
877
1022
|
return current_scope if current_type.nil?
|
|
878
1023
|
|
|
879
|
-
narrowed =
|
|
1024
|
+
narrowed = Narrowing.narrow_for_fact(current_type, fact, current_scope.environment)
|
|
880
1025
|
current_scope.with_local(local_name, narrowed)
|
|
881
1026
|
end
|
|
882
1027
|
|
|
883
|
-
#
|
|
884
|
-
#
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
if effect.target_kind == :self
|
|
1028
|
+
# `:self` routes to the call receiver; otherwise we look
|
|
1029
|
+
# up the matching positional argument by parameter name.
|
|
1030
|
+
def fact_target_node(fact, call_node, method_def)
|
|
1031
|
+
if fact.target_kind == :self
|
|
888
1032
|
call_node.receiver
|
|
889
1033
|
else
|
|
890
|
-
|
|
1034
|
+
lookup_post_return_arg(call_node, method_def, fact.target_name)
|
|
891
1035
|
end
|
|
892
1036
|
end
|
|
893
1037
|
|
|
894
|
-
def
|
|
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)
|
|
903
|
-
else
|
|
904
|
-
Narrowing.narrow_class(current_type, effect.class_name, exact: false, environment: environment)
|
|
905
|
-
end
|
|
906
|
-
end
|
|
907
|
-
|
|
908
|
-
def lookup_assert_arg(call_node, method_def, target_name)
|
|
1038
|
+
def lookup_post_return_arg(call_node, method_def, target_name)
|
|
909
1039
|
arguments = call_node.arguments&.arguments || []
|
|
910
1040
|
method_def.method_types.each do |mt|
|
|
911
1041
|
params = mt.type.required_positionals + mt.type.optional_positionals
|
|
@@ -1031,10 +1161,29 @@ module Rigor
|
|
|
1031
1161
|
# bindings for every named block parameter on top. Parameter
|
|
1032
1162
|
# types come from the receiving method's RBS signature when
|
|
1033
1163
|
# one is available; the rest default to `Dynamic[Top]`.
|
|
1164
|
+
#
|
|
1165
|
+
# `;`-prefixed block-locals (`do |i; x|`) are bound to
|
|
1166
|
+
# `Constant[nil]` so the inner read shadows any outer
|
|
1167
|
+
# `x` per Ruby's semantics — at runtime the block-local is
|
|
1168
|
+
# a fresh nil-valued variable on every block invocation.
|
|
1169
|
+
# Without this shadow, an inner `x.even?` before the first
|
|
1170
|
+
# write would type-check against the OUTER `x` (e.g.
|
|
1171
|
+
# `Integer`) when the runtime would actually `NoMethodError`
|
|
1172
|
+
# on `nil`.
|
|
1034
1173
|
def build_block_entry_scope(call_node, block_node)
|
|
1035
1174
|
expected = expected_block_param_types_for(call_node)
|
|
1036
1175
|
bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_node)
|
|
1037
|
-
bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
|
|
1176
|
+
scope_with_params = bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
|
|
1177
|
+
block_local_names(block_node).reduce(scope_with_params) do |acc, name|
|
|
1178
|
+
acc.with_local(name, Type::Combinator.constant_of(nil))
|
|
1179
|
+
end
|
|
1180
|
+
end
|
|
1181
|
+
|
|
1182
|
+
def block_local_names(block_node)
|
|
1183
|
+
params_root = block_node.parameters
|
|
1184
|
+
return [] unless params_root.is_a?(Prism::BlockParametersNode)
|
|
1185
|
+
|
|
1186
|
+
params_root.locals.map(&:name)
|
|
1038
1187
|
end
|
|
1039
1188
|
|
|
1040
1189
|
def expected_block_param_types_for(call_node)
|
|
@@ -1318,6 +1467,197 @@ module Rigor
|
|
|
1318
1467
|
def reduce_scopes_with_nil_injection(scopes)
|
|
1319
1468
|
scopes.reduce { |a, b| join_with_nil_injection(a, b) }
|
|
1320
1469
|
end
|
|
1470
|
+
|
|
1471
|
+
# ---------------------------------------------------------------
|
|
1472
|
+
# rescue variable binding helpers
|
|
1473
|
+
# ---------------------------------------------------------------
|
|
1474
|
+
|
|
1475
|
+
# Returns `scope` extended with the rescue reference variable bound
|
|
1476
|
+
# to the exception instance type. Leaves scope unchanged when the
|
|
1477
|
+
# node carries no reference (bare `rescue` without `=> var`).
|
|
1478
|
+
def bind_rescue_reference(rescue_node, scope)
|
|
1479
|
+
ref = rescue_node.reference
|
|
1480
|
+
return scope unless ref.is_a?(Prism::LocalVariableTargetNode)
|
|
1481
|
+
|
|
1482
|
+
scope.with_local(ref.name, rescue_exception_type(rescue_node, scope))
|
|
1483
|
+
end
|
|
1484
|
+
|
|
1485
|
+
# Derives the exception instance type for a `RescueNode`. When the
|
|
1486
|
+
# exceptions list is empty (bare `rescue`) the type is
|
|
1487
|
+
# `StandardError`. When one or more exception classes are named the
|
|
1488
|
+
# types are unioned. Falls back to `StandardError` for any class
|
|
1489
|
+
# that cannot be resolved to a `Singleton` type.
|
|
1490
|
+
def rescue_exception_type(rescue_node, scope)
|
|
1491
|
+
exceptions = rescue_node.exceptions
|
|
1492
|
+
if exceptions.empty?
|
|
1493
|
+
Type::Combinator.nominal_of("StandardError")
|
|
1494
|
+
else
|
|
1495
|
+
types = exceptions.map do |exc_node|
|
|
1496
|
+
singleton_type, = sub_eval(exc_node, scope)
|
|
1497
|
+
singleton_to_nominal(singleton_type)
|
|
1498
|
+
end
|
|
1499
|
+
Type::Combinator.union(*types)
|
|
1500
|
+
end
|
|
1501
|
+
end
|
|
1502
|
+
|
|
1503
|
+
# ---------------------------------------------------------------
|
|
1504
|
+
# `case/in` pattern variable binding helpers
|
|
1505
|
+
# ---------------------------------------------------------------
|
|
1506
|
+
|
|
1507
|
+
# Builds the entry scope for an `in` branch by injecting every
|
|
1508
|
+
# variable captured by the pattern as a local binding.
|
|
1509
|
+
def apply_in_pattern_bindings(subject, pattern, scope)
|
|
1510
|
+
bindings = collect_in_pattern_bindings(subject, pattern, scope)
|
|
1511
|
+
bindings.reduce(scope) { |s, (name, type)| s.with_local(name, type) }
|
|
1512
|
+
end
|
|
1513
|
+
|
|
1514
|
+
# Returns an array of `[Symbol, Rigor::Type]` pairs for every
|
|
1515
|
+
# variable captured by `pattern`. Unrecognised pattern nodes
|
|
1516
|
+
# contribute no bindings (fail-soft).
|
|
1517
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
1518
|
+
def collect_in_pattern_bindings(subject, pattern, scope)
|
|
1519
|
+
case pattern
|
|
1520
|
+
when Prism::CapturePatternNode
|
|
1521
|
+
[[pattern.target.name, pattern_capture_type(pattern.value, scope)]]
|
|
1522
|
+
when Prism::LocalVariableTargetNode
|
|
1523
|
+
subject_type = subject.is_a?(Prism::LocalVariableReadNode) ? scope.local(subject.name) : nil
|
|
1524
|
+
[[pattern.name, subject_type || Type::Combinator.untyped]]
|
|
1525
|
+
when Prism::ImplicitNode
|
|
1526
|
+
collect_in_pattern_bindings(subject, pattern.value, scope)
|
|
1527
|
+
when Prism::ArrayPatternNode
|
|
1528
|
+
collect_array_pattern_bindings(pattern, scope)
|
|
1529
|
+
when Prism::FindPatternNode
|
|
1530
|
+
collect_find_pattern_bindings(pattern, scope)
|
|
1531
|
+
when Prism::HashPatternNode
|
|
1532
|
+
collect_hash_pattern_bindings(pattern, scope)
|
|
1533
|
+
when Prism::AlternationPatternNode
|
|
1534
|
+
collect_alternation_pattern_bindings(subject, pattern, scope)
|
|
1535
|
+
else
|
|
1536
|
+
[]
|
|
1537
|
+
end
|
|
1538
|
+
end
|
|
1539
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
1540
|
+
|
|
1541
|
+
def collect_array_pattern_bindings(pattern, scope)
|
|
1542
|
+
bindings = [*pattern.requireds, *pattern.posts].flat_map do |elem|
|
|
1543
|
+
collect_in_pattern_bindings(nil, elem, scope)
|
|
1544
|
+
end
|
|
1545
|
+
append_array_splat_binding(bindings, pattern.rest)
|
|
1546
|
+
bindings
|
|
1547
|
+
end
|
|
1548
|
+
|
|
1549
|
+
def collect_hash_pattern_bindings(pattern, scope)
|
|
1550
|
+
bindings = pattern.elements.flat_map do |assoc|
|
|
1551
|
+
next [] unless assoc.is_a?(Prism::AssocNode) && assoc.value
|
|
1552
|
+
|
|
1553
|
+
collect_in_pattern_bindings(nil, assoc.value, scope)
|
|
1554
|
+
end
|
|
1555
|
+
rest = pattern.rest
|
|
1556
|
+
if rest.is_a?(Prism::AssocSplatNode)
|
|
1557
|
+
val = rest.value
|
|
1558
|
+
bindings << [val.name, hash_pattern_rest_type] if val.is_a?(Prism::LocalVariableTargetNode)
|
|
1559
|
+
end
|
|
1560
|
+
bindings
|
|
1561
|
+
end
|
|
1562
|
+
|
|
1563
|
+
def collect_find_pattern_bindings(pattern, scope)
|
|
1564
|
+
bindings = pattern.requireds.flat_map do |elem|
|
|
1565
|
+
collect_in_pattern_bindings(nil, elem, scope)
|
|
1566
|
+
end
|
|
1567
|
+
[pattern.left, pattern.right].each { |splat| append_array_splat_binding(bindings, splat) }
|
|
1568
|
+
bindings
|
|
1569
|
+
end
|
|
1570
|
+
|
|
1571
|
+
# `[..., *rest, ...]` / `[*pre, x, *post]` capture an Array of
|
|
1572
|
+
# the unmatched elements; bind `rest` to `Array[untyped]` rather
|
|
1573
|
+
# than the previous bare `untyped`. Per-element typing waits on
|
|
1574
|
+
# subject-aware element-type extraction (the binder doesn't see
|
|
1575
|
+
# the case subject).
|
|
1576
|
+
def append_array_splat_binding(bindings, splat)
|
|
1577
|
+
return unless splat.is_a?(Prism::SplatNode)
|
|
1578
|
+
|
|
1579
|
+
target = splat.expression
|
|
1580
|
+
return unless target.is_a?(Prism::LocalVariableTargetNode)
|
|
1581
|
+
|
|
1582
|
+
bindings << [target.name, Type::Combinator.nominal_of("Array", type_args: [Type::Combinator.untyped])]
|
|
1583
|
+
end
|
|
1584
|
+
|
|
1585
|
+
# `{ key:, **rest }` binds `rest` to a Hash whose keys are
|
|
1586
|
+
# Symbols (the only legal key shape for a hash pattern) and
|
|
1587
|
+
# whose values are untyped (the binder can't see the subject's
|
|
1588
|
+
# value type).
|
|
1589
|
+
def hash_pattern_rest_type
|
|
1590
|
+
Type::Combinator.nominal_of(
|
|
1591
|
+
"Hash",
|
|
1592
|
+
type_args: [Type::Combinator.nominal_of("Symbol"), Type::Combinator.untyped]
|
|
1593
|
+
)
|
|
1594
|
+
end
|
|
1595
|
+
|
|
1596
|
+
# ---------------------------------------------------------------
|
|
1597
|
+
# named-capture regex binding (`MatchWriteNode`)
|
|
1598
|
+
# ---------------------------------------------------------------
|
|
1599
|
+
|
|
1600
|
+
# `/(?<year>\d+)/ =~ str` — Prism emits a `MatchWriteNode` that
|
|
1601
|
+
# wraps the `=~` call and lists the named-capture targets. Each
|
|
1602
|
+
# target is bound to `String | nil` (the capture is absent as nil
|
|
1603
|
+
# when the pattern doesn't match or the group didn't participate).
|
|
1604
|
+
def eval_match_write(node)
|
|
1605
|
+
match_type, post_scope = sub_eval(node.call, scope)
|
|
1606
|
+
string_or_nil = Type::Combinator.union(
|
|
1607
|
+
Type::Combinator.nominal_of("String"),
|
|
1608
|
+
Type::Combinator.constant_of(nil)
|
|
1609
|
+
)
|
|
1610
|
+
bound_scope = node.targets.reduce(post_scope) do |s, target|
|
|
1611
|
+
next s unless target.is_a?(Prism::LocalVariableTargetNode)
|
|
1612
|
+
|
|
1613
|
+
s.with_local(target.name, string_or_nil)
|
|
1614
|
+
end
|
|
1615
|
+
[match_type, bound_scope]
|
|
1616
|
+
end
|
|
1617
|
+
|
|
1618
|
+
# ---------------------------------------------------------------
|
|
1619
|
+
# shared type conversion helper
|
|
1620
|
+
# ---------------------------------------------------------------
|
|
1621
|
+
|
|
1622
|
+
# Converts a `Singleton[ClassName]` (the class object) to the
|
|
1623
|
+
# corresponding `Nominal[ClassName]` (an instance). Falls back to
|
|
1624
|
+
# `untyped` for carriers that are not Singleton (e.g. Dynamic[Top]
|
|
1625
|
+
# when the class could not be resolved).
|
|
1626
|
+
def singleton_to_nominal(type)
|
|
1627
|
+
type.is_a?(Type::Singleton) ? Type::Combinator.nominal_of(type.class_name) : Type::Combinator.untyped
|
|
1628
|
+
end
|
|
1629
|
+
|
|
1630
|
+
# Returns the type to bind for a `CapturePatternNode`'s target.
|
|
1631
|
+
# Plain class references collapse to the matching `Nominal[T]`;
|
|
1632
|
+
# `AlternationPatternNode` (`Integer | String => x`) unions every
|
|
1633
|
+
# alternate's resolved type. Anything else falls back to
|
|
1634
|
+
# `untyped` (the conservative legacy behaviour).
|
|
1635
|
+
def pattern_capture_type(value_node, scope)
|
|
1636
|
+
if value_node.is_a?(Prism::AlternationPatternNode)
|
|
1637
|
+
left = pattern_capture_type(value_node.left, scope)
|
|
1638
|
+
right = pattern_capture_type(value_node.right, scope)
|
|
1639
|
+
Type::Combinator.union(left, right)
|
|
1640
|
+
else
|
|
1641
|
+
singleton_to_nominal(sub_eval(value_node, scope).first)
|
|
1642
|
+
end
|
|
1643
|
+
end
|
|
1644
|
+
|
|
1645
|
+
# `in PatternA | PatternB` — Ruby requires both alternates to
|
|
1646
|
+
# bind the same names, but the binder runs against the AST and
|
|
1647
|
+
# cannot enforce that. We collect bindings from each side and
|
|
1648
|
+
# merge by name, unioning types when both alternates contribute.
|
|
1649
|
+
# Names that only one alternate contributes still surface (the
|
|
1650
|
+
# parser would have rejected the case at compile time, so by the
|
|
1651
|
+
# time we see it the user's intent is the merged set).
|
|
1652
|
+
def collect_alternation_pattern_bindings(subject, pattern, scope)
|
|
1653
|
+
left = collect_in_pattern_bindings(subject, pattern.left, scope)
|
|
1654
|
+
right = collect_in_pattern_bindings(subject, pattern.right, scope)
|
|
1655
|
+
merged = {}
|
|
1656
|
+
(left + right).each do |name, type|
|
|
1657
|
+
merged[name] = merged.key?(name) ? Type::Combinator.union(merged[name], type) : type
|
|
1658
|
+
end
|
|
1659
|
+
merged.to_a
|
|
1660
|
+
end
|
|
1321
1661
|
end
|
|
1322
1662
|
# rubocop:enable Metrics/ClassLength
|
|
1323
1663
|
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
|