rigortype 0.1.8 → 0.1.10
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 +186 -513
- data/lib/rigor/analysis/check_rules.rb +20 -0
- data/lib/rigor/analysis/runner.rb +67 -9
- data/lib/rigor/analysis/worker_session.rb +13 -4
- data/lib/rigor/cache/rbs_descriptor.rb +21 -2
- data/lib/rigor/cache/rbs_environment.rb +2 -1
- data/lib/rigor/cli/annotate_command.rb +274 -0
- data/lib/rigor/cli/baseline_command.rb +36 -16
- data/lib/rigor/cli/coverage_command.rb +126 -0
- data/lib/rigor/cli/coverage_renderer.rb +162 -0
- data/lib/rigor/cli/coverage_report.rb +75 -0
- data/lib/rigor/cli/mcp_command.rb +70 -0
- data/lib/rigor/cli/prism_colorizer.rb +111 -0
- data/lib/rigor/cli.rb +134 -6
- data/lib/rigor/environment/rbs_loader.rb +46 -5
- data/lib/rigor/environment/reporters.rb +3 -2
- data/lib/rigor/environment.rb +168 -5
- data/lib/rigor/inference/builtins/method_catalog.rb +17 -1
- data/lib/rigor/inference/builtins/time_catalog.rb +10 -1
- data/lib/rigor/inference/def_return_typer.rb +98 -0
- data/lib/rigor/inference/expression_typer.rb +308 -18
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +109 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +178 -10
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +53 -1
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +62 -15
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +149 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +20 -1
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +431 -9
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +126 -0
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +56 -0
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +148 -1
- data/lib/rigor/inference/method_parameter_binder.rb +67 -10
- data/lib/rigor/inference/narrowing.rb +29 -10
- data/lib/rigor/inference/precision_scanner.rb +131 -0
- data/lib/rigor/inference/statement_evaluator.rb +29 -3
- data/lib/rigor/mcp/loop.rb +43 -0
- data/lib/rigor/mcp/server.rb +263 -0
- data/lib/rigor/mcp.rb +16 -0
- data/lib/rigor/plugin/base.rb +67 -5
- data/lib/rigor/plugin/loader.rb +22 -1
- data/lib/rigor/plugin/manifest.rb +101 -10
- data/lib/rigor/plugin/protocol_contract.rb +185 -0
- data/lib/rigor/plugin/registry.rb +87 -0
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +51 -0
- data/lib/rigor/sig_gen/generator.rb +150 -75
- data/lib/rigor/triage/catalogue.rb +2 -2
- data/lib/rigor/type/combinator.rb +57 -0
- data/lib/rigor/type/constant.rb +29 -2
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +39 -0
- data/sig/rigor/environment.rbs +3 -2
- data/sig/rigor/type.rbs +4 -0
- data/sig/rigor.rbs +2 -0
- metadata +42 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../type"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
# Returns the inferred return type of a `Prism::DefNode`, or nil
|
|
10
|
+
# when no type can be derived (empty body, scope-lookup miss,
|
|
11
|
+
# or any failure during inference — caller surfaces "no
|
|
12
|
+
# annotation" on a nil).
|
|
13
|
+
#
|
|
14
|
+
# The inferred type is the union of:
|
|
15
|
+
#
|
|
16
|
+
# - the body's last-statement type, and
|
|
17
|
+
# - the type of every explicit `return value` reachable in the
|
|
18
|
+
# body. Nested `def` / lambda / block bodies are return
|
|
19
|
+
# barriers — their `return`s do not bubble up to the enclosing
|
|
20
|
+
# method.
|
|
21
|
+
#
|
|
22
|
+
# Extracted from `Rigor::SigGen::Generator#infer_return_type` so
|
|
23
|
+
# `LineTypeCollector` (`rigor annotate`'s def-line annotator)
|
|
24
|
+
# and the sig-generator share one source of truth.
|
|
25
|
+
module DefReturnTyper
|
|
26
|
+
RETURN_BARRIER_NODES = [Prism::DefNode, Prism::LambdaNode, Prism::BlockNode].freeze
|
|
27
|
+
private_constant :RETURN_BARRIER_NODES
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
def call(def_node, scope_index)
|
|
32
|
+
body = def_node.body
|
|
33
|
+
return nil if body.nil?
|
|
34
|
+
|
|
35
|
+
last = body_last_expression(body)
|
|
36
|
+
return nil if last.nil?
|
|
37
|
+
|
|
38
|
+
inner_scope = scope_index[last] || scope_index[body] || scope_index[def_node]
|
|
39
|
+
return nil if inner_scope.nil?
|
|
40
|
+
|
|
41
|
+
last_type = safe_type_of(inner_scope, last)
|
|
42
|
+
return nil if last_type.nil?
|
|
43
|
+
|
|
44
|
+
union_with_explicit_returns(body, last_type, scope_index)
|
|
45
|
+
rescue StandardError
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def body_last_expression(body)
|
|
50
|
+
case body
|
|
51
|
+
when Prism::StatementsNode then body.body.last
|
|
52
|
+
when Prism::BeginNode then body_last_expression(body.statements)
|
|
53
|
+
else body
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def union_with_explicit_returns(body, last_type, scope_index)
|
|
58
|
+
return_types = []
|
|
59
|
+
collect_return_types(body, scope_index, return_types)
|
|
60
|
+
return last_type if return_types.empty?
|
|
61
|
+
|
|
62
|
+
Type::Combinator.union(last_type, *return_types)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def collect_return_types(node, scope_index, out)
|
|
66
|
+
return unless node.is_a?(Prism::Node)
|
|
67
|
+
return if RETURN_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
68
|
+
|
|
69
|
+
type_return_node(node, scope_index, out) if node.is_a?(Prism::ReturnNode)
|
|
70
|
+
node.compact_child_nodes.each { |c| collect_return_types(c, scope_index, out) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def type_return_node(return_node, scope_index, out)
|
|
74
|
+
args = return_node.arguments&.arguments || []
|
|
75
|
+
if args.empty?
|
|
76
|
+
out << Type::Combinator.constant_of(nil)
|
|
77
|
+
return
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
scope = scope_index[return_node] || scope_index[args.first]
|
|
81
|
+
return if scope.nil?
|
|
82
|
+
# `return a, b` packs into a Tuple at runtime; the MVP only
|
|
83
|
+
# handles the single-value form. Multi-arg returns
|
|
84
|
+
# contribute no type to keep the implementation focused.
|
|
85
|
+
return unless args.size == 1
|
|
86
|
+
|
|
87
|
+
type = safe_type_of(scope, args.first)
|
|
88
|
+
out << type unless type.nil?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def safe_type_of(scope, node)
|
|
92
|
+
scope.type_of(node)
|
|
93
|
+
rescue StandardError
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -8,6 +8,7 @@ require_relative "block_parameter_binder"
|
|
|
8
8
|
require_relative "fallback"
|
|
9
9
|
require_relative "macro_block_self_type"
|
|
10
10
|
require_relative "method_dispatcher"
|
|
11
|
+
require_relative "narrowing"
|
|
11
12
|
|
|
12
13
|
module Rigor
|
|
13
14
|
module Inference
|
|
@@ -660,16 +661,26 @@ module Rigor
|
|
|
660
661
|
end
|
|
661
662
|
|
|
662
663
|
# Returns `:truthy`, `:falsey`, or `nil` for an arbitrary
|
|
663
|
-
# predicate expression.
|
|
664
|
-
#
|
|
665
|
-
#
|
|
664
|
+
# predicate expression under three-valued logic. Uses the
|
|
665
|
+
# same {Narrowing} probe as `StatementEvaluator#eval_if`:
|
|
666
|
+
# the predicate is truthy when its falsey fragment is `Bot`,
|
|
667
|
+
# falsey when its truthy fragment is `Bot`. So
|
|
668
|
+
# `Nominal[Integer]` (always truthy in Ruby), `Constant[nil]`,
|
|
669
|
+
# and `Constant[false]` fold one branch; `Union[true, false]`,
|
|
670
|
+
# `Dynamic[T]`, and `Top` keep both branches live.
|
|
666
671
|
def constant_predicate_polarity(predicate)
|
|
667
672
|
return nil if predicate.nil?
|
|
668
673
|
|
|
669
674
|
type = type_of(predicate)
|
|
670
|
-
return nil
|
|
675
|
+
return nil if type.nil? || type.is_a?(Type::Bot)
|
|
671
676
|
|
|
672
|
-
type.
|
|
677
|
+
truthy_bot = Narrowing.narrow_truthy(type).is_a?(Type::Bot)
|
|
678
|
+
falsey_bot = Narrowing.narrow_falsey(type).is_a?(Type::Bot)
|
|
679
|
+
|
|
680
|
+
return :falsey if truthy_bot && !falsey_bot
|
|
681
|
+
return :truthy if !truthy_bot && falsey_bot
|
|
682
|
+
|
|
683
|
+
nil
|
|
673
684
|
end
|
|
674
685
|
|
|
675
686
|
def type_of_else(node)
|
|
@@ -712,15 +723,135 @@ module Rigor
|
|
|
712
723
|
type.value ? :truthy : :falsey
|
|
713
724
|
end
|
|
714
725
|
|
|
726
|
+
# Three-valued evaluation of `case predicate when pattern`
|
|
727
|
+
# dispatch. For each `when` clause we ask: under static types,
|
|
728
|
+
# does `pattern === predicate` definitely match (`:yes`),
|
|
729
|
+
# definitely not match (`:no`), or possibly match (`:maybe`)?
|
|
730
|
+
# Walking in source order:
|
|
731
|
+
#
|
|
732
|
+
# - `:yes` — this branch fires, subsequent branches are
|
|
733
|
+
# unreachable. Result = union(prior `:maybe` branches, this
|
|
734
|
+
# `:yes` branch).
|
|
735
|
+
# - `:no` — branch dropped.
|
|
736
|
+
# - `:maybe` — branch is a candidate, continue.
|
|
737
|
+
#
|
|
738
|
+
# If no `:yes` was reached, the else clause (or `Constant[nil]`
|
|
739
|
+
# when absent) is added to the candidate set.
|
|
740
|
+
#
|
|
741
|
+
# The `case ... in` pattern-matching form (`CaseMatchNode`) and
|
|
742
|
+
# the predicate-less form (`case; when c1; ...`) bypass the
|
|
743
|
+
# `===` analysis: pattern matching has richer semantics, and a
|
|
744
|
+
# predicate-less `case` reduces to a `if c1; ...; elsif c2`
|
|
745
|
+
# chain that statement-level narrowing already handles.
|
|
715
746
|
def type_of_case(node)
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
747
|
+
return type_of_case_simple_union(node) if node.is_a?(Prism::CaseMatchNode) || node.predicate.nil?
|
|
748
|
+
|
|
749
|
+
subject_type = type_of(node.predicate)
|
|
750
|
+
candidates = []
|
|
751
|
+
reached_yes = false
|
|
752
|
+
|
|
753
|
+
node.conditions.each do |when_node|
|
|
754
|
+
case case_when_branch_certainty(subject_type, when_node)
|
|
755
|
+
when :yes
|
|
756
|
+
candidates << type_of(when_node)
|
|
757
|
+
reached_yes = true
|
|
758
|
+
break
|
|
759
|
+
when :maybe
|
|
760
|
+
candidates << type_of(when_node)
|
|
761
|
+
# :no — drop the branch
|
|
722
762
|
end
|
|
723
|
-
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
candidates << type_of_case_else(node) unless reached_yes
|
|
766
|
+
Type::Combinator.union(*candidates)
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
def type_of_case_simple_union(node)
|
|
770
|
+
branch_types = node.conditions.map { |branch| type_of(branch) }
|
|
771
|
+
Type::Combinator.union(*branch_types, type_of_case_else(node))
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
def type_of_case_else(node)
|
|
775
|
+
return Type::Combinator.constant_of(nil) if node.else_clause.nil?
|
|
776
|
+
|
|
777
|
+
type_of(node.else_clause)
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
# Combines per-pattern certainty across a `when` clause's
|
|
781
|
+
# conditions (`when a, b, c` ≡ `a === s || b === s || c === s`).
|
|
782
|
+
# `:yes` if any pattern is `:yes`; `:no` if all are `:no`;
|
|
783
|
+
# `:maybe` otherwise.
|
|
784
|
+
def case_when_branch_certainty(subject_type, when_node)
|
|
785
|
+
return :maybe unless when_node.respond_to?(:conditions)
|
|
786
|
+
|
|
787
|
+
results = when_node.conditions.map { |c| case_when_pattern_certainty(subject_type, c) }
|
|
788
|
+
return :maybe if results.empty?
|
|
789
|
+
return :yes if results.include?(:yes)
|
|
790
|
+
return :no if results.all?(:no)
|
|
791
|
+
|
|
792
|
+
:maybe
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
# Static three-valued certainty for `pattern === subject`.
|
|
796
|
+
# Specialises two pattern shapes:
|
|
797
|
+
#
|
|
798
|
+
# - **Class / Module reference** (`Integer`, `Foo::Bar`):
|
|
799
|
+
# reduce to `subject.is_a?(class)` via
|
|
800
|
+
# `Narrowing.narrow_class` / `narrow_not_class`. A Bot
|
|
801
|
+
# truthy fragment means no inhabitant matches (`:no`); a
|
|
802
|
+
# Bot falsey fragment means every inhabitant matches
|
|
803
|
+
# (`:yes`).
|
|
804
|
+
# - **Value-equality literal** (numeric / String / Symbol /
|
|
805
|
+
# true / false / nil) against a `Constant[c]` subject:
|
|
806
|
+
# the static comparison `pattern_value === c` is exact.
|
|
807
|
+
# Other subject carriers stay `:maybe` because the
|
|
808
|
+
# runtime value isn't pinned.
|
|
809
|
+
#
|
|
810
|
+
# Other pattern shapes (Range, Regexp, custom `===`) stay
|
|
811
|
+
# `:maybe` — the existing union fallback handles them.
|
|
812
|
+
def case_when_pattern_certainty(subject_type, pattern_node)
|
|
813
|
+
class_name = build_constant_path_name(pattern_node)
|
|
814
|
+
return class_pattern_certainty(subject_type, class_name) if class_name
|
|
815
|
+
|
|
816
|
+
literal = literal_pattern_value(pattern_node)
|
|
817
|
+
return literal_pattern_certainty(subject_type, literal[:value]) if literal
|
|
818
|
+
|
|
819
|
+
:maybe
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
def class_pattern_certainty(subject_type, class_name)
|
|
823
|
+
env = scope.environment
|
|
824
|
+
truthy_bot = Narrowing.narrow_class(subject_type, class_name, environment: env).is_a?(Type::Bot)
|
|
825
|
+
falsey_bot = Narrowing.narrow_not_class(subject_type, class_name, environment: env).is_a?(Type::Bot)
|
|
826
|
+
|
|
827
|
+
return :no if truthy_bot && !falsey_bot
|
|
828
|
+
return :yes if !truthy_bot && falsey_bot
|
|
829
|
+
|
|
830
|
+
:maybe
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
VALUE_EQUALITY_CLASSES = [Integer, Float, Rational, Complex, String, Symbol,
|
|
834
|
+
TrueClass, FalseClass, NilClass].freeze
|
|
835
|
+
private_constant :VALUE_EQUALITY_CLASSES
|
|
836
|
+
|
|
837
|
+
# Returns `{ value: v }` when `pattern_node` types to a
|
|
838
|
+
# `Constant[v]` of a value-equality-safe class (so `===`
|
|
839
|
+
# reduces to `==`), else nil. Wrapped in a hash so a literal
|
|
840
|
+
# `nil` / `false` value doesn't collide with the "no literal"
|
|
841
|
+
# signal.
|
|
842
|
+
def literal_pattern_value(pattern_node)
|
|
843
|
+
type = type_of(pattern_node)
|
|
844
|
+
return nil unless type.is_a?(Type::Constant)
|
|
845
|
+
return nil unless VALUE_EQUALITY_CLASSES.any? { |klass| type.value.is_a?(klass) }
|
|
846
|
+
|
|
847
|
+
{ value: type.value }
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
def literal_pattern_certainty(subject_type, pattern_value)
|
|
851
|
+
return :maybe unless subject_type.is_a?(Type::Constant)
|
|
852
|
+
return :maybe unless VALUE_EQUALITY_CLASSES.any? { |klass| subject_type.value.is_a?(klass) }
|
|
853
|
+
|
|
854
|
+
pattern_value == subject_type.value ? :yes : :no
|
|
724
855
|
end
|
|
725
856
|
|
|
726
857
|
# `when` clauses for `case` and `in` clauses for `case ... in` have
|
|
@@ -1056,6 +1187,9 @@ module Rigor
|
|
|
1056
1187
|
per_element = try_per_element_block_fold(node, receiver)
|
|
1057
1188
|
return per_element if per_element
|
|
1058
1189
|
|
|
1190
|
+
hash_transform = try_hash_shape_block_fold(node, receiver)
|
|
1191
|
+
return hash_transform if hash_transform
|
|
1192
|
+
|
|
1059
1193
|
result = MethodDispatcher.dispatch(
|
|
1060
1194
|
receiver_type: receiver,
|
|
1061
1195
|
method_name: node.name,
|
|
@@ -1455,10 +1589,17 @@ module Rigor
|
|
|
1455
1589
|
# the dispatch chain untouched.
|
|
1456
1590
|
PER_ELEMENT_TUPLE_METHODS = Set[
|
|
1457
1591
|
:map, :collect, :filter_map, :flat_map,
|
|
1592
|
+
:select, :filter, :reject,
|
|
1458
1593
|
:find, :detect, :find_index, :index
|
|
1459
1594
|
].freeze
|
|
1460
1595
|
private_constant :PER_ELEMENT_TUPLE_METHODS
|
|
1461
1596
|
|
|
1597
|
+
HASH_SHAPE_TRANSFORM_METHODS = Set[
|
|
1598
|
+
:transform_keys, :transform_keys!,
|
|
1599
|
+
:transform_values, :transform_values!
|
|
1600
|
+
].freeze
|
|
1601
|
+
private_constant :HASH_SHAPE_TRANSFORM_METHODS
|
|
1602
|
+
|
|
1462
1603
|
# Cardinality cap for per-element block fold over
|
|
1463
1604
|
# finite-bound `Constant<Range>` receivers. Walking
|
|
1464
1605
|
# `(1..1_000_000).map { … }` element-wise would balloon
|
|
@@ -1475,15 +1616,50 @@ module Rigor
|
|
|
1475
1616
|
element_types = per_element_elements_of(receiver_type)
|
|
1476
1617
|
return nil if element_types.nil? || element_types.empty?
|
|
1477
1618
|
|
|
1478
|
-
|
|
1479
|
-
return nil
|
|
1619
|
+
per_position = per_element_block_results(call_node.block, element_types)
|
|
1620
|
+
return nil if per_position.nil? || per_position.any?(&:nil?)
|
|
1480
1621
|
|
|
1481
|
-
per_position
|
|
1482
|
-
|
|
1622
|
+
assemble_per_element_result(call_node.name, per_position, element_types)
|
|
1623
|
+
end
|
|
1624
|
+
|
|
1625
|
+
# Evaluates the call's block once per receiver element.
|
|
1626
|
+
# Two block shapes are supported:
|
|
1627
|
+
#
|
|
1628
|
+
# - `Prism::BlockNode` — a full `do … end` / `{ … }` block;
|
|
1629
|
+
# the body is re-typed per position with the element
|
|
1630
|
+
# bound to the block parameter.
|
|
1631
|
+
# - `Prism::BlockArgumentNode` wrapping a `SymbolNode` —
|
|
1632
|
+
# the `&:predicate` shorthand; the symbol is dispatched
|
|
1633
|
+
# as a zero-arg method on each element type.
|
|
1634
|
+
#
|
|
1635
|
+
# Any other shape (`&proc_local`, `&method(:foo)`, no
|
|
1636
|
+
# block) returns `nil` so the fold declines.
|
|
1637
|
+
def per_element_block_results(block, element_types)
|
|
1638
|
+
case block
|
|
1639
|
+
when Prism::BlockNode
|
|
1640
|
+
element_types.map { |element_type| type_block_body_with_param(block, [element_type]) }
|
|
1641
|
+
when Prism::BlockArgumentNode
|
|
1642
|
+
per_element_symbol_results(block, element_types)
|
|
1483
1643
|
end
|
|
1484
|
-
|
|
1644
|
+
end
|
|
1485
1645
|
|
|
1486
|
-
|
|
1646
|
+
def per_element_symbol_results(block_arg, element_types)
|
|
1647
|
+
expression = block_arg.expression
|
|
1648
|
+
return nil unless expression.is_a?(Prism::SymbolNode)
|
|
1649
|
+
|
|
1650
|
+
method_name = expression.unescaped.to_sym
|
|
1651
|
+
element_types.map do |element_type|
|
|
1652
|
+
MethodDispatcher.dispatch(
|
|
1653
|
+
receiver_type: element_type,
|
|
1654
|
+
method_name: method_name,
|
|
1655
|
+
arg_types: [],
|
|
1656
|
+
block_type: nil,
|
|
1657
|
+
environment: scope.environment,
|
|
1658
|
+
scope: scope
|
|
1659
|
+
)
|
|
1660
|
+
end
|
|
1661
|
+
rescue StandardError
|
|
1662
|
+
nil
|
|
1487
1663
|
end
|
|
1488
1664
|
|
|
1489
1665
|
# Returns the per-position element types for a finite,
|
|
@@ -1532,11 +1708,37 @@ module Rigor
|
|
|
1532
1708
|
when :map, :collect then Type::Combinator.tuple_of(*per_position)
|
|
1533
1709
|
when :filter_map then assemble_filter_map_result(per_position)
|
|
1534
1710
|
when :flat_map then assemble_flat_map_result(per_position)
|
|
1711
|
+
when :select, :filter
|
|
1712
|
+
assemble_filter_result(per_position, element_types, keep_on_truthy: true)
|
|
1713
|
+
when :reject
|
|
1714
|
+
assemble_filter_result(per_position, element_types, keep_on_truthy: false)
|
|
1535
1715
|
when :find, :detect then assemble_find_result(per_position, element_types)
|
|
1536
1716
|
when :find_index, :index then assemble_find_index_result(per_position)
|
|
1537
1717
|
end
|
|
1538
1718
|
end
|
|
1539
1719
|
|
|
1720
|
+
# `select` / `filter` / `reject`: keeps each receiver
|
|
1721
|
+
# element whose per-position predicate result folds to a
|
|
1722
|
+
# decisive `Constant` — Ruby-truthy for `select` / `filter`,
|
|
1723
|
+
# Ruby-falsey for `reject`. The surviving elements assemble
|
|
1724
|
+
# into a `Tuple`, strictly tighter than the RBS-projected
|
|
1725
|
+
# `Array[Elem]`.
|
|
1726
|
+
#
|
|
1727
|
+
# Folds tightly only when EVERY position is a `Constant`:
|
|
1728
|
+
# a single non-`Constant` position leaves the result
|
|
1729
|
+
# cardinality unknown (the element might or might not
|
|
1730
|
+
# survive), so the dispatcher declines and the RBS tier
|
|
1731
|
+
# widens to `Array[Elem]`. `[].select` style empty results
|
|
1732
|
+
# are sound — an empty `Tuple` is the empty-array carrier.
|
|
1733
|
+
def assemble_filter_result(per_position, element_types, keep_on_truthy:)
|
|
1734
|
+
return nil unless per_position.all?(Type::Constant)
|
|
1735
|
+
|
|
1736
|
+
kept = element_types.each_index.filter_map do |index|
|
|
1737
|
+
element_types[index] if truthy_constant?(per_position[index]) == keep_on_truthy
|
|
1738
|
+
end
|
|
1739
|
+
Type::Combinator.tuple_of(*kept)
|
|
1740
|
+
end
|
|
1741
|
+
|
|
1540
1742
|
# `filter_map` folds tightly only when every per-position
|
|
1541
1743
|
# result is a `Constant`: positions whose value is `nil`
|
|
1542
1744
|
# or `false` drop, the rest survive in declaration order.
|
|
@@ -1611,6 +1813,94 @@ module Rigor
|
|
|
1611
1813
|
type.is_a?(Type::Constant) && type.value && type.value != false
|
|
1612
1814
|
end
|
|
1613
1815
|
|
|
1816
|
+
# Per-pair block fold for `HashShape#transform_keys` and
|
|
1817
|
+
# `HashShape#transform_values` (and their bang variants).
|
|
1818
|
+
#
|
|
1819
|
+
# When the receiver is a closed `HashShape` with no optional
|
|
1820
|
+
# keys, applies the call's block (a `Prism::BlockNode` or
|
|
1821
|
+
# `Prism::BlockArgumentNode`) to each key/value pair
|
|
1822
|
+
# independently and assembles a new `HashShape`:
|
|
1823
|
+
#
|
|
1824
|
+
# - `transform_values` / `transform_values!`: re-types
|
|
1825
|
+
# each VALUE by binding it to the block parameter; keys
|
|
1826
|
+
# are preserved unchanged.
|
|
1827
|
+
# - `transform_keys` / `transform_keys!`: re-types each
|
|
1828
|
+
# KEY by wrapping it in `Constant[k]` and passing it to
|
|
1829
|
+
# the block; values are preserved unchanged. The result
|
|
1830
|
+
# key must be a `Constant[Symbol | String]` — otherwise
|
|
1831
|
+
# the tier declines (the new key cannot be used as a
|
|
1832
|
+
# static HashShape index). Collisions (two old keys
|
|
1833
|
+
# mapping to the same new key) also decline.
|
|
1834
|
+
#
|
|
1835
|
+
# Returns `nil` on any decline so the dispatcher falls
|
|
1836
|
+
# through to `RbsDispatch` and gets the widened `Hash[K, V]`
|
|
1837
|
+
# answer.
|
|
1838
|
+
def try_hash_shape_block_fold(call_node, receiver_type)
|
|
1839
|
+
return nil unless HASH_SHAPE_TRANSFORM_METHODS.include?(call_node.name)
|
|
1840
|
+
return nil unless receiver_type.is_a?(Type::HashShape)
|
|
1841
|
+
return nil unless receiver_type.closed?
|
|
1842
|
+
return nil unless receiver_type.optional_keys.empty?
|
|
1843
|
+
|
|
1844
|
+
block_arg = call_node.block
|
|
1845
|
+
return nil if block_arg.nil?
|
|
1846
|
+
|
|
1847
|
+
if %i[transform_values transform_values!].include?(call_node.name)
|
|
1848
|
+
fold_hash_shape_transform_values(receiver_type, block_arg)
|
|
1849
|
+
else
|
|
1850
|
+
fold_hash_shape_transform_keys(receiver_type, block_arg)
|
|
1851
|
+
end
|
|
1852
|
+
end
|
|
1853
|
+
|
|
1854
|
+
def fold_hash_shape_transform_values(shape, block_arg)
|
|
1855
|
+
new_pairs = {}
|
|
1856
|
+
shape.pairs.each do |key, value|
|
|
1857
|
+
new_value = apply_hash_block(block_arg, value)
|
|
1858
|
+
return nil if new_value.nil?
|
|
1859
|
+
|
|
1860
|
+
new_pairs[key] = new_value
|
|
1861
|
+
end
|
|
1862
|
+
Type::Combinator.hash_shape_of(new_pairs)
|
|
1863
|
+
end
|
|
1864
|
+
|
|
1865
|
+
def fold_hash_shape_transform_keys(shape, block_arg)
|
|
1866
|
+
new_pairs = {}
|
|
1867
|
+
shape.pairs.each do |key, value|
|
|
1868
|
+
key_type = Type::Combinator.constant_of(key)
|
|
1869
|
+
new_key_type = apply_hash_block(block_arg, key_type)
|
|
1870
|
+
return nil unless new_key_type.is_a?(Type::Constant)
|
|
1871
|
+
|
|
1872
|
+
new_key = new_key_type.value
|
|
1873
|
+
return nil unless new_key.is_a?(Symbol) || new_key.is_a?(String)
|
|
1874
|
+
return nil if new_pairs.key?(new_key)
|
|
1875
|
+
|
|
1876
|
+
new_pairs[new_key] = value
|
|
1877
|
+
end
|
|
1878
|
+
Type::Combinator.hash_shape_of(new_pairs)
|
|
1879
|
+
end
|
|
1880
|
+
|
|
1881
|
+
# Applies a single-argument block (either a full BlockNode
|
|
1882
|
+
# or a `&:symbol` BlockArgumentNode) to `param_type` and
|
|
1883
|
+
# returns the resulting type, or `nil` on failure.
|
|
1884
|
+
def apply_hash_block(block_arg, param_type)
|
|
1885
|
+
case block_arg
|
|
1886
|
+
when Prism::BlockNode
|
|
1887
|
+
type_block_body_with_param(block_arg, [param_type])
|
|
1888
|
+
when Prism::BlockArgumentNode
|
|
1889
|
+
expression = block_arg.expression
|
|
1890
|
+
return nil unless expression.is_a?(Prism::SymbolNode)
|
|
1891
|
+
|
|
1892
|
+
MethodDispatcher.dispatch(
|
|
1893
|
+
receiver_type: param_type,
|
|
1894
|
+
method_name: expression.unescaped.to_sym,
|
|
1895
|
+
arg_types: [],
|
|
1896
|
+
block_type: nil,
|
|
1897
|
+
environment: scope.environment,
|
|
1898
|
+
call_node: block_arg,
|
|
1899
|
+
scope: scope
|
|
1900
|
+
)
|
|
1901
|
+
end
|
|
1902
|
+
end
|
|
1903
|
+
|
|
1614
1904
|
def type_block_body_with_param(block_node, expected_param_types)
|
|
1615
1905
|
bindings = BlockParameterBinder.new(expected_param_types: expected_param_types).bind(block_node)
|
|
1616
1906
|
block_scope = bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi/util"
|
|
4
|
+
require_relative "../../type"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
module MethodDispatcher
|
|
9
|
+
# Folds `CGI` module-function calls on statically known
|
|
10
|
+
# string constants.
|
|
11
|
+
#
|
|
12
|
+
# `CGI.escapeHTML` / `CGI.unescapeHTML` and related methods
|
|
13
|
+
# are pure, deterministic functions over their string inputs.
|
|
14
|
+
# When the argument is a `Constant[String]`, the analyzer can
|
|
15
|
+
# evaluate the call at inference time and return the concrete
|
|
16
|
+
# `Constant[String]` result.
|
|
17
|
+
#
|
|
18
|
+
# === Supported methods
|
|
19
|
+
#
|
|
20
|
+
# * `escapeHTML(str)` / `escape_html(str)` / `h(str)` —
|
|
21
|
+
# HTML-escape. Returns `Constant[String]`.
|
|
22
|
+
# * `unescapeHTML(str)` / `unescape_html(str)` —
|
|
23
|
+
# HTML-unescape. Returns `Constant[String]`.
|
|
24
|
+
# * `escape(str)` / `unescape(str)` —
|
|
25
|
+
# URL-encode / decode (`application/x-www-form-urlencoded`).
|
|
26
|
+
# Returns `Constant[String]`.
|
|
27
|
+
# * `escapeURIComponent(str)` / `escape_uri_component(str)`,
|
|
28
|
+
# `unescapeURIComponent(str)` / `unescape_uri_component(str)` —
|
|
29
|
+
# URI-component percent-encode / decode. Returns `Constant[String]`.
|
|
30
|
+
# * `escapeElement(str, *elements)` / `escape_element(str, *elements)`,
|
|
31
|
+
# `unescapeElement(str, *elements)` / `unescape_element(str, *elements)` —
|
|
32
|
+
# element-level escape / unescape (first arg is the string,
|
|
33
|
+
# remaining args are element names). Returns `Constant[String]`.
|
|
34
|
+
#
|
|
35
|
+
# === Non-constant / unsupported cases
|
|
36
|
+
#
|
|
37
|
+
# Returns `nil` (deferring to the next dispatcher tier) when:
|
|
38
|
+
# - the receiver is not `Singleton[CGI]`,
|
|
39
|
+
# - the first argument is not a `Constant[String]`,
|
|
40
|
+
# - the method is not in the supported set.
|
|
41
|
+
module CGIFolding
|
|
42
|
+
CGI_HTML_ESCAPE_METHODS = Set[:escapeHTML, :escape_html, :h].freeze
|
|
43
|
+
CGI_HTML_UNESCAPE_METHODS = Set[:unescapeHTML, :unescape_html].freeze
|
|
44
|
+
CGI_URL_ESCAPE_METHODS = Set[:escape, :unescape].freeze
|
|
45
|
+
CGI_URI_ESCAPE_METHODS = Set[
|
|
46
|
+
:escapeURIComponent, :escape_uri_component,
|
|
47
|
+
:unescapeURIComponent, :unescape_uri_component
|
|
48
|
+
].freeze
|
|
49
|
+
CGI_ELEMENT_ESCAPE_METHODS = Set[
|
|
50
|
+
:escapeElement, :escape_element,
|
|
51
|
+
:unescapeElement, :unescape_element
|
|
52
|
+
].freeze
|
|
53
|
+
CGI_ALL_ESCAPE_METHODS = (
|
|
54
|
+
CGI_HTML_ESCAPE_METHODS | CGI_HTML_UNESCAPE_METHODS |
|
|
55
|
+
CGI_URL_ESCAPE_METHODS | CGI_URI_ESCAPE_METHODS |
|
|
56
|
+
CGI_ELEMENT_ESCAPE_METHODS
|
|
57
|
+
).freeze
|
|
58
|
+
|
|
59
|
+
private_constant :CGI_HTML_ESCAPE_METHODS, :CGI_HTML_UNESCAPE_METHODS,
|
|
60
|
+
:CGI_URL_ESCAPE_METHODS, :CGI_URI_ESCAPE_METHODS,
|
|
61
|
+
:CGI_ELEMENT_ESCAPE_METHODS, :CGI_ALL_ESCAPE_METHODS
|
|
62
|
+
|
|
63
|
+
module_function
|
|
64
|
+
|
|
65
|
+
# @return [Rigor::Type, nil] folded result, or nil to defer.
|
|
66
|
+
def try_dispatch(receiver:, method_name:, args:)
|
|
67
|
+
return nil unless dispatch_target?(receiver)
|
|
68
|
+
return nil unless CGI_ALL_ESCAPE_METHODS.include?(method_name)
|
|
69
|
+
|
|
70
|
+
fold_cgi_call(method_name, args)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def dispatch_target?(receiver)
|
|
74
|
+
receiver.is_a?(Type::Singleton) && receiver.class_name == "CGI"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def fold_cgi_call(method_name, args)
|
|
78
|
+
return nil if args.empty?
|
|
79
|
+
return nil unless args.first.is_a?(Type::Constant) && args.first.value.is_a?(String)
|
|
80
|
+
|
|
81
|
+
str = args.first.value
|
|
82
|
+
|
|
83
|
+
if CGI_ELEMENT_ESCAPE_METHODS.include?(method_name)
|
|
84
|
+
fold_cgi_element(method_name, str, args.drop(1))
|
|
85
|
+
else
|
|
86
|
+
Type::Combinator.constant_of(CGI.public_send(method_name, str))
|
|
87
|
+
end
|
|
88
|
+
rescue StandardError
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# `CGI.escapeElement(str, "elem1", "elem2", ...)` — element-
|
|
93
|
+
# level escape / unescape. The remaining args after the first
|
|
94
|
+
# must be `Constant[String]` element names.
|
|
95
|
+
def fold_cgi_element(method_name, str, element_args)
|
|
96
|
+
elements = element_args.map do |arg|
|
|
97
|
+
return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(String)
|
|
98
|
+
|
|
99
|
+
arg.value
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
Type::Combinator.constant_of(CGI.public_send(method_name, str, *elements))
|
|
103
|
+
rescue StandardError
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|