rigortype 0.1.7 → 0.1.9
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 +23 -1
- data/lib/rigor/analysis/diagnostic.rb +17 -3
- data/lib/rigor/analysis/runner.rb +178 -3
- data/lib/rigor/analysis/worker_session.rb +14 -3
- data/lib/rigor/cli/annotate_command.rb +224 -0
- data/lib/rigor/cli/baseline_command.rb +36 -16
- data/lib/rigor/cli/prism_colorizer.rb +111 -0
- data/lib/rigor/cli/triage_command.rb +83 -0
- data/lib/rigor/cli/triage_renderer.rb +77 -0
- data/lib/rigor/cli.rb +71 -5
- data/lib/rigor/environment.rb +9 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +17 -1
- data/lib/rigor/inference/builtins/time_catalog.rb +10 -1
- data/lib/rigor/inference/expression_typer.rb +300 -18
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +109 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +173 -10
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +53 -1
- 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/rbs_dispatch.rb +33 -8
- 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 +316 -2
- 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 +179 -4
- data/lib/rigor/inference/method_parameter_binder.rb +67 -10
- data/lib/rigor/inference/narrowing.rb +29 -10
- data/lib/rigor/inference/scope_indexer.rb +156 -6
- data/lib/rigor/inference/statement_evaluator.rb +43 -21
- data/lib/rigor/plugin/base.rb +39 -0
- data/lib/rigor/plugin/loader.rb +22 -1
- data/lib/rigor/plugin/manifest.rb +73 -10
- data/lib/rigor/plugin/protocol_contract.rb +185 -0
- data/lib/rigor/plugin/registry.rb +66 -0
- data/lib/rigor/scope.rb +46 -0
- data/lib/rigor/triage/catalogue.rb +296 -0
- data/lib/rigor/triage/hint.rb +27 -0
- data/lib/rigor/triage.rb +89 -0
- data/lib/rigor/type/constant.rb +29 -2
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/inference.rbs +1 -0
- data/sig/rigor/scope.rbs +6 -0
- metadata +16 -1
|
@@ -1025,11 +1025,15 @@ module Rigor
|
|
|
1025
1025
|
local_def = node.receiver.nil? ? scope.top_level_def_for(node.name) : nil
|
|
1026
1026
|
if local_def
|
|
1027
1027
|
local_inference = infer_top_level_user_method(local_def, receiver, arg_types)
|
|
1028
|
-
return local_inference if local_inference
|
|
1029
|
-
|
|
1030
|
-
# The local def matches by name but the
|
|
1031
|
-
#
|
|
1032
|
-
#
|
|
1028
|
+
return local_inference if local_inference && adoptable_self_call_result?(local_inference)
|
|
1029
|
+
|
|
1030
|
+
# The local def matches by name but the inference
|
|
1031
|
+
# was disqualified — either the parameter shape is
|
|
1032
|
+
# too complex for the first-iteration binder
|
|
1033
|
+
# (kwargs / optionals / rest), or ADR-24 slice 1's
|
|
1034
|
+
# conservative gate declined the resolved return
|
|
1035
|
+
# type inside a class body (see
|
|
1036
|
+
# `adoptable_self_call_result?`).
|
|
1033
1037
|
# Returning `Dynamic[Top]` is the safest answer:
|
|
1034
1038
|
# we know RBS dispatch would be wrong (the
|
|
1035
1039
|
# method is user-defined and shadows whatever
|
|
@@ -1052,6 +1056,9 @@ module Rigor
|
|
|
1052
1056
|
per_element = try_per_element_block_fold(node, receiver)
|
|
1053
1057
|
return per_element if per_element
|
|
1054
1058
|
|
|
1059
|
+
hash_transform = try_hash_shape_block_fold(node, receiver)
|
|
1060
|
+
return hash_transform if hash_transform
|
|
1061
|
+
|
|
1055
1062
|
result = MethodDispatcher.dispatch(
|
|
1056
1063
|
receiver_type: receiver,
|
|
1057
1064
|
method_name: node.name,
|
|
@@ -1069,7 +1076,11 @@ module Rigor
|
|
|
1069
1076
|
# the body with the call's argument types bound and
|
|
1070
1077
|
# return the body's last-expression type.
|
|
1071
1078
|
user_inference = try_user_method_inference(receiver, node, arg_types)
|
|
1072
|
-
|
|
1079
|
+
if user_inference
|
|
1080
|
+
return user_inference if adoptable_self_call_result?(user_inference)
|
|
1081
|
+
|
|
1082
|
+
return dynamic_top
|
|
1083
|
+
end
|
|
1073
1084
|
|
|
1074
1085
|
# Dynamic-origin propagation: when the receiver is Dynamic[T] and
|
|
1075
1086
|
# no positive rule resolves the call, the result inherits the
|
|
@@ -1112,10 +1123,39 @@ module Rigor
|
|
|
1112
1123
|
nil
|
|
1113
1124
|
end
|
|
1114
1125
|
|
|
1126
|
+
# ADR-24 slice 1 — implicit-self method-call resolution.
|
|
1127
|
+
# `discovered_def_nodes` is now carried into method /
|
|
1128
|
+
# class body scopes (see `StatementEvaluator#build_fresh_body_scope`),
|
|
1129
|
+
# so a call written with no explicit receiver inside a
|
|
1130
|
+
# method body resolves against the enclosing class's own
|
|
1131
|
+
# definitions and the file's top-level defs. Before
|
|
1132
|
+
# slice 1 every such call typed `Dynamic[top]`.
|
|
1133
|
+
#
|
|
1134
|
+
# The adoption of the resolved return type is gated:
|
|
1135
|
+
#
|
|
1136
|
+
# - At top-level / inside a DSL block (`scope.self_type`
|
|
1137
|
+
# is nil) the result is adopted unchanged — this is
|
|
1138
|
+
# the pre-slice-1 surface (the v0.0.3 A local-`def`
|
|
1139
|
+
# shortcut) and MUST keep working.
|
|
1140
|
+
# - Inside a class body / method body (`self_type` set)
|
|
1141
|
+
# the result is adopted ONLY when it is `Bot`. A `Bot`
|
|
1142
|
+
# return is an always-diverging guard helper; adopting
|
|
1143
|
+
# it can only ever enable correct terminating-branch
|
|
1144
|
+
# narrowing, never a new `undefined-method` /
|
|
1145
|
+
# argument-type false positive. A non-`Bot` resolved
|
|
1146
|
+
# return is kept as `Dynamic[top]` (WD3) — adopting
|
|
1147
|
+
# precise non-`Bot` returns project-wide awaits the
|
|
1148
|
+
# callee-return-inference precision a later slice
|
|
1149
|
+
# brings (measured: unconditional adoption regressed
|
|
1150
|
+
# `rigor check lib` by 16 diagnostics).
|
|
1151
|
+
def adoptable_self_call_result?(type)
|
|
1152
|
+
scope.self_type.nil? || type.is_a?(Type::Bot)
|
|
1153
|
+
end
|
|
1154
|
+
|
|
1115
1155
|
def try_user_method_inference(receiver, call_node, arg_types)
|
|
1116
1156
|
return nil unless receiver.is_a?(Type::Nominal)
|
|
1117
1157
|
|
|
1118
|
-
def_node =
|
|
1158
|
+
def_node = resolve_user_def_through_ancestors(receiver.class_name, call_node.name)
|
|
1119
1159
|
return nil if def_node.nil?
|
|
1120
1160
|
|
|
1121
1161
|
infer_user_method_return(def_node, receiver, arg_types)
|
|
@@ -1123,6 +1163,81 @@ module Rigor
|
|
|
1123
1163
|
nil
|
|
1124
1164
|
end
|
|
1125
1165
|
|
|
1166
|
+
# ADR-24 slice 2 — resolves `method_name` against
|
|
1167
|
+
# `class_name`'s own `def`s, then walks the user-class
|
|
1168
|
+
# ancestor chain: included / prepended modules (transitive)
|
|
1169
|
+
# and the superclass chain. RBS-known ancestors are NOT
|
|
1170
|
+
# walked here — the `MethodDispatcher` RBS tier runs before
|
|
1171
|
+
# `try_user_method_inference` and already covers them; an
|
|
1172
|
+
# ancestor name that resolves to no project-discovered
|
|
1173
|
+
# class/module ends that branch. Cross-file: the chain is
|
|
1174
|
+
# followed through `Scope#discovered_superclasses` /
|
|
1175
|
+
# `#discovered_includes` / `#discovered_def_nodes`, which
|
|
1176
|
+
# the runner seeds from the project-wide pre-pass. The walk
|
|
1177
|
+
# is breadth-first, cycle-guarded, and node-count-capped.
|
|
1178
|
+
ANCESTOR_WALK_LIMIT = 100
|
|
1179
|
+
private_constant :ANCESTOR_WALK_LIMIT
|
|
1180
|
+
|
|
1181
|
+
def resolve_user_def_through_ancestors(class_name, method_name)
|
|
1182
|
+
queue = [class_name.to_s]
|
|
1183
|
+
seen = {}
|
|
1184
|
+
visited = 0
|
|
1185
|
+
until queue.empty?
|
|
1186
|
+
current = queue.shift
|
|
1187
|
+
next if current.nil? || seen[current]
|
|
1188
|
+
|
|
1189
|
+
seen[current] = true
|
|
1190
|
+
visited += 1
|
|
1191
|
+
return nil if visited > ANCESTOR_WALK_LIMIT
|
|
1192
|
+
|
|
1193
|
+
found = scope.user_def_for(current, method_name)
|
|
1194
|
+
return found if found
|
|
1195
|
+
|
|
1196
|
+
enqueue_ancestors(current, queue)
|
|
1197
|
+
end
|
|
1198
|
+
nil
|
|
1199
|
+
end
|
|
1200
|
+
|
|
1201
|
+
# Pushes `current`'s direct ancestors onto the BFS queue:
|
|
1202
|
+
# included / prepended modules first (Ruby places mixins
|
|
1203
|
+
# nearer than the superclass), then the superclass. Each
|
|
1204
|
+
# as-written name is resolved against `current`'s lexical
|
|
1205
|
+
# nesting; names that resolve to no project class/module
|
|
1206
|
+
# are dropped (RBS-known / third-party ancestors).
|
|
1207
|
+
def enqueue_ancestors(current, queue)
|
|
1208
|
+
scope.includes_of(current).each do |raw|
|
|
1209
|
+
resolved = resolve_ancestor_class_name(current, raw)
|
|
1210
|
+
queue.push(resolved) if resolved
|
|
1211
|
+
end
|
|
1212
|
+
raw_super = scope.superclass_of(current)
|
|
1213
|
+
return if raw_super.nil?
|
|
1214
|
+
|
|
1215
|
+
resolved_super = resolve_ancestor_class_name(current, raw_super)
|
|
1216
|
+
queue.push(resolved_super) if resolved_super
|
|
1217
|
+
end
|
|
1218
|
+
|
|
1219
|
+
# Resolves a superclass name AS WRITTEN (`"Base"`, or a
|
|
1220
|
+
# qualified `"A::B"`) to a project-discovered class,
|
|
1221
|
+
# following Ruby's `Module.nesting` constant lookup: try
|
|
1222
|
+
# the raw name under each enclosing namespace of the
|
|
1223
|
+
# subclass, innermost first, then bare. Returns nil when
|
|
1224
|
+
# no candidate names a discovered user class (e.g. the
|
|
1225
|
+
# superclass is an RBS-known or third-party class).
|
|
1226
|
+
def resolve_ancestor_class_name(subclass_qualified, raw_superclass)
|
|
1227
|
+
segments = subclass_qualified.split("::")
|
|
1228
|
+
(segments.length - 1).downto(0) do |i|
|
|
1229
|
+
candidate = (segments[0, i] + [raw_superclass]).join("::")
|
|
1230
|
+
return candidate if known_user_class?(candidate)
|
|
1231
|
+
end
|
|
1232
|
+
nil
|
|
1233
|
+
end
|
|
1234
|
+
|
|
1235
|
+
def known_user_class?(name)
|
|
1236
|
+
scope.discovered_superclasses.key?(name) ||
|
|
1237
|
+
scope.discovered_def_nodes.key?(name) ||
|
|
1238
|
+
scope.discovered_includes.key?(name)
|
|
1239
|
+
end
|
|
1240
|
+
|
|
1126
1241
|
INFERENCE_GUARD_KEY = :__rigor_user_method_inference_stack__
|
|
1127
1242
|
private_constant :INFERENCE_GUARD_KEY
|
|
1128
1243
|
|
|
@@ -1132,11 +1247,20 @@ module Rigor
|
|
|
1132
1247
|
body_scope = build_user_method_body_scope(def_node, receiver, arg_types)
|
|
1133
1248
|
return nil if body_scope.nil?
|
|
1134
1249
|
|
|
1135
|
-
# Recursion-guard signature.
|
|
1136
|
-
#
|
|
1137
|
-
#
|
|
1138
|
-
#
|
|
1139
|
-
|
|
1250
|
+
# Recursion-guard signature. Keyed on `(receiver,
|
|
1251
|
+
# method)` only — NOT the argument types. ADR-24 WD5:
|
|
1252
|
+
# a method whose summary is still being computed
|
|
1253
|
+
# resolves to `Dynamic[top]` for that cycle. Keying on
|
|
1254
|
+
# arg types would let mutual recursion through a
|
|
1255
|
+
# `module_function` module (`Acceptance#accepts` →
|
|
1256
|
+
# `accepts_one` → `accepts_dynamic` → `accepts`)
|
|
1257
|
+
# recurse unboundedly whenever the carried argument
|
|
1258
|
+
# types differ at each level — observed as a
|
|
1259
|
+
# `SystemStackError` once implicit-self calls began
|
|
1260
|
+
# resolving during the main walk. `describe(:short)`
|
|
1261
|
+
# keeps non-Nominal receivers (the implicit `Object`
|
|
1262
|
+
# carrier for top-level / DSL-block defs) printable.
|
|
1263
|
+
signature = [receiver.describe(:short), def_node.name]
|
|
1140
1264
|
stack = (Thread.current[INFERENCE_GUARD_KEY] ||= [])
|
|
1141
1265
|
return Type::Combinator.untyped if stack.include?(signature)
|
|
1142
1266
|
|
|
@@ -1174,6 +1298,8 @@ module Rigor
|
|
|
1174
1298
|
.with_program_globals(scope.program_globals)
|
|
1175
1299
|
.with_discovered_methods(scope.discovered_methods)
|
|
1176
1300
|
.with_discovered_def_nodes(scope.discovered_def_nodes)
|
|
1301
|
+
.with_discovered_superclasses(scope.discovered_superclasses)
|
|
1302
|
+
.with_discovered_includes(scope.discovered_includes)
|
|
1177
1303
|
.with_self_type(receiver)
|
|
1178
1304
|
|
|
1179
1305
|
required.each_with_index do |param, index|
|
|
@@ -1332,10 +1458,17 @@ module Rigor
|
|
|
1332
1458
|
# the dispatch chain untouched.
|
|
1333
1459
|
PER_ELEMENT_TUPLE_METHODS = Set[
|
|
1334
1460
|
:map, :collect, :filter_map, :flat_map,
|
|
1461
|
+
:select, :filter, :reject,
|
|
1335
1462
|
:find, :detect, :find_index, :index
|
|
1336
1463
|
].freeze
|
|
1337
1464
|
private_constant :PER_ELEMENT_TUPLE_METHODS
|
|
1338
1465
|
|
|
1466
|
+
HASH_SHAPE_TRANSFORM_METHODS = Set[
|
|
1467
|
+
:transform_keys, :transform_keys!,
|
|
1468
|
+
:transform_values, :transform_values!
|
|
1469
|
+
].freeze
|
|
1470
|
+
private_constant :HASH_SHAPE_TRANSFORM_METHODS
|
|
1471
|
+
|
|
1339
1472
|
# Cardinality cap for per-element block fold over
|
|
1340
1473
|
# finite-bound `Constant<Range>` receivers. Walking
|
|
1341
1474
|
# `(1..1_000_000).map { … }` element-wise would balloon
|
|
@@ -1352,15 +1485,50 @@ module Rigor
|
|
|
1352
1485
|
element_types = per_element_elements_of(receiver_type)
|
|
1353
1486
|
return nil if element_types.nil? || element_types.empty?
|
|
1354
1487
|
|
|
1355
|
-
|
|
1356
|
-
return nil
|
|
1488
|
+
per_position = per_element_block_results(call_node.block, element_types)
|
|
1489
|
+
return nil if per_position.nil? || per_position.any?(&:nil?)
|
|
1357
1490
|
|
|
1358
|
-
per_position
|
|
1359
|
-
|
|
1491
|
+
assemble_per_element_result(call_node.name, per_position, element_types)
|
|
1492
|
+
end
|
|
1493
|
+
|
|
1494
|
+
# Evaluates the call's block once per receiver element.
|
|
1495
|
+
# Two block shapes are supported:
|
|
1496
|
+
#
|
|
1497
|
+
# - `Prism::BlockNode` — a full `do … end` / `{ … }` block;
|
|
1498
|
+
# the body is re-typed per position with the element
|
|
1499
|
+
# bound to the block parameter.
|
|
1500
|
+
# - `Prism::BlockArgumentNode` wrapping a `SymbolNode` —
|
|
1501
|
+
# the `&:predicate` shorthand; the symbol is dispatched
|
|
1502
|
+
# as a zero-arg method on each element type.
|
|
1503
|
+
#
|
|
1504
|
+
# Any other shape (`&proc_local`, `&method(:foo)`, no
|
|
1505
|
+
# block) returns `nil` so the fold declines.
|
|
1506
|
+
def per_element_block_results(block, element_types)
|
|
1507
|
+
case block
|
|
1508
|
+
when Prism::BlockNode
|
|
1509
|
+
element_types.map { |element_type| type_block_body_with_param(block, [element_type]) }
|
|
1510
|
+
when Prism::BlockArgumentNode
|
|
1511
|
+
per_element_symbol_results(block, element_types)
|
|
1360
1512
|
end
|
|
1361
|
-
|
|
1513
|
+
end
|
|
1362
1514
|
|
|
1363
|
-
|
|
1515
|
+
def per_element_symbol_results(block_arg, element_types)
|
|
1516
|
+
expression = block_arg.expression
|
|
1517
|
+
return nil unless expression.is_a?(Prism::SymbolNode)
|
|
1518
|
+
|
|
1519
|
+
method_name = expression.unescaped.to_sym
|
|
1520
|
+
element_types.map do |element_type|
|
|
1521
|
+
MethodDispatcher.dispatch(
|
|
1522
|
+
receiver_type: element_type,
|
|
1523
|
+
method_name: method_name,
|
|
1524
|
+
arg_types: [],
|
|
1525
|
+
block_type: nil,
|
|
1526
|
+
environment: scope.environment,
|
|
1527
|
+
scope: scope
|
|
1528
|
+
)
|
|
1529
|
+
end
|
|
1530
|
+
rescue StandardError
|
|
1531
|
+
nil
|
|
1364
1532
|
end
|
|
1365
1533
|
|
|
1366
1534
|
# Returns the per-position element types for a finite,
|
|
@@ -1409,11 +1577,37 @@ module Rigor
|
|
|
1409
1577
|
when :map, :collect then Type::Combinator.tuple_of(*per_position)
|
|
1410
1578
|
when :filter_map then assemble_filter_map_result(per_position)
|
|
1411
1579
|
when :flat_map then assemble_flat_map_result(per_position)
|
|
1580
|
+
when :select, :filter
|
|
1581
|
+
assemble_filter_result(per_position, element_types, keep_on_truthy: true)
|
|
1582
|
+
when :reject
|
|
1583
|
+
assemble_filter_result(per_position, element_types, keep_on_truthy: false)
|
|
1412
1584
|
when :find, :detect then assemble_find_result(per_position, element_types)
|
|
1413
1585
|
when :find_index, :index then assemble_find_index_result(per_position)
|
|
1414
1586
|
end
|
|
1415
1587
|
end
|
|
1416
1588
|
|
|
1589
|
+
# `select` / `filter` / `reject`: keeps each receiver
|
|
1590
|
+
# element whose per-position predicate result folds to a
|
|
1591
|
+
# decisive `Constant` — Ruby-truthy for `select` / `filter`,
|
|
1592
|
+
# Ruby-falsey for `reject`. The surviving elements assemble
|
|
1593
|
+
# into a `Tuple`, strictly tighter than the RBS-projected
|
|
1594
|
+
# `Array[Elem]`.
|
|
1595
|
+
#
|
|
1596
|
+
# Folds tightly only when EVERY position is a `Constant`:
|
|
1597
|
+
# a single non-`Constant` position leaves the result
|
|
1598
|
+
# cardinality unknown (the element might or might not
|
|
1599
|
+
# survive), so the dispatcher declines and the RBS tier
|
|
1600
|
+
# widens to `Array[Elem]`. `[].select` style empty results
|
|
1601
|
+
# are sound — an empty `Tuple` is the empty-array carrier.
|
|
1602
|
+
def assemble_filter_result(per_position, element_types, keep_on_truthy:)
|
|
1603
|
+
return nil unless per_position.all?(Type::Constant)
|
|
1604
|
+
|
|
1605
|
+
kept = element_types.each_index.filter_map do |index|
|
|
1606
|
+
element_types[index] if truthy_constant?(per_position[index]) == keep_on_truthy
|
|
1607
|
+
end
|
|
1608
|
+
Type::Combinator.tuple_of(*kept)
|
|
1609
|
+
end
|
|
1610
|
+
|
|
1417
1611
|
# `filter_map` folds tightly only when every per-position
|
|
1418
1612
|
# result is a `Constant`: positions whose value is `nil`
|
|
1419
1613
|
# or `false` drop, the rest survive in declaration order.
|
|
@@ -1488,6 +1682,94 @@ module Rigor
|
|
|
1488
1682
|
type.is_a?(Type::Constant) && type.value && type.value != false
|
|
1489
1683
|
end
|
|
1490
1684
|
|
|
1685
|
+
# Per-pair block fold for `HashShape#transform_keys` and
|
|
1686
|
+
# `HashShape#transform_values` (and their bang variants).
|
|
1687
|
+
#
|
|
1688
|
+
# When the receiver is a closed `HashShape` with no optional
|
|
1689
|
+
# keys, applies the call's block (a `Prism::BlockNode` or
|
|
1690
|
+
# `Prism::BlockArgumentNode`) to each key/value pair
|
|
1691
|
+
# independently and assembles a new `HashShape`:
|
|
1692
|
+
#
|
|
1693
|
+
# - `transform_values` / `transform_values!`: re-types
|
|
1694
|
+
# each VALUE by binding it to the block parameter; keys
|
|
1695
|
+
# are preserved unchanged.
|
|
1696
|
+
# - `transform_keys` / `transform_keys!`: re-types each
|
|
1697
|
+
# KEY by wrapping it in `Constant[k]` and passing it to
|
|
1698
|
+
# the block; values are preserved unchanged. The result
|
|
1699
|
+
# key must be a `Constant[Symbol | String]` — otherwise
|
|
1700
|
+
# the tier declines (the new key cannot be used as a
|
|
1701
|
+
# static HashShape index). Collisions (two old keys
|
|
1702
|
+
# mapping to the same new key) also decline.
|
|
1703
|
+
#
|
|
1704
|
+
# Returns `nil` on any decline so the dispatcher falls
|
|
1705
|
+
# through to `RbsDispatch` and gets the widened `Hash[K, V]`
|
|
1706
|
+
# answer.
|
|
1707
|
+
def try_hash_shape_block_fold(call_node, receiver_type)
|
|
1708
|
+
return nil unless HASH_SHAPE_TRANSFORM_METHODS.include?(call_node.name)
|
|
1709
|
+
return nil unless receiver_type.is_a?(Type::HashShape)
|
|
1710
|
+
return nil unless receiver_type.closed?
|
|
1711
|
+
return nil unless receiver_type.optional_keys.empty?
|
|
1712
|
+
|
|
1713
|
+
block_arg = call_node.block
|
|
1714
|
+
return nil if block_arg.nil?
|
|
1715
|
+
|
|
1716
|
+
if %i[transform_values transform_values!].include?(call_node.name)
|
|
1717
|
+
fold_hash_shape_transform_values(receiver_type, block_arg)
|
|
1718
|
+
else
|
|
1719
|
+
fold_hash_shape_transform_keys(receiver_type, block_arg)
|
|
1720
|
+
end
|
|
1721
|
+
end
|
|
1722
|
+
|
|
1723
|
+
def fold_hash_shape_transform_values(shape, block_arg)
|
|
1724
|
+
new_pairs = {}
|
|
1725
|
+
shape.pairs.each do |key, value|
|
|
1726
|
+
new_value = apply_hash_block(block_arg, value)
|
|
1727
|
+
return nil if new_value.nil?
|
|
1728
|
+
|
|
1729
|
+
new_pairs[key] = new_value
|
|
1730
|
+
end
|
|
1731
|
+
Type::Combinator.hash_shape_of(new_pairs)
|
|
1732
|
+
end
|
|
1733
|
+
|
|
1734
|
+
def fold_hash_shape_transform_keys(shape, block_arg)
|
|
1735
|
+
new_pairs = {}
|
|
1736
|
+
shape.pairs.each do |key, value|
|
|
1737
|
+
key_type = Type::Combinator.constant_of(key)
|
|
1738
|
+
new_key_type = apply_hash_block(block_arg, key_type)
|
|
1739
|
+
return nil unless new_key_type.is_a?(Type::Constant)
|
|
1740
|
+
|
|
1741
|
+
new_key = new_key_type.value
|
|
1742
|
+
return nil unless new_key.is_a?(Symbol) || new_key.is_a?(String)
|
|
1743
|
+
return nil if new_pairs.key?(new_key)
|
|
1744
|
+
|
|
1745
|
+
new_pairs[new_key] = value
|
|
1746
|
+
end
|
|
1747
|
+
Type::Combinator.hash_shape_of(new_pairs)
|
|
1748
|
+
end
|
|
1749
|
+
|
|
1750
|
+
# Applies a single-argument block (either a full BlockNode
|
|
1751
|
+
# or a `&:symbol` BlockArgumentNode) to `param_type` and
|
|
1752
|
+
# returns the resulting type, or `nil` on failure.
|
|
1753
|
+
def apply_hash_block(block_arg, param_type)
|
|
1754
|
+
case block_arg
|
|
1755
|
+
when Prism::BlockNode
|
|
1756
|
+
type_block_body_with_param(block_arg, [param_type])
|
|
1757
|
+
when Prism::BlockArgumentNode
|
|
1758
|
+
expression = block_arg.expression
|
|
1759
|
+
return nil unless expression.is_a?(Prism::SymbolNode)
|
|
1760
|
+
|
|
1761
|
+
MethodDispatcher.dispatch(
|
|
1762
|
+
receiver_type: param_type,
|
|
1763
|
+
method_name: expression.unescaped.to_sym,
|
|
1764
|
+
arg_types: [],
|
|
1765
|
+
block_type: nil,
|
|
1766
|
+
environment: scope.environment,
|
|
1767
|
+
call_node: block_arg,
|
|
1768
|
+
scope: scope
|
|
1769
|
+
)
|
|
1770
|
+
end
|
|
1771
|
+
end
|
|
1772
|
+
|
|
1491
1773
|
def type_block_body_with_param(block_node, expected_param_types)
|
|
1492
1774
|
bindings = BlockParameterBinder.new(expected_param_types: expected_param_types).bind(block_node)
|
|
1493
1775
|
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
|