rigortype 0.1.16 → 0.1.17
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/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +100 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +209 -0
- data/lib/rigor/analysis/check_rules.rb +149 -70
- data/lib/rigor/analysis/dependency_recorder.rb +122 -0
- data/lib/rigor/analysis/diagnostic.rb +18 -0
- data/lib/rigor/analysis/incremental.rb +162 -0
- data/lib/rigor/analysis/incremental_session.rb +337 -0
- data/lib/rigor/analysis/rule_catalog.rb +48 -0
- data/lib/rigor/analysis/runner.rb +434 -37
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +121 -0
- data/lib/rigor/builtins/static_return_refinements.rb +7 -1
- data/lib/rigor/cache/descriptor.rb +50 -49
- data/lib/rigor/cache/incremental_snapshot.rb +147 -0
- data/lib/rigor/cache/rbs_cache_producer.rb +30 -0
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +2 -8
- data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -8
- data/lib/rigor/cache/rbs_constant_table.rb +2 -8
- data/lib/rigor/cache/rbs_environment.rb +2 -8
- data/lib/rigor/cache/rbs_instance_definitions.rb +3 -16
- data/lib/rigor/cache/rbs_known_class_names.rb +2 -8
- data/lib/rigor/cache/store.rb +99 -1
- data/lib/rigor/cli/annotate_command.rb +2 -7
- data/lib/rigor/cli/baseline_command.rb +2 -7
- data/lib/rigor/cli/command.rb +47 -0
- data/lib/rigor/cli/coverage_command.rb +3 -23
- data/lib/rigor/cli/coverage_renderer.rb +3 -8
- data/lib/rigor/cli/diff_command.rb +3 -7
- data/lib/rigor/cli/explain_command.rb +2 -7
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mcp_command.rb +3 -7
- data/lib/rigor/cli/options.rb +57 -0
- data/lib/rigor/cli/plugin_command.rb +3 -7
- data/lib/rigor/cli/plugins_command.rb +2 -7
- data/lib/rigor/cli/renderable.rb +26 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -7
- data/lib/rigor/cli/skill_command.rb +3 -7
- data/lib/rigor/cli/triage_command.rb +2 -7
- data/lib/rigor/cli/type_of_command.rb +5 -38
- data/lib/rigor/cli/type_of_renderer.rb +4 -9
- data/lib/rigor/cli/type_scan_command.rb +3 -23
- data/lib/rigor/cli/type_scan_renderer.rb +4 -9
- data/lib/rigor/cli.rb +125 -43
- data/lib/rigor/configuration/dependencies.rb +18 -1
- data/lib/rigor/configuration/severity_profile.rb +22 -3
- data/lib/rigor/configuration.rb +13 -3
- data/lib/rigor/environment/rbs_loader.rb +76 -3
- data/lib/rigor/inference/block_parameter_binder.rb +1 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/complex_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/date_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/encoding_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/exception_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/hash_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/method_catalog.rb +15 -0
- data/lib/rigor/inference/builtins/numeric_catalog.rb +21 -93
- data/lib/rigor/inference/builtins/pathname_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/proc_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/random_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/range_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/rational_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/re_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/set_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/string_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/struct_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/time_catalog.rb +2 -5
- data/lib/rigor/inference/expression_typer.rb +140 -20
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +5 -1
- data/lib/rigor/inference/method_dispatcher/call_context.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +11 -10
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +12 -6
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +246 -0
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -1
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +4 -1
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +12 -7
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +23 -13
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +9 -9
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +120 -9
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +12 -12
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +49 -0
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -9
- data/lib/rigor/inference/method_dispatcher.rb +99 -59
- data/lib/rigor/inference/narrowing.rb +202 -5
- data/lib/rigor/inference/scope_indexer.rb +134 -7
- data/lib/rigor/inference/statement_evaluator.rb +105 -26
- data/lib/rigor/language_server/buffer_resolution.rb +33 -0
- data/lib/rigor/language_server/completion_provider.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +4 -4
- data/lib/rigor/language_server/folding_range_provider.rb +4 -4
- data/lib/rigor/language_server/hover_provider.rb +4 -4
- data/lib/rigor/language_server/selection_range_provider.rb +4 -4
- data/lib/rigor/language_server/signature_help_provider.rb +4 -4
- data/lib/rigor/plugin/base.rb +20 -4
- data/lib/rigor/plugin/registry.rb +39 -1
- data/lib/rigor/rbs_extended/conformance_checker.rb +208 -0
- data/lib/rigor/rbs_extended.rb +39 -0
- data/lib/rigor/scope.rb +123 -9
- data/lib/rigor/type/acceptance_router.rb +19 -0
- data/lib/rigor/type/accepts_result.rb +3 -10
- data/lib/rigor/type/app.rb +3 -7
- data/lib/rigor/type/bot.rb +2 -3
- data/lib/rigor/type/bound_method.rb +5 -12
- data/lib/rigor/type/combinator.rb +17 -0
- data/lib/rigor/type/constant.rb +2 -3
- data/lib/rigor/type/data_class.rb +80 -0
- data/lib/rigor/type/data_instance.rb +100 -0
- data/lib/rigor/type/difference.rb +5 -10
- data/lib/rigor/type/dynamic.rb +5 -10
- data/lib/rigor/type/hash_shape.rb +5 -15
- data/lib/rigor/type/integer_range.rb +5 -10
- data/lib/rigor/type/intersection.rb +5 -10
- data/lib/rigor/type/nominal.rb +5 -10
- data/lib/rigor/type/refined.rb +5 -10
- data/lib/rigor/type/singleton.rb +5 -10
- data/lib/rigor/type/top.rb +2 -3
- data/lib/rigor/type/tuple.rb +5 -10
- data/lib/rigor/type/union.rb +5 -10
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/value_semantics.rb +77 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +12 -2
- data/sig/rigor/cache.rbs +19 -0
- data/sig/rigor/inference.rbs +22 -0
- data/sig/rigor/rbs_extended.rbs +2 -0
- data/sig/rigor/scope.rbs +5 -0
- data/sig/rigor/type.rbs +58 -1
- data/sig/rigor.rbs +6 -1
- metadata +22 -1
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../type"
|
|
6
6
|
require_relative "../ast"
|
|
7
|
+
require_relative "../analysis/self_call_resolution_recorder"
|
|
7
8
|
require_relative "block_parameter_binder"
|
|
8
9
|
require_relative "budget_trace"
|
|
9
10
|
require_relative "fallback"
|
|
@@ -111,7 +112,7 @@ module Rigor
|
|
|
111
112
|
Prism::GlobalVariableOperatorWriteNode => :type_of_assignment_write,
|
|
112
113
|
Prism::GlobalVariableOrWriteNode => :type_of_assignment_write,
|
|
113
114
|
Prism::GlobalVariableAndWriteNode => :type_of_assignment_write,
|
|
114
|
-
# Compound writes that share the
|
|
115
|
+
# Compound writes that share the `.value` rvalue accessor
|
|
115
116
|
Prism::LocalVariableOperatorWriteNode => :type_of_assignment_write,
|
|
116
117
|
Prism::LocalVariableOrWriteNode => :type_of_assignment_write,
|
|
117
118
|
Prism::LocalVariableAndWriteNode => :type_of_assignment_write,
|
|
@@ -443,8 +444,12 @@ module Rigor
|
|
|
443
444
|
candidates = []
|
|
444
445
|
while prefix && !prefix.empty?
|
|
445
446
|
candidates << "#{prefix}::#{name}"
|
|
446
|
-
|
|
447
|
-
|
|
447
|
+
# Strip the last `::` segment without `rpartition`'s throwaway
|
|
448
|
+
# 3-element array + extra substrings (this loop is the sole
|
|
449
|
+
# caller of the `String#rpartition` allocation seen in the
|
|
450
|
+
# profile): `rindex` + slice gives the same prefix, or nil.
|
|
451
|
+
idx = prefix.rindex("::")
|
|
452
|
+
prefix = idx ? prefix[0, idx] : nil
|
|
448
453
|
end
|
|
449
454
|
candidates << name
|
|
450
455
|
candidates
|
|
@@ -1260,9 +1265,64 @@ module Rigor
|
|
|
1260
1265
|
# MUST NOT record a tracer event.
|
|
1261
1266
|
return dynamic_top if receiver.is_a?(Type::Dynamic)
|
|
1262
1267
|
|
|
1268
|
+
# ADR-24 slice 4a — this is the engine choke-point where an
|
|
1269
|
+
# implicit-self call has exhausted every resolution tier (RBS
|
|
1270
|
+
# dispatch + user-class ancestor walk) and falls through to
|
|
1271
|
+
# `Dynamic[top]`. When the slice-4 recorder is active, capture the
|
|
1272
|
+
# miss so a later slice's closed-class gate can flag it. Off by
|
|
1273
|
+
# default: `active?` is a plain integer read.
|
|
1274
|
+
record_unresolved_self_call(node, receiver) if Analysis::SelfCallResolutionRecorder.active?
|
|
1275
|
+
|
|
1263
1276
|
fallback_for(node, family: :prism)
|
|
1264
1277
|
end
|
|
1265
1278
|
|
|
1279
|
+
# ADR-24 slice 4a — records an unresolved *implicit-self* call (no
|
|
1280
|
+
# explicit receiver) whose `self` types to a concrete user class.
|
|
1281
|
+
# Explicit-receiver misses are out of scope (the existing
|
|
1282
|
+
# `call.undefined-method` rule already owns receiver-typed dispatch);
|
|
1283
|
+
# a non-`Nominal` self (top-level / DSL-block `self`, or a `Dynamic`
|
|
1284
|
+
# self) is skipped so the gradual guarantee is never touched here.
|
|
1285
|
+
def record_unresolved_self_call(node, receiver)
|
|
1286
|
+
return unless node.receiver.nil?
|
|
1287
|
+
return unless receiver.is_a?(Type::Nominal)
|
|
1288
|
+
return if self_call_method_known?(receiver.class_name, node.name)
|
|
1289
|
+
|
|
1290
|
+
location = node.message_loc || node.location
|
|
1291
|
+
Analysis::SelfCallResolutionRecorder.record(
|
|
1292
|
+
class_name: receiver.class_name,
|
|
1293
|
+
method_name: node.name,
|
|
1294
|
+
node: node,
|
|
1295
|
+
path: scope.source_path,
|
|
1296
|
+
line: location&.start_line,
|
|
1297
|
+
column: location ? location.start_column + 1 : nil
|
|
1298
|
+
)
|
|
1299
|
+
end
|
|
1300
|
+
|
|
1301
|
+
# The recorder must capture *existence* misses, not type misses.
|
|
1302
|
+
# Reaching the choke-point means RBS dispatch produced no result, but
|
|
1303
|
+
# a project method can still EXIST without an inferable return type —
|
|
1304
|
+
# a `module_function` sibling whose body the engine can't fully type,
|
|
1305
|
+
# an `attr_reader` / `define_method` / `Data.define` member. Recording
|
|
1306
|
+
# those would reproduce the 135 false positives of slice-4 attempt 1.
|
|
1307
|
+
# So skip any name the engine's own existence signals already know:
|
|
1308
|
+
# a `def` resolvable through the ancestor walk, or an own-class entry
|
|
1309
|
+
# in the discovered-methods table (`def` / `attr_*` / `define_method`
|
|
1310
|
+
# / alias). This reuses the engine's real resolution — the
|
|
1311
|
+
# "collect, don't recompute" lesson — so only a name that exists
|
|
1312
|
+
# nowhere a project signal can see reaches the recorder.
|
|
1313
|
+
# `module_function` records its defs as `:singleton` (an implicit-self
|
|
1314
|
+
# call inside such a method dispatches to the module's singleton
|
|
1315
|
+
# method), while ordinary instance methods record `:instance`. The
|
|
1316
|
+
# recorder cannot tell the two contexts apart from the call node, so
|
|
1317
|
+
# existence under EITHER kind suppresses recording — the FP-safe
|
|
1318
|
+
# choice, since either means the method genuinely exists.
|
|
1319
|
+
def self_call_method_known?(class_name, method_name)
|
|
1320
|
+
return true if resolve_user_def_through_ancestors(class_name, method_name)
|
|
1321
|
+
|
|
1322
|
+
scope.discovered_method?(class_name, method_name, :instance) ||
|
|
1323
|
+
scope.discovered_method?(class_name, method_name, :singleton)
|
|
1324
|
+
end
|
|
1325
|
+
|
|
1266
1326
|
# v0.0.2 #5 — re-types the body of a user-defined
|
|
1267
1327
|
# instance method with the call site's argument types
|
|
1268
1328
|
# bound to the method's parameters. Used as a
|
|
@@ -1349,7 +1409,44 @@ module Rigor
|
|
|
1349
1409
|
ANCESTOR_WALK_LIMIT = 100
|
|
1350
1410
|
private_constant :ANCESTOR_WALK_LIMIT
|
|
1351
1411
|
|
|
1412
|
+
CLASS_GRAPH_CACHE_KEY = :__rigor_class_graph_cache__
|
|
1413
|
+
private_constant :CLASS_GRAPH_CACHE_KEY
|
|
1414
|
+
|
|
1415
|
+
# Run-scoped memo for the static class-graph resolvers below. They
|
|
1416
|
+
# are pure functions of the *frozen* project index trio
|
|
1417
|
+
# (`discovered_def_nodes` / `discovered_superclasses` /
|
|
1418
|
+
# `discovered_includes`) — `user_def_for` / `superclass_of` /
|
|
1419
|
+
# `includes_of` read nothing else, and never touch the current
|
|
1420
|
+
# scope's locals or narrowings — so a result computed for one
|
|
1421
|
+
# `(class, method)` is valid for every `Scope` that shares those
|
|
1422
|
+
# tables. `ExpressionTyper` is rebuilt per `Scope#type_of`, so the
|
|
1423
|
+
# memo lives on `Thread.current` rather than on `self`. It is keyed
|
|
1424
|
+
# by the *identity* of the three frozen tables (nested
|
|
1425
|
+
# `compare_by_identity` stores): a new analysis generation, or any
|
|
1426
|
+
# `Scope` that swaps an index via `with_discovered_*`, transparently
|
|
1427
|
+
# lands in a fresh bucket while everything sharing the tables shares
|
|
1428
|
+
# the memo. Steady-state cost is three identity-keyed hash reads and
|
|
1429
|
+
# zero allocation — the `||=` chains only allocate on the first miss
|
|
1430
|
+
# of a generation. (Pool mode forks per worker, so the
|
|
1431
|
+
# `Thread.current` store is process-local and never crosses a
|
|
1432
|
+
# project boundary.)
|
|
1433
|
+
def class_graph_buckets
|
|
1434
|
+
store = (Thread.current[CLASS_GRAPH_CACHE_KEY] ||= {}.compare_by_identity)
|
|
1435
|
+
by_def = (store[scope.discovered_def_nodes] ||= {}.compare_by_identity)
|
|
1436
|
+
by_super = (by_def[scope.discovered_superclasses] ||= {}.compare_by_identity)
|
|
1437
|
+
by_super[scope.discovered_includes] ||= { name: {}, user_def: {} }
|
|
1438
|
+
end
|
|
1439
|
+
|
|
1352
1440
|
def resolve_user_def_through_ancestors(class_name, method_name)
|
|
1441
|
+
cache = class_graph_buckets[:user_def]
|
|
1442
|
+
table = (cache[class_name.to_s] ||= {})
|
|
1443
|
+
key = method_name.to_sym
|
|
1444
|
+
return table[key] if table.key?(key)
|
|
1445
|
+
|
|
1446
|
+
table[key] = compute_user_def_through_ancestors(class_name, method_name)
|
|
1447
|
+
end
|
|
1448
|
+
|
|
1449
|
+
def compute_user_def_through_ancestors(class_name, method_name)
|
|
1353
1450
|
queue = [class_name.to_s]
|
|
1354
1451
|
seen = {}
|
|
1355
1452
|
visited = 0
|
|
@@ -1398,6 +1495,14 @@ module Rigor
|
|
|
1398
1495
|
# no candidate names a discovered user class (e.g. the
|
|
1399
1496
|
# superclass is an RBS-known or third-party class).
|
|
1400
1497
|
def resolve_ancestor_class_name(subclass_qualified, raw_superclass)
|
|
1498
|
+
by_subclass = (class_graph_buckets[:name][subclass_qualified] ||= {})
|
|
1499
|
+
return by_subclass[raw_superclass] if by_subclass.key?(raw_superclass)
|
|
1500
|
+
|
|
1501
|
+
by_subclass[raw_superclass] =
|
|
1502
|
+
compute_ancestor_class_name(subclass_qualified, raw_superclass)
|
|
1503
|
+
end
|
|
1504
|
+
|
|
1505
|
+
def compute_ancestor_class_name(subclass_qualified, raw_superclass)
|
|
1401
1506
|
segments = subclass_qualified.split("::")
|
|
1402
1507
|
(segments.length - 1).downto(0) do |i|
|
|
1403
1508
|
candidate = (segments[0, i] + [raw_superclass]).join("::")
|
|
@@ -1466,23 +1571,38 @@ module Rigor
|
|
|
1466
1571
|
return nil unless params.nil? || user_method_param_shape_simple?(params)
|
|
1467
1572
|
return nil unless required.size == arg_types.size
|
|
1468
1573
|
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1574
|
+
# Bind required positionals by index. The body scope starts from an
|
|
1575
|
+
# empty fact store and narrowing set, so `with_local`'s fact /
|
|
1576
|
+
# narrowing invalidations would be no-ops here — build the locals
|
|
1577
|
+
# table directly (matching `with_local`'s `name.to_sym` key).
|
|
1578
|
+
locals = {}
|
|
1579
|
+
required.each_with_index { |param, index| locals[param.name.to_sym] = arg_types[index] }
|
|
1580
|
+
|
|
1581
|
+
# Construct the body scope in a SINGLE allocation. The previous
|
|
1582
|
+
# `Scope.empty.with_*.with_*…` chain allocated a fresh frozen Scope
|
|
1583
|
+
# per field — ~12 throwaway Scopes to build one body scope, run per
|
|
1584
|
+
# user-method-call inference, which made these `with_*` the dominant
|
|
1585
|
+
# `Scope#rebuild` source. Each field here is a plain inherited
|
|
1586
|
+
# reference (the project-wide indexes + self_type); every unset
|
|
1587
|
+
# field defaults to the same empty binding the old chain left it at,
|
|
1588
|
+
# so the result is identical (ADR-44).
|
|
1589
|
+
Scope.new(
|
|
1590
|
+
environment: scope.environment,
|
|
1591
|
+
locals: locals.freeze,
|
|
1592
|
+
self_type: receiver,
|
|
1593
|
+
declared_types: scope.declared_types,
|
|
1594
|
+
discovered_classes: scope.discovered_classes,
|
|
1595
|
+
in_source_constants: scope.in_source_constants,
|
|
1596
|
+
class_ivars: scope.class_ivars,
|
|
1597
|
+
class_cvars: scope.class_cvars,
|
|
1598
|
+
program_globals: scope.program_globals,
|
|
1599
|
+
discovered_methods: scope.discovered_methods,
|
|
1600
|
+
discovered_def_nodes: scope.discovered_def_nodes,
|
|
1601
|
+
discovered_def_sources: scope.discovered_def_sources,
|
|
1602
|
+
discovered_superclasses: scope.discovered_superclasses,
|
|
1603
|
+
discovered_includes: scope.discovered_includes,
|
|
1604
|
+
discovered_class_sources: scope.discovered_class_sources
|
|
1605
|
+
)
|
|
1486
1606
|
end
|
|
1487
1607
|
|
|
1488
1608
|
# First iteration accepts only required positional
|
|
@@ -69,7 +69,11 @@ module Rigor
|
|
|
69
69
|
# the call's block. `nil` means "no block at the call site"
|
|
70
70
|
# and disqualifies every rule here.
|
|
71
71
|
# @return [Rigor::Type, nil]
|
|
72
|
-
def
|
|
72
|
+
def try_dispatch(context)
|
|
73
|
+
receiver = context.receiver
|
|
74
|
+
method_name = context.method_name
|
|
75
|
+
args = context.args
|
|
76
|
+
block_type = context.block_type
|
|
73
77
|
return nil if receiver.nil? || block_type.nil?
|
|
74
78
|
|
|
75
79
|
truthiness = constant_truthiness(block_type)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
module MethodDispatcher
|
|
6
|
+
# Immutable value object carrying everything a dispatch tier needs
|
|
7
|
+
# to fold a single call site. Built once per `MethodDispatcher.dispatch`
|
|
8
|
+
# and threaded — unchanged — through every tier, replacing the
|
|
9
|
+
# `(receiver:, method_name:, args:, …)` keyword quartet that each
|
|
10
|
+
# tier used to redeclare (several with `# rubocop:disable
|
|
11
|
+
# Metrics/ParameterLists`).
|
|
12
|
+
#
|
|
13
|
+
# Every tier satisfies one interface — `try_dispatch(CallContext) ->
|
|
14
|
+
# Rigor::Type?` (the `_DispatchTier` RBS interface). Pure tiers
|
|
15
|
+
# (the singleton folders, ConstantFolding, ShapeDispatch, …) read
|
|
16
|
+
# only `receiver` / `method_name` / `args` and ignore the rest;
|
|
17
|
+
# the RBS / backward / block-param tiers consult the wider context.
|
|
18
|
+
#
|
|
19
|
+
# Derived call sites (the user-class fallback's `public_only` RBS
|
|
20
|
+
# retry, the Tier-B origin-module redispatch) use `Data#with` to
|
|
21
|
+
# copy the base context with the few fields that differ rather than
|
|
22
|
+
# rebuilding the quartet by hand.
|
|
23
|
+
#
|
|
24
|
+
# Fields:
|
|
25
|
+
# - `receiver` — the receiver `Rigor::Type` (nil short-circuits)
|
|
26
|
+
# - `method_name` — the called selector (Symbol)
|
|
27
|
+
# - `args` — positional argument `Rigor::Type`s
|
|
28
|
+
# - `block_type` — the block's `Rigor::Type`, or nil
|
|
29
|
+
# - `environment` — the analysis `Environment` (RBS loader, …)
|
|
30
|
+
# - `call_node` — the Prism call node, when available
|
|
31
|
+
# - `scope` — the enclosing `Scope` (discovered methods, …)
|
|
32
|
+
# - `self_type_override` — receiver to attribute private dispatch to
|
|
33
|
+
# - `public_only` — suppress private-method resolution (explicit, non-self receiver)
|
|
34
|
+
CallContext = Data.define(
|
|
35
|
+
:receiver, :method_name, :args,
|
|
36
|
+
:block_type, :environment, :call_node, :scope,
|
|
37
|
+
:self_type_override, :public_only
|
|
38
|
+
) do
|
|
39
|
+
# Keyword factory with nil/false defaults for the optional
|
|
40
|
+
# context fields, so a caller that only has the call quartet
|
|
41
|
+
# (the common precise-tier path) need not spell out the rest.
|
|
42
|
+
#
|
|
43
|
+
# This is the single place the call-context field list is
|
|
44
|
+
# enumerated — the whole point of the value object is to absorb
|
|
45
|
+
# the wide keyword list the tiers used to each redeclare. The
|
|
46
|
+
# ParameterLists disable here retires the per-tier disables (the
|
|
47
|
+
# `RbsDispatch` quartet-plus signatures) rather than adding to
|
|
48
|
+
# them.
|
|
49
|
+
def self.build(receiver:, method_name:, args:, # rubocop:disable Metrics/ParameterLists
|
|
50
|
+
block_type: nil, environment: nil, call_node: nil,
|
|
51
|
+
scope: nil, self_type_override: nil, public_only: false)
|
|
52
|
+
# Positional `new` (field-definition order) avoids the keyword
|
|
53
|
+
# hash a `new(receiver:, …)` call allocates — this runs once per
|
|
54
|
+
# dispatch and was a top allocation site. Order MUST track the
|
|
55
|
+
# `Data.define` field list above.
|
|
56
|
+
new(
|
|
57
|
+
receiver, method_name, args,
|
|
58
|
+
block_type, environment, call_node, scope,
|
|
59
|
+
self_type_override, public_only
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "cgi/util"
|
|
4
4
|
require_relative "../../type"
|
|
5
|
+
require_relative "singleton_folding"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
7
8
|
module Inference
|
|
@@ -63,22 +64,21 @@ module Rigor
|
|
|
63
64
|
module_function
|
|
64
65
|
|
|
65
66
|
# @return [Rigor::Type, nil] folded result, or nil to defer.
|
|
66
|
-
def try_dispatch(
|
|
67
|
-
|
|
67
|
+
def try_dispatch(context)
|
|
68
|
+
receiver = context.receiver
|
|
69
|
+
method_name = context.method_name
|
|
70
|
+
args = context.args
|
|
71
|
+
return nil unless SingletonFolding.receiver?(receiver, "CGI")
|
|
68
72
|
return nil unless CGI_ALL_ESCAPE_METHODS.include?(method_name)
|
|
69
73
|
|
|
70
74
|
fold_cgi_call(method_name, args)
|
|
71
75
|
end
|
|
72
76
|
|
|
73
|
-
def dispatch_target?(receiver)
|
|
74
|
-
receiver.is_a?(Type::Singleton) && receiver.class_name == "CGI"
|
|
75
|
-
end
|
|
76
|
-
|
|
77
77
|
def fold_cgi_call(method_name, args)
|
|
78
78
|
return nil if args.empty?
|
|
79
|
-
return nil unless args.first.is_a?(Type::Constant) && args.first.value.is_a?(String)
|
|
80
79
|
|
|
81
|
-
str = args.first
|
|
80
|
+
str = SingletonFolding.constant_string(args.first)
|
|
81
|
+
return nil if str.nil?
|
|
82
82
|
|
|
83
83
|
if CGI_ELEMENT_ESCAPE_METHODS.include?(method_name)
|
|
84
84
|
fold_cgi_element(method_name, str, args.drop(1))
|
|
@@ -94,9 +94,10 @@ module Rigor
|
|
|
94
94
|
# must be `Constant[String]` element names.
|
|
95
95
|
def fold_cgi_element(method_name, str, element_args)
|
|
96
96
|
elements = element_args.map do |arg|
|
|
97
|
-
|
|
97
|
+
value = SingletonFolding.constant_string(arg)
|
|
98
|
+
return nil if value.nil?
|
|
98
99
|
|
|
99
|
-
|
|
100
|
+
value
|
|
100
101
|
end
|
|
101
102
|
|
|
102
103
|
Type::Combinator.constant_of(CGI.public_send(method_name, str, *elements))
|
|
@@ -57,7 +57,10 @@ module Rigor
|
|
|
57
57
|
:+, :*, :==, :!=, :<, :<=, :>, :>=, :<=>,
|
|
58
58
|
:start_with?, :end_with?, :include?,
|
|
59
59
|
:delete_prefix, :delete_suffix,
|
|
60
|
-
:match?, :index, :rindex, :center, :ljust, :rjust
|
|
60
|
+
:match?, :index, :rindex, :center, :ljust, :rjust,
|
|
61
|
+
# 1-arg pure transforms/queries whose output never exceeds the
|
|
62
|
+
# input: `delete`/`squeeze` shrink the string, `count` → Integer.
|
|
63
|
+
:delete, :count, :squeeze
|
|
61
64
|
].freeze
|
|
62
65
|
SYMBOL_BINARY = Set[:==, :!=, :<=>, :<, :<=, :>, :>=].freeze
|
|
63
66
|
BOOL_BINARY = Set[:&, :|, :^, :==, :!=, :===].freeze
|
|
@@ -98,7 +101,7 @@ module Rigor
|
|
|
98
101
|
STRING_UNARY = Set[
|
|
99
102
|
:upcase, :downcase, :capitalize, :swapcase,
|
|
100
103
|
:reverse, :length, :size, :bytesize,
|
|
101
|
-
:empty?, :strip, :lstrip, :rstrip, :chomp, :chop,
|
|
104
|
+
:empty?, :strip, :lstrip, :rstrip, :chomp, :chop, :squeeze,
|
|
102
105
|
:to_s, :to_str, :to_sym, :intern,
|
|
103
106
|
:to_i, :to_f, :ord, :chr, :hex, :oct, :succ, :next,
|
|
104
107
|
:inspect, :hash
|
|
@@ -132,7 +135,10 @@ module Rigor
|
|
|
132
135
|
UNION_FOLD_OUTPUT_LIMIT = 8
|
|
133
136
|
|
|
134
137
|
# @return [Rigor::Type::Constant, Rigor::Type::Union, Rigor::Type::IntegerRange, nil]
|
|
135
|
-
def
|
|
138
|
+
def try_dispatch(context)
|
|
139
|
+
receiver = context.receiver
|
|
140
|
+
method_name = context.method_name
|
|
141
|
+
args = context.args
|
|
136
142
|
# v0.0.7 — `String#%` against a `Tuple` / `HashShape`
|
|
137
143
|
# argument runs Ruby's format-string engine when both
|
|
138
144
|
# sides are statically constant. The standard
|
|
@@ -1150,7 +1156,7 @@ module Rigor
|
|
|
1150
1156
|
#
|
|
1151
1157
|
# Resolution order:
|
|
1152
1158
|
#
|
|
1153
|
-
# 1. Primary class catalog (e.g.
|
|
1159
|
+
# 1. Primary class catalog (e.g. NUMERIC_CATALOG for an
|
|
1154
1160
|
# Integer receiver). When the catalog has an entry —
|
|
1155
1161
|
# even one classified `:dispatch` — that answer wins.
|
|
1156
1162
|
# The class's direct `rb_define_method` registration is
|
|
@@ -1210,8 +1216,8 @@ module Rigor
|
|
|
1210
1216
|
# arm first and the catalog would consult the Date entry
|
|
1211
1217
|
# in `DATE_CATALOG` for the wrong class.
|
|
1212
1218
|
CATALOG_BY_CLASS = Ractor.make_shareable([
|
|
1213
|
-
[Integer, [Builtins::
|
|
1214
|
-
[Float, [Builtins::
|
|
1219
|
+
[Integer, [Builtins::NUMERIC_CATALOG, "Integer"]],
|
|
1220
|
+
[Float, [Builtins::NUMERIC_CATALOG, "Float"]],
|
|
1215
1221
|
[String, [Builtins::STRING_CATALOG, "String"]],
|
|
1216
1222
|
[Symbol, [Builtins::STRING_CATALOG, "Symbol"]],
|
|
1217
1223
|
[Array, [Builtins::ARRAY_CATALOG, "Array"]],
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
require_relative "singleton_folding"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
module MethodDispatcher
|
|
9
|
+
# ADR-48 — `Data.define` value folding. Three responsibilities, all
|
|
10
|
+
# gated on a fully-decidable shape and degrading to today's behaviour
|
|
11
|
+
# (no carrier / the `Data` nominal) the moment a premise is uncertain,
|
|
12
|
+
# so the tier is precision-additive and adds no false-positive
|
|
13
|
+
# surface:
|
|
14
|
+
#
|
|
15
|
+
# 1. `Data.define(:x, :y)` on a `Singleton[Data]` receiver with
|
|
16
|
+
# literal-Symbol args and NO block -> `DataClass{members: [...]}`.
|
|
17
|
+
# A block (`Data.define(:x) do ... end`) defers (slice 4 hardens
|
|
18
|
+
# the block-body case); non-literal members (`Data.define(*names)`)
|
|
19
|
+
# defer.
|
|
20
|
+
# 2. `.new` / `.[]` on a `DataClass` receiver -> a `DataInstance`
|
|
21
|
+
# whose member map is built from the call's positional or keyword
|
|
22
|
+
# arguments. An arity / key mismatch degrades to the `Data` (or the
|
|
23
|
+
# tagged class) nominal rather than a wrong member map.
|
|
24
|
+
# 3. member reads + `[]` / `to_h` / `deconstruct` / `deconstruct_keys`
|
|
25
|
+
# / `members` / `with` on a `DataInstance` receiver -> the precise
|
|
26
|
+
# projected type. Unhandled methods return nil so the pipeline
|
|
27
|
+
# projects the instance to its nominal through RbsDispatch.
|
|
28
|
+
#
|
|
29
|
+
# See docs/adr/48-data-struct-value-folding.md.
|
|
30
|
+
module DataFolding
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# @return [Rigor::Type, nil] the folded result, or nil to defer.
|
|
34
|
+
def try_dispatch(context)
|
|
35
|
+
receiver = context.receiver
|
|
36
|
+
|
|
37
|
+
return fold_define(context) if SingletonFolding.receiver?(receiver, "Data")
|
|
38
|
+
|
|
39
|
+
case receiver
|
|
40
|
+
when Type::DataClass
|
|
41
|
+
materialize_instance(receiver.members, receiver.class_name, context)
|
|
42
|
+
when Type::DataInstance
|
|
43
|
+
fold_instance(receiver, context)
|
|
44
|
+
when Type::Singleton
|
|
45
|
+
fold_named_new(receiver, context)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# A `Data.define` value object assigned to a constant (or a
|
|
50
|
+
# `class Point < Data.define(...)` subclass) is canonicalised by the
|
|
51
|
+
# engine to `Singleton[Point]`, not a `DataClass` — so its member
|
|
52
|
+
# layout is read from the project side-table the scope indexer built
|
|
53
|
+
# (`Scope#data_member_layout`) rather than from the receiver carrier.
|
|
54
|
+
def fold_named_new(singleton, context)
|
|
55
|
+
scope = context.scope
|
|
56
|
+
return nil if scope.nil?
|
|
57
|
+
|
|
58
|
+
members = scope.data_member_layout(singleton.class_name)
|
|
59
|
+
return nil if members.nil?
|
|
60
|
+
|
|
61
|
+
materialize_instance(members, singleton.class_name, context)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# --- 1. Data.define(:x, :y) -------------------------------------
|
|
65
|
+
|
|
66
|
+
def fold_define(context)
|
|
67
|
+
return nil unless context.method_name == :define
|
|
68
|
+
# Block-form (`Data.define(:x) do ... end`) defers — slice 4.
|
|
69
|
+
return nil unless context.block_type.nil?
|
|
70
|
+
|
|
71
|
+
members = member_names_from_args(context.args)
|
|
72
|
+
return nil if members.nil? || members.empty?
|
|
73
|
+
|
|
74
|
+
Type::Combinator.data_class_of(members: members)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The ordered Symbol member names, or nil when any argument is not
|
|
78
|
+
# a literal `Constant[Symbol]` (a splat or dynamic name).
|
|
79
|
+
def member_names_from_args(args)
|
|
80
|
+
names = args.map do |arg|
|
|
81
|
+
return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(Symbol)
|
|
82
|
+
|
|
83
|
+
arg.value
|
|
84
|
+
end
|
|
85
|
+
return nil unless names.uniq.size == names.size
|
|
86
|
+
|
|
87
|
+
names
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# --- 2. Point.new(...) / Point[...] -----------------------------
|
|
91
|
+
|
|
92
|
+
def materialize_instance(members, class_name, context)
|
|
93
|
+
method_name = context.method_name
|
|
94
|
+
return nil unless %i[new []].include?(method_name)
|
|
95
|
+
|
|
96
|
+
map = member_map_for_new(members, context)
|
|
97
|
+
return degraded_instance(class_name) if map.nil?
|
|
98
|
+
|
|
99
|
+
Type::Combinator.data_instance_of(members: map, class_name: class_name)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Builds the member -> type map from the call's arguments, honouring
|
|
103
|
+
# the keyword vs positional distinction read off the call node. nil
|
|
104
|
+
# when the arguments cannot soundly populate every member.
|
|
105
|
+
def member_map_for_new(members, context)
|
|
106
|
+
if keyword_new?(context)
|
|
107
|
+
keyword_member_map(members, context.args)
|
|
108
|
+
else
|
|
109
|
+
positional_member_map(members, context.args)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# `Point.new(x: 1, y: 2)` arrives as a single trailing `HashShape`
|
|
114
|
+
# arg whose call node is a `KeywordHashNode`. Distinguishing it from
|
|
115
|
+
# a positional hash (`Point.new({x: 1})`, a `HashNode`) needs the
|
|
116
|
+
# call node, since both type to a `HashShape`.
|
|
117
|
+
def keyword_new?(context)
|
|
118
|
+
node = context.call_node
|
|
119
|
+
return false if node.nil?
|
|
120
|
+
|
|
121
|
+
arguments = node.arguments&.arguments
|
|
122
|
+
return false if arguments.nil? || arguments.empty?
|
|
123
|
+
|
|
124
|
+
arguments.last.is_a?(Prism::KeywordHashNode)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def keyword_member_map(members, args)
|
|
128
|
+
return nil unless args.size == 1
|
|
129
|
+
|
|
130
|
+
shape = args.first
|
|
131
|
+
return nil unless shape.is_a?(Type::HashShape) && shape.closed?
|
|
132
|
+
return nil unless shape.optional_keys.empty?
|
|
133
|
+
return nil unless shape.pairs.keys.sort == members.sort
|
|
134
|
+
|
|
135
|
+
members.to_h { |name| [name, shape.pairs.fetch(name)] }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def positional_member_map(members, args)
|
|
139
|
+
return nil unless args.size == members.size
|
|
140
|
+
|
|
141
|
+
members.zip(args).to_h
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# A `.new` whose arguments do not fold to a precise map still has a
|
|
145
|
+
# sound, more-precise-than-Dynamic answer: an instance of the
|
|
146
|
+
# tagged class (or the `Data` supertype).
|
|
147
|
+
def degraded_instance(class_name)
|
|
148
|
+
Type::Combinator.nominal_of(class_name || "Data")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# --- 3. inst.x / inst[...] / inst.to_h / ... --------------------
|
|
152
|
+
|
|
153
|
+
def fold_instance(instance, context)
|
|
154
|
+
method_name = context.method_name
|
|
155
|
+
args = context.args
|
|
156
|
+
members = instance.members
|
|
157
|
+
|
|
158
|
+
if members.key?(method_name) && args.empty? && !reader_overridden?(instance, method_name, context.scope)
|
|
159
|
+
return members.fetch(method_name)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
case method_name
|
|
163
|
+
when :[] then instance_index(instance, args)
|
|
164
|
+
when :to_h, :to_hash then instance_to_h(instance)
|
|
165
|
+
when :deconstruct then instance_deconstruct(instance)
|
|
166
|
+
when :deconstruct_keys then instance_deconstruct_keys(instance, args)
|
|
167
|
+
when :members then instance_members(instance)
|
|
168
|
+
when :with then instance_with(instance, args)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# A `Data.define` class body (the `class Point < Data.define(:x);
|
|
173
|
+
# def x; …; end; end` subclass body, or a `Const = Data.define(:x) do
|
|
174
|
+
# def x; …; end; end` block) can redefine a member's synthesised
|
|
175
|
+
# reader. When it does, `inst.x` runs that `def`, NOT the member, so
|
|
176
|
+
# folding the read to the member type would be unsound (a downstream
|
|
177
|
+
# FP). Both named forms register the override as a real `def` node
|
|
178
|
+
# under the class name, so an entry in the project def-node table is
|
|
179
|
+
# the discriminator (the synthesised reader has no def node). The
|
|
180
|
+
# value accessors `[]` / `to_h` / `deconstruct` bypass the reader and
|
|
181
|
+
# stay foldable, so this gate is on the bare member read only.
|
|
182
|
+
def reader_overridden?(instance, method_name, scope)
|
|
183
|
+
class_name = instance.class_name
|
|
184
|
+
return false if class_name.nil? || scope.nil?
|
|
185
|
+
|
|
186
|
+
!scope.user_def_for(class_name, method_name).nil?
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def instance_index(instance, args)
|
|
190
|
+
return nil unless args.size == 1
|
|
191
|
+
|
|
192
|
+
arg = args.first
|
|
193
|
+
return nil unless arg.is_a?(Type::Constant)
|
|
194
|
+
|
|
195
|
+
key = arg.value
|
|
196
|
+
case key
|
|
197
|
+
when Symbol
|
|
198
|
+
instance.members[key]
|
|
199
|
+
when Integer
|
|
200
|
+
values = instance.members.values
|
|
201
|
+
idx = key.negative? ? key + values.size : key
|
|
202
|
+
values[idx] if idx && idx >= 0 && idx < values.size
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def instance_to_h(instance)
|
|
207
|
+
Type::Combinator.hash_shape_of(instance.members.dup)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def instance_deconstruct(instance)
|
|
211
|
+
Type::Combinator.tuple_of(*instance.members.values)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# `deconstruct_keys(nil)` / `deconstruct_keys([:x])` both yield a
|
|
215
|
+
# subset of the member map; the conservative, always-correct answer
|
|
216
|
+
# is the full closed member shape.
|
|
217
|
+
def instance_deconstruct_keys(instance, args)
|
|
218
|
+
return nil unless args.size <= 1
|
|
219
|
+
|
|
220
|
+
Type::Combinator.hash_shape_of(instance.members.dup)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def instance_members(instance)
|
|
224
|
+
Type::Combinator.tuple_of(*instance.member_names.map { |name| Type::Combinator.constant_of(name) })
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# `Data#with(x: 9)` returns a new frozen copy with the named members
|
|
228
|
+
# overridden. Only a closed keyword `HashShape` whose keys are a
|
|
229
|
+
# subset of the members folds; anything else defers (RBS resolves
|
|
230
|
+
# `with` to `self`, returning the unchanged instance type).
|
|
231
|
+
def instance_with(instance, args)
|
|
232
|
+
return instance if args.empty?
|
|
233
|
+
return nil unless args.size == 1
|
|
234
|
+
|
|
235
|
+
shape = args.first
|
|
236
|
+
return nil unless shape.is_a?(Type::HashShape) && shape.closed?
|
|
237
|
+
return nil unless shape.optional_keys.empty?
|
|
238
|
+
return nil unless shape.pairs.keys.all? { |key| instance.members.key?(key) }
|
|
239
|
+
|
|
240
|
+
merged = instance.members.merge(shape.pairs)
|
|
241
|
+
Type::Combinator.data_instance_of(members: merged, class_name: instance.class_name)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../type"
|
|
4
|
+
require_relative "singleton_folding"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
module Inference
|
|
@@ -90,7 +91,10 @@ module Rigor
|
|
|
90
91
|
|
|
91
92
|
# @return [Rigor::Type, nil] folded result, or nil to defer
|
|
92
93
|
# to the next dispatcher tier.
|
|
93
|
-
def try_dispatch(
|
|
94
|
+
def try_dispatch(context)
|
|
95
|
+
receiver = context.receiver
|
|
96
|
+
method_name = context.method_name
|
|
97
|
+
args = context.args
|
|
94
98
|
return nil unless dispatch_target?(receiver)
|
|
95
99
|
return nil unless FILE_PURE_CLASS_METHODS.include?(method_name)
|
|
96
100
|
return nil if platform_specific_skip?(method_name)
|
|
@@ -107,7 +111,7 @@ module Rigor
|
|
|
107
111
|
end
|
|
108
112
|
|
|
109
113
|
def dispatch_target?(receiver)
|
|
110
|
-
receiver
|
|
114
|
+
SingletonFolding.receiver?(receiver, "File")
|
|
111
115
|
end
|
|
112
116
|
|
|
113
117
|
def constant_string_args(args)
|