rigortype 0.4.2 → 0.4.3
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 +1 -1
- data/docs/handbook/11-sig-gen.md +12 -5
- data/docs/manual/02-cli-reference.md +19 -1
- data/docs/manual/03-configuration.md +6 -4
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +16 -9
- data/lib/rigor/analysis/check_rules/rule_ids.rb +7 -0
- data/lib/rigor/analysis/check_rules.rb +23 -22
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
- data/lib/rigor/analysis/incremental_session.rb +89 -22
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +119 -15
- data/lib/rigor/analysis/reachability/scan.rb +332 -173
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/run_cache_probe.rb +1 -1
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +26 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +28 -9
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
- data/lib/rigor/analysis/runner.rb +29 -11
- data/lib/rigor/analysis/worker_session.rb +1 -1
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +36 -8
- data/lib/rigor/cli/check_command.rb +138 -39
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +157 -24
- data/lib/rigor/cli.rb +30 -4
- data/lib/rigor/configuration.rb +15 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_loader.rb +33 -0
- data/lib/rigor/environment/required_features.rb +4 -2
- data/lib/rigor/environment.rb +12 -5
- data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
- data/lib/rigor/inference/captured_locals.rb +43 -3
- data/lib/rigor/inference/expression_typer.rb +58 -25
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -16
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +52 -3
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +2 -2
- data/lib/rigor/inference/project_patched_scanner.rb +38 -24
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +112 -40
- data/lib/rigor/inference/refined_dispatch.rb +62 -14
- data/lib/rigor/inference/repeated_or_writes.rb +2 -0
- data/lib/rigor/inference/scope_indexer.rb +140 -5
- data/lib/rigor/inference/statement_evaluator.rb +223 -31
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/language_server/project_context.rb +56 -5
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +118 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +433 -0
- data/lib/rigor/lens/members.rb +153 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +280 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +40 -0
- data/lib/rigor/project_environment.rb +20 -1
- data/lib/rigor/protection/mutation_cache.rb +2 -1
- data/lib/rigor/scope.rb +22 -11
- data/lib/rigor/sig_gen/generator.rb +45 -5
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor.rbs +6 -3
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +18 -1
data/lib/rigor/environment.rb
CHANGED
|
@@ -97,7 +97,7 @@ module Rigor
|
|
|
97
97
|
# for the overlay, instead of aborting the run outright (see {#hkt_registry}).
|
|
98
98
|
@hkt_scan_failure = FailureSlot.new
|
|
99
99
|
@constant_type_cache = ConstantTypeCacheHolder.new
|
|
100
|
-
# ADR-82 WD9 — `[gem_name, version]`
|
|
100
|
+
# ADR-82 WD9 — `[gem_name, version, variants]` entries for the locked gems with no resolvable RBS. The
|
|
101
101
|
# root-constant ownership index over them is built lazily (first unresolved constant read) so runs
|
|
102
102
|
# whose constants all resolve never pay the entry-file scan.
|
|
103
103
|
@missing_rbs_gems = missing_rbs_gems.freeze
|
|
@@ -444,8 +444,8 @@ module Rigor
|
|
|
444
444
|
end
|
|
445
445
|
|
|
446
446
|
# The `:missing` classification (locked gems with no RBS through any resolution path), computed once
|
|
447
|
-
# and consumed twice: as `[gem_name, version]`
|
|
448
|
-
# as the eligible-gem set for the ADR-72 overlay resolution. Returns `[
|
|
447
|
+
# and consumed twice: as `[gem_name, version, variants]` entries for the ADR-82 WD9 constant-ownership
|
|
448
|
+
# index, and as the eligible-gem set for the ADR-72 overlay resolution. Returns `[entries, overlay_paths]`.
|
|
449
449
|
def missing_gems_and_overlay_paths(locked:, default_libraries:, bundle_sig_paths:,
|
|
450
450
|
rbs_collection_paths:, plugin_registry:, signature_paths: [],
|
|
451
451
|
bundle_root: nil)
|
|
@@ -458,7 +458,7 @@ module Rigor
|
|
|
458
458
|
missing_gem_names: rows.map(&:gem_name), plugin_registry: plugin_registry,
|
|
459
459
|
signature_paths: signature_paths
|
|
460
460
|
)
|
|
461
|
-
[rows
|
|
461
|
+
[index_entries(rows, locked), overlays]
|
|
462
462
|
end
|
|
463
463
|
|
|
464
464
|
# Issue #530 — a project with no `Gemfile.lock` still has gems on disk, and before this it handed the
|
|
@@ -472,7 +472,14 @@ module Rigor
|
|
|
472
472
|
return [[], []] if installed.empty?
|
|
473
473
|
|
|
474
474
|
rows = missing_rows(installed, default_libraries, bundle_sig_paths, rbs_collection_paths)
|
|
475
|
-
[rows
|
|
475
|
+
[index_entries(rows, installed), []]
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# The constant-ownership index's input: each `:missing` row with every `[version, platform]` variant its
|
|
479
|
+
# gem was locked (or found installed) under, because Bundler installs the variant matching the host —
|
|
480
|
+
# `commonmarker-2.10.0-arm64-darwin/` under a lockfile whose representative is the `ruby` one (#1844).
|
|
481
|
+
def index_entries(rows, gems)
|
|
482
|
+
rows.map { |row| [row.gem_name, row.version, gems[row.gem_name]&.variants] }
|
|
476
483
|
end
|
|
477
484
|
|
|
478
485
|
def missing_rows(gems, default_libraries, bundle_sig_paths, rbs_collection_paths)
|
|
@@ -16,6 +16,9 @@ module Rigor
|
|
|
16
16
|
# (`Integer#quo` and `File#to_path`, whose CRuby owners are `Numeric` and `IO`), and declares a few CRuby no
|
|
17
17
|
# longer defines (`Process::Status#&`). A class no catalogue covers answers false, as does a method its
|
|
18
18
|
# catalogue does not list: the caller reads both as "not proven". The catalogue is one CRuby snapshot.
|
|
19
|
+
#
|
|
20
|
+
# Issue #1752 — the singleton side reads the catalogue's `singleton_methods` (each `rb_define_singleton_method`
|
|
21
|
+
# on the class), the record that `Integer.sqrt` is CRuby's own method of `Integer`'s singleton class.
|
|
19
22
|
module CRubyDefiners
|
|
20
23
|
module_function
|
|
21
24
|
|
|
@@ -25,7 +28,15 @@ module Rigor
|
|
|
25
28
|
!names.nil? && names.include?(method_name.to_s)
|
|
26
29
|
end
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
# Does the catalogue prove CRuby defines `method_name` on `class_name`'s singleton class itself?
|
|
32
|
+
def singleton_defines?(class_name, method_name)
|
|
33
|
+
names = SINGLETON_INDEX[class_name.to_s.delete_prefix("::")]
|
|
34
|
+
!names.nil? && names.include?(method_name.to_s)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# `{class name => Set[method name]}` from each catalogued class's instance methods and aliases, or from its
|
|
38
|
+
# `singleton_methods` when `side` is `:singleton`.
|
|
39
|
+
def build_index(side = :instance)
|
|
29
40
|
index = Hash.new { |hash, key| hash[key] = Set.new }
|
|
30
41
|
MethodCatalog.topic_paths.each do |path|
|
|
31
42
|
data = YAML.safe_load_file(path, permitted_classes: [Symbol])
|
|
@@ -35,9 +46,7 @@ module Rigor
|
|
|
35
46
|
classes.each do |name, klass|
|
|
36
47
|
next if klass["rdoc_only"]
|
|
37
48
|
|
|
38
|
-
|
|
39
|
-
names.merge(defined_names(klass["instance_methods"]))
|
|
40
|
-
names.merge(defined_names(klass["aliases"]))
|
|
49
|
+
merge_names(index[name], klass, side)
|
|
41
50
|
end
|
|
42
51
|
end
|
|
43
52
|
Ractor.make_shareable(index.to_h { |name, names| [name, names.freeze] })
|
|
@@ -45,6 +54,13 @@ module Rigor
|
|
|
45
54
|
Ractor.make_shareable({})
|
|
46
55
|
end
|
|
47
56
|
|
|
57
|
+
def merge_names(names, klass, side)
|
|
58
|
+
return names.merge(defined_names(klass["singleton_methods"])) if side == :singleton
|
|
59
|
+
|
|
60
|
+
names.merge(defined_names(klass["instance_methods"]))
|
|
61
|
+
names.merge(defined_names(klass["aliases"]))
|
|
62
|
+
end
|
|
63
|
+
|
|
48
64
|
# The names of `entries` CRuby defines at runtime: one the catalogue marks `rdoc_only` was registered only for
|
|
49
65
|
# RDoc, inside an `#if 0` block.
|
|
50
66
|
def defined_names(entries)
|
|
@@ -54,7 +70,8 @@ module Rigor
|
|
|
54
70
|
end
|
|
55
71
|
|
|
56
72
|
INDEX = build_index
|
|
57
|
-
|
|
73
|
+
SINGLETON_INDEX = build_index(:singleton)
|
|
74
|
+
private_constant :INDEX, :SINGLETON_INDEX
|
|
58
75
|
end
|
|
59
76
|
end
|
|
60
77
|
end
|
|
@@ -310,23 +310,63 @@ module Rigor
|
|
|
310
310
|
|
|
311
311
|
# The mutation sites under `body` whose reads name a variable {.captured_target?} accepts, keyed by name.
|
|
312
312
|
# The block yields the names the enclosing construct introduces, asked only when a local site is found.
|
|
313
|
+
#
|
|
314
|
+
# A site whose receiver is a body local that aliases a capture (`l2 = log; l2 << e`) mutates that capture's
|
|
315
|
+
# object, so it is attributed to the capture as well ({.aliased_captures}): left out, the next iteration
|
|
316
|
+
# read `log` at its entry `[]` and `[1, 2].inject(0) { |a, e| r = a + log.size; l2 = log; l2 << e; r }`
|
|
317
|
+
# folded to `0` where Ruby answers `1` (issue #1332).
|
|
313
318
|
def body_mutation_sites(body, base_scope, non_locals, &)
|
|
314
319
|
sites = nil
|
|
315
320
|
evaluator = nil
|
|
321
|
+
aliases = nil
|
|
316
322
|
Source::NodeWalker.each_with_ancestors(body) do |descendant, ancestors|
|
|
317
323
|
# A callee is resolved in the call-site scope, as the straight-line callee floor resolves it.
|
|
318
324
|
site = mutation_site(descendant) { evaluator ||= StatementEvaluator.new(scope: base_scope) }
|
|
319
325
|
next if site.nil?
|
|
320
326
|
|
|
321
327
|
site_reads(site).each do |read|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
328
|
+
targets = captured_target?(read, ancestors, base_scope, non_locals, &) ? [read] : []
|
|
329
|
+
if read.is_a?(Prism::LocalVariableReadNode)
|
|
330
|
+
aliases ||= local_aliases(body)
|
|
331
|
+
targets.concat(aliased_captures(read.name, aliases, base_scope, non_locals, Set.new, &))
|
|
332
|
+
end
|
|
333
|
+
targets.each { |target| ((sites ||= {})[target.name] ||= []) << site }
|
|
325
334
|
end
|
|
326
335
|
end
|
|
327
336
|
sites || NO_SITES
|
|
328
337
|
end
|
|
329
338
|
|
|
339
|
+
# `{ name => [[read, ancestors], ...] }`: every variable read a local write under `body` may store
|
|
340
|
+
# ({ReceiverAlias.candidates} of its value), with the ancestors {.captured_target?} resolves it against.
|
|
341
|
+
def local_aliases(body)
|
|
342
|
+
aliases = {}
|
|
343
|
+
Source::NodeWalker.each_with_ancestors(body) do |node, ancestors|
|
|
344
|
+
next unless node.is_a?(Prism::LocalVariableWriteNode) || node.is_a?(Prism::LocalVariableOrWriteNode)
|
|
345
|
+
|
|
346
|
+
ReceiverAlias.candidates(node.value).each do |read|
|
|
347
|
+
(aliases[node.name] ||= []) << [read, ancestors + [node]]
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
aliases
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# The captured reads the local `name` may alias, followed through local-to-local copies (`l3 = l2 = log`).
|
|
354
|
+
# It over-approximates — a write in any branch, or in a nested scope reusing the name, counts — which only
|
|
355
|
+
# widens a capture's binding.
|
|
356
|
+
def aliased_captures(name, aliases, base_scope, non_locals, visited, &)
|
|
357
|
+
return [] unless visited.add?(name)
|
|
358
|
+
|
|
359
|
+
aliases.fetch(name, NO_NAMES).flat_map do |read, ancestors|
|
|
360
|
+
if captured_target?(read, ancestors, base_scope, non_locals, &)
|
|
361
|
+
[read]
|
|
362
|
+
elsif read.is_a?(Prism::LocalVariableReadNode)
|
|
363
|
+
aliased_captures(read.name, aliases, base_scope, non_locals, visited, &)
|
|
364
|
+
else
|
|
365
|
+
NO_NAMES
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
330
370
|
# The mutation site `node` is — the node itself, or a {UnknownStoreWidening::CalleeStore} for a self-call
|
|
331
371
|
# whose callee content-mutates a local it is passed — or nil. A self-call is resolved by the
|
|
332
372
|
# `StatementEvaluator` the block yields, which the caller builds only when one is needed. A mutator name
|
|
@@ -1952,19 +1952,20 @@ module Rigor
|
|
|
1952
1952
|
# replaces (a refined `String#upcase` must not fold, a refined `map` must not fold per element, a top-level
|
|
1953
1953
|
# `def` must not bind ahead of a refinement of `Object`, a plugin models the class's own method). An unreadable
|
|
1954
1954
|
# body, or a list that may hold any refinement, answers `Dynamic[top]`, never the replaced method's signature.
|
|
1955
|
-
# A union receiver is decided per member; a `Dynamic` receiver
|
|
1956
|
-
#
|
|
1957
|
-
#
|
|
1955
|
+
# A union receiver is decided per member; a `Dynamic` receiver and a call no refinement wins fall through. A
|
|
1956
|
+
# class object walks its own lookup (issue #1752): under `refine(Object) { def oo = :o }` a refinement that wins
|
|
1957
|
+
# `Integer.oo` answers `Dynamic[top]` ({#refined_class_object_result}). A call site no refinement is in effect
|
|
1958
|
+
# at answers nil at once and records nothing: its answer cannot change without an edit to its own file.
|
|
1958
1959
|
#
|
|
1959
1960
|
# ADR-121 WD7 (A2) — inside the span of an activation that puts an opaque module in effect
|
|
1960
|
-
# ({InEffectRefinements.opaque_module?}), any method of any
|
|
1961
|
-
#
|
|
1961
|
+
# ({InEffectRefinements.opaque_module?}), any method of any object may be replaced, so every call on an instance
|
|
1962
|
+
# or a class object answers `Dynamic[top]`.
|
|
1962
1963
|
def try_refined_dispatch(node, receiver, arg_types, block_type)
|
|
1963
1964
|
list = scope.in_effect_refinements(node)
|
|
1964
1965
|
return nil if list.empty?
|
|
1965
1966
|
|
|
1966
1967
|
InEffectRefinements.record_refinement_names(node.name)
|
|
1967
|
-
return dynamic_top if
|
|
1968
|
+
return dynamic_top if refined_receiver?(receiver) && opaque_refinement_span?(node, list)
|
|
1968
1969
|
return nil if scope.discovered_refinements.empty?
|
|
1969
1970
|
|
|
1970
1971
|
refined_indirect_result(node, receiver, list) ||
|
|
@@ -2010,6 +2011,9 @@ module Rigor
|
|
|
2010
2011
|
def refined_name_for?(receiver, name, list)
|
|
2011
2012
|
members = receiver.is_a?(Type::Union) ? receiver.members : [receiver]
|
|
2012
2013
|
members.any? do |member|
|
|
2014
|
+
next !RefinedDispatch.winner(scope, member.class_name, name, list, kind: :singleton).nil? if
|
|
2015
|
+
member.is_a?(Type::Singleton)
|
|
2016
|
+
|
|
2013
2017
|
class_name = refined_receiver_class_name(member)
|
|
2014
2018
|
!class_name.nil? && !RefinedDispatch.winner(scope, class_name, name, list).nil?
|
|
2015
2019
|
end
|
|
@@ -2033,11 +2037,11 @@ module Rigor
|
|
|
2033
2037
|
InEffectRefinements.opaque_in?(scope, lexical)
|
|
2034
2038
|
end
|
|
2035
2039
|
|
|
2036
|
-
# Does `receiver` (or a member of a union) have a
|
|
2037
|
-
# object
|
|
2038
|
-
def
|
|
2040
|
+
# Does `receiver` (or a member of a union) have a lookup the refined arm answers for: an instance of a class,
|
|
2041
|
+
# or a class object (issue #1752), not `Dynamic`?
|
|
2042
|
+
def refined_receiver?(receiver)
|
|
2039
2043
|
members = receiver.is_a?(Type::Union) ? receiver.members : [receiver]
|
|
2040
|
-
members.any? { |member| !refined_receiver_class_name(member).nil? }
|
|
2044
|
+
members.any? { |member| member.is_a?(Type::Singleton) || !refined_receiver_class_name(member).nil? }
|
|
2041
2045
|
end
|
|
2042
2046
|
|
|
2043
2047
|
def refined_receiver_result(node, receiver, list, arg_types, block_type)
|
|
@@ -2053,6 +2057,9 @@ module Rigor
|
|
|
2053
2057
|
end
|
|
2054
2058
|
|
|
2055
2059
|
def refined_member_result(node, receiver, list, arg_types, block_type)
|
|
2060
|
+
return refined_class_object_result(node, receiver, list, arg_types, block_type) if
|
|
2061
|
+
receiver.is_a?(Type::Singleton)
|
|
2062
|
+
|
|
2056
2063
|
class_name = refined_receiver_class_name(receiver)
|
|
2057
2064
|
return nil if class_name.nil?
|
|
2058
2065
|
|
|
@@ -2064,6 +2071,16 @@ module Rigor
|
|
|
2064
2071
|
dynamic_top
|
|
2065
2072
|
end
|
|
2066
2073
|
|
|
2074
|
+
# Issue #1752 — a call on a class object, walked through its singleton levels. A refinement that wins answers
|
|
2075
|
+
# `Dynamic[top]`, never its refine body's type: a hook on any ancestor (an RBS-declared `include Singleton`, a
|
|
2076
|
+
# gem concern, `define_singleton_method(:included)`, a `Module#included` patch) can install the class object's
|
|
2077
|
+
# own method where no table sees it, and the body's type then fires downstream on correct code. A walk that finds
|
|
2078
|
+
# no winner keeps the class object's own answer (`Integer.sqrt(16)` stays `Integer`).
|
|
2079
|
+
def refined_class_object_result(node, receiver, list, _arg_types, _block_type)
|
|
2080
|
+
winner = RefinedDispatch.winner(scope, receiver.class_name, node.name, list, kind: :singleton)
|
|
2081
|
+
dynamic_top unless winner.nil?
|
|
2082
|
+
end
|
|
2083
|
+
|
|
2067
2084
|
# The winning refine body's inferred return, re-typed with the receiver's class as `self` and the call's
|
|
2068
2085
|
# argument types bound as an undeclared method's are; nil when the body is not readable here.
|
|
2069
2086
|
#
|
|
@@ -2082,8 +2099,9 @@ module Rigor
|
|
|
2082
2099
|
nil
|
|
2083
2100
|
end
|
|
2084
2101
|
|
|
2085
|
-
# The class an instance-side receiver is an instance of, or nil for one
|
|
2086
|
-
# class object (
|
|
2102
|
+
# The class an instance-side receiver is an instance of, or nil for one this instance walk does not answer: a
|
|
2103
|
+
# class object (it walks its own lookup, {#refined_class_object_result}), `Dynamic`, `top`, or a shape with no
|
|
2104
|
+
# class.
|
|
2087
2105
|
def refined_receiver_class_name(receiver)
|
|
2088
2106
|
case receiver
|
|
2089
2107
|
when Type::Nominal, Type::StructInstance, Type::DataInstance then receiver.class_name
|
|
@@ -2453,7 +2471,7 @@ module Rigor
|
|
|
2453
2471
|
return every_definer_raises_type(raising, receiver, call_node, arg_types, block_type) if def_node.nil?
|
|
2454
2472
|
|
|
2455
2473
|
result = infer_user_method_return(def_node, receiver, arg_types,
|
|
2456
|
-
self_fold_safe: fold_safe_call_receiver?(call_node
|
|
2474
|
+
self_fold_safe: fold_safe_call_receiver?(call_node),
|
|
2457
2475
|
yield_type: block_type)
|
|
2458
2476
|
return result if result.nil?
|
|
2459
2477
|
|
|
@@ -2488,11 +2506,11 @@ module Rigor
|
|
|
2488
2506
|
# This is only a property of the receiver expression; whether the grant is actually issued also needs
|
|
2489
2507
|
# a `StructInstance` carrier and a body that survives the self-use scan, both decided in
|
|
2490
2508
|
# {#build_user_method_body_scope}.
|
|
2491
|
-
def fold_safe_call_receiver?(call_node
|
|
2509
|
+
def fold_safe_call_receiver?(call_node)
|
|
2492
2510
|
return false if call_node.nil?
|
|
2493
2511
|
|
|
2494
2512
|
case call_node.receiver
|
|
2495
|
-
when Prism::CallNode then materialization_call?(call_node.receiver
|
|
2513
|
+
when Prism::CallNode then materialization_call?(call_node.receiver)
|
|
2496
2514
|
when Prism::LocalVariableReadNode then scope.struct_fold_safe?(call_node.receiver.name)
|
|
2497
2515
|
when nil, Prism::SelfNode then scope.struct_fold_safe?(:self)
|
|
2498
2516
|
else false
|
|
@@ -2502,10 +2520,10 @@ module Rigor
|
|
|
2502
2520
|
# Issue #595 — the materialisation test lives ONCE, in
|
|
2503
2521
|
# [`struct_materialization.rb`](method_dispatcher/struct_materialization.rb), because the direct
|
|
2504
2522
|
# member-read gate ({MethodDispatcher::StructFolding}`#fresh_receiver?`) needs the identical answer;
|
|
2505
|
-
# see {MethodDispatcher::StructMaterialization.materialization_call?}.
|
|
2506
|
-
#
|
|
2507
|
-
def materialization_call?(node
|
|
2508
|
-
MethodDispatcher::StructMaterialization.materialization_call?(node,
|
|
2523
|
+
# see {MethodDispatcher::StructMaterialization.materialization_call?}. A `.with` call is never one
|
|
2524
|
+
# (`Struct` has no `#with`, #1335), so the test needs no lookup from either consumer.
|
|
2525
|
+
def materialization_call?(node)
|
|
2526
|
+
MethodDispatcher::StructMaterialization.materialization_call?(node, scope)
|
|
2509
2527
|
end
|
|
2510
2528
|
|
|
2511
2529
|
# Module-singleton call resolution (ADR-57 follow-up) — resolves `Foo.<name>` on a `Singleton[Foo]`
|
|
@@ -2674,7 +2692,7 @@ module Rigor
|
|
|
2674
2692
|
def every_definer_raises_type(defs, receiver, call_node, arg_types, block_type)
|
|
2675
2693
|
return nil if defs.nil?
|
|
2676
2694
|
|
|
2677
|
-
self_fold_safe = fold_safe_call_receiver?(call_node
|
|
2695
|
+
self_fold_safe = fold_safe_call_receiver?(call_node)
|
|
2678
2696
|
defs.each do |def_node|
|
|
2679
2697
|
result = infer_user_method_return(def_node, receiver, arg_types,
|
|
2680
2698
|
self_fold_safe: self_fold_safe, yield_type: block_type)
|
|
@@ -5417,13 +5435,27 @@ module Rigor
|
|
|
5417
5435
|
acc, rest = inject_constant_start(members, seed, has_seed)
|
|
5418
5436
|
return nil if acc.nil?
|
|
5419
5437
|
|
|
5420
|
-
rest.
|
|
5421
|
-
|
|
5438
|
+
captured = inject_captured_bindings(block, rest.map { |value| Type::Combinator.constant_of(value) })
|
|
5439
|
+
rest.each_with_index do |element_value, position|
|
|
5440
|
+
acc = inject_constant_step(block, acc, element_value, captured, position)
|
|
5422
5441
|
return nil if acc.nil?
|
|
5423
5442
|
end
|
|
5424
5443
|
acc
|
|
5425
5444
|
end
|
|
5426
5445
|
|
|
5446
|
+
# Issue #1332 — the #587 (b) captured-block binding ({#captured_block_bindings}) both inject arms lay under
|
|
5447
|
+
# every step, as the Tuple / Range per-element folds and the per-pair `HashShape` fold do. Without it each
|
|
5448
|
+
# step reads a captured variable at its entry binding, so a body that rebinds or mutates one (`n += 1`,
|
|
5449
|
+
# `log << e`, `cache[:k] ||= e`) pinned the result: `n = 0; [1, 2].inject(0) { |a, e| r = a + n; n += 1;
|
|
5450
|
+
# r }` folded to `0` where Ruby answers `1`. The fixpoint binds the accumulator parameter to `Dynamic[top]`
|
|
5451
|
+
# rather than to a type the inject fixpoint has not converged yet: a captured name that reads the
|
|
5452
|
+
# accumulator widens rather than pins. `element_types` is the constant arm's steps, one position per block
|
|
5453
|
+
# run, which {RepeatedOrWrites} reads; nil for the nominal arm, which marks every repeated site.
|
|
5454
|
+
def inject_captured_bindings(block, element_types, element: nil)
|
|
5455
|
+
element ||= Type::Combinator.union(*element_types)
|
|
5456
|
+
captured_block_bindings(block, [Type::Combinator.untyped, element], element_types: element_types)
|
|
5457
|
+
end
|
|
5458
|
+
|
|
5427
5459
|
# Extracts the receiver's foldable constant values, size-capped before enumeration, or nil to decline.
|
|
5428
5460
|
def inject_constant_members(receiver)
|
|
5429
5461
|
case receiver
|
|
@@ -5475,9 +5507,9 @@ module Rigor
|
|
|
5475
5507
|
# Evaluates the block body once with the running constant accumulator + the next constant element
|
|
5476
5508
|
# bound to the block params, returning the result when it is a foldable `Constant` within the
|
|
5477
5509
|
# magnitude cap, else nil to decline the whole fold.
|
|
5478
|
-
def inject_constant_step(block, acc, element_value)
|
|
5510
|
+
def inject_constant_step(block, acc, element_value, captured, position)
|
|
5479
5511
|
element = Type::Combinator.constant_of(element_value)
|
|
5480
|
-
result = type_block_body_with_param(block, [acc, element])
|
|
5512
|
+
result = type_block_body_with_param(block, [acc, element], captured: captured, position: position)
|
|
5481
5513
|
return nil unless result.is_a?(Type::Constant)
|
|
5482
5514
|
return nil unless inject_foldable?(result.value)
|
|
5483
5515
|
return nil if inject_magnitude_too_large?(result.value)
|
|
@@ -5507,13 +5539,14 @@ module Rigor
|
|
|
5507
5539
|
seed_acc = has_seed ? seed : element
|
|
5508
5540
|
return nil if seed_acc.nil?
|
|
5509
5541
|
|
|
5542
|
+
captured = inject_captured_bindings(block, nil, element: element)
|
|
5510
5543
|
converged = BodyFixpoint.converge(
|
|
5511
5544
|
names: [:__inject_acc__],
|
|
5512
5545
|
seed_bindings: { __inject_acc__: seed_acc },
|
|
5513
5546
|
widen: method(:widen_value_pinned),
|
|
5514
5547
|
evaluate_body: lambda do |bindings|
|
|
5515
5548
|
acc = bindings[:__inject_acc__]
|
|
5516
|
-
result = type_block_body_with_param(block, [acc, element])
|
|
5549
|
+
result = type_block_body_with_param(block, [acc, element], captured: captured)
|
|
5517
5550
|
result.nil? ? {} : { __inject_acc__: result }
|
|
5518
5551
|
end
|
|
5519
5552
|
)
|
|
@@ -148,35 +148,26 @@ module Rigor
|
|
|
148
148
|
# define any name), and a row whose class the walk could not name ({.class_wildcard_key?}), which reaches
|
|
149
149
|
# every receiver.
|
|
150
150
|
#
|
|
151
|
+
# Issue #1752 — `kind: :singleton` asks for the class object `class_name`: a refinement of any class or module
|
|
152
|
+
# on its lookup reaches it (`Class`, `Module`, `Object`, `Kernel`, `BasicObject`, a module it or a superclass
|
|
153
|
+
# extends; {RefinedDispatch::ClassObject.reach}). Where that lookup cannot be read, every row counts whose class
|
|
154
|
+
# a class object may reach ({RefinedDispatch::ClassObject.reachable?}).
|
|
155
|
+
#
|
|
151
156
|
# ADR-46 — the answer is a function of every refinement of this name in the project, so the consumer
|
|
152
157
|
# depends on the name whichever way it answers; a refine body edited in another file must re-check it
|
|
153
158
|
# (`IncrementalSession#refinement_affected`). It also depends on the wildcard rows (`refinement:*`).
|
|
154
|
-
def refining_modules(scope, class_name, method_name)
|
|
159
|
+
def refining_modules(scope, class_name, method_name, kind = :instance)
|
|
155
160
|
record_refinement_names(method_name)
|
|
156
161
|
refinements = scope.discovered_refinements
|
|
157
162
|
return nil if refinements.empty?
|
|
158
163
|
|
|
164
|
+
reaches = receiver_reach(scope, class_name, kind)
|
|
159
165
|
modules = nil
|
|
160
166
|
refinements.each do |refined, methods|
|
|
161
167
|
names = methods[method_name]
|
|
162
168
|
unread = methods[WILDCARD]
|
|
163
169
|
next if names.nil? && unread.nil?
|
|
164
|
-
next unless
|
|
165
|
-
|
|
166
|
-
modules = (modules || []).concat(names || EMPTY, unread || EMPTY)
|
|
167
|
-
end
|
|
168
|
-
modules
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# ADR-121 WD7 — {.refining_modules} for a receiver the class rows do not reach (a class object): only the rows
|
|
172
|
-
# whose class the walk could not name ({.class_wildcard_key?}), for the name or as a names-wildcard.
|
|
173
|
-
def class_unknown_refining_modules(scope, method_name)
|
|
174
|
-
record_refinement_names(method_name)
|
|
175
|
-
modules = nil
|
|
176
|
-
scope.discovered_refinements.each do |refined, methods|
|
|
177
|
-
names = methods[method_name]
|
|
178
|
-
unread = methods[WILDCARD]
|
|
179
|
-
next if (names.nil? && unread.nil?) || !class_wildcard_key?(scope, refined)
|
|
170
|
+
next unless reaches.call(refined) || class_wildcard_key?(scope, refined)
|
|
180
171
|
|
|
181
172
|
modules = (modules || []).concat(names || EMPTY, unread || EMPTY)
|
|
182
173
|
end
|
|
@@ -258,6 +249,19 @@ module Rigor
|
|
|
258
249
|
end
|
|
259
250
|
end
|
|
260
251
|
|
|
252
|
+
# Does a row keyed on a refined class reach the receiver? A class object's lookup is read once, on the first
|
|
253
|
+
# row that asks.
|
|
254
|
+
def receiver_reach(scope, class_name, kind)
|
|
255
|
+
return ->(refined) { refined_receiver_class?(scope, class_name, refined) } unless kind == :singleton
|
|
256
|
+
|
|
257
|
+
reach = nil
|
|
258
|
+
lambda do |refined|
|
|
259
|
+
reach ||= [RefinedDispatch::ClassObject.reach(scope, class_name)]
|
|
260
|
+
names = reach.first
|
|
261
|
+
names.nil? ? RefinedDispatch::ClassObject.reachable?(scope, refined) : names.include?(refined)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
261
265
|
def refined_receiver_class?(scope, class_name, refined)
|
|
262
266
|
return true if class_name == refined
|
|
263
267
|
|
|
@@ -16,7 +16,7 @@ require_relative "last_line"
|
|
|
16
16
|
|
|
17
17
|
module Rigor
|
|
18
18
|
module Inference
|
|
19
|
-
# Which code may rebind the regex match globals (`$~` and the `$&` / `` $` `` / `$'` / `$+` / `$1`..`$
|
|
19
|
+
# Which code may rebind the regex match globals (`$~` and the `$&` / `` $` `` / `$'` / `$+` / `$1`..`$N` family
|
|
20
20
|
# derived from it) that a scope's narrowing speaks for. Ruby keeps them in the method frame's special-variable
|
|
21
21
|
# slot, and every block and closure made in the method reaches that same slot, so a match a block body runs
|
|
22
22
|
# rebinds the enclosing method's `$~` (issue #1358). A `def`, class or module body runs in a frame of its own,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
module MethodDispatcher
|
|
6
|
+
# What a call's AST says about its argument types that the types alone cannot: shared by the RBS tier's
|
|
7
|
+
# overload selection and the constructor folds, so the two readings of one call cannot drift.
|
|
8
|
+
module ArgumentNodes
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# Issue #1727 — whether `args`' last entry is the call's keyword hash (`f(a, k: 1)`), which the overload
|
|
12
|
+
# selector then matches against an overload's keywords. Only the AST tells a keyword hash from a
|
|
13
|
+
# braced positional one (`f(a, { k: 1 })`), so a dispatch without the call node, or one whose
|
|
14
|
+
# arguments are not the node's own (a forwarded `...`, a `send` re-dispatch), answers false and keeps
|
|
15
|
+
# reading every argument positionally.
|
|
16
|
+
def keyword_hash_last?(call_node, args)
|
|
17
|
+
return false if args.empty? || !call_node.respond_to?(:arguments)
|
|
18
|
+
|
|
19
|
+
arguments = call_node.arguments&.arguments
|
|
20
|
+
!arguments.nil? && arguments.size == args.size && arguments.last.is_a?(Prism::KeywordHashNode)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../../type"
|
|
4
4
|
require_relative "constant_folding"
|
|
5
5
|
require_relative "random_folding"
|
|
6
|
+
require_relative "unit_denominator_rational"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Inference
|
|
@@ -346,8 +347,8 @@ module Rigor
|
|
|
346
347
|
# is a numeric Constant. The actual Ruby constructor runs at fold time (host-side), so the result
|
|
347
348
|
# respects Ruby's normalisation (`Rational(2, 4)` → `Rational(1, 2)`).
|
|
348
349
|
def try_numeric_constructor(method_name, args)
|
|
349
|
-
|
|
350
|
-
return
|
|
350
|
+
foldable = [1, 2].include?(args.size) && args.all? { |arg| numeric_constant?(arg) }
|
|
351
|
+
return UnitDenominatorRational.answer(method_name, args) unless foldable
|
|
351
352
|
|
|
352
353
|
values = args.map(&:value)
|
|
353
354
|
result = NUMERIC_CONSTRUCTORS[method_name].call(*values)
|
|
@@ -35,8 +35,8 @@ module Rigor
|
|
|
35
35
|
# beginless),
|
|
36
36
|
# - the slice is empty.
|
|
37
37
|
#
|
|
38
|
-
# K is read from `$1`..`$9` only:
|
|
39
|
-
#
|
|
38
|
+
# K is read from `$1`..`$9` only: the fold declines an index above nine rather than widen its reach.
|
|
39
|
+
# `Scope#forget_match_globals` clears `$10` and above too (#1384), so a binding there is never stale.
|
|
40
40
|
module MatchDataFolding
|
|
41
41
|
MAX_GROUP_GLOBAL = 9
|
|
42
42
|
private_constant :MAX_GROUP_GLOBAL
|
|
@@ -12,9 +12,9 @@ module Rigor
|
|
|
12
12
|
# The member-shape projections shared by {DataFolding} and {StructFolding}. A `DataInstance` and a
|
|
13
13
|
# `StructInstance` expose the same surface — an ordered `members` map, `member_names`, and a
|
|
14
14
|
# `class_name` — so the value projections off that surface (`[]` / `to_h` / `deconstruct` /
|
|
15
|
-
# `deconstruct_keys` / `members`
|
|
16
|
-
#
|
|
17
|
-
#
|
|
15
|
+
# `deconstruct_keys` / `members`) and the reader-redefinition guard are identical between the two
|
|
16
|
+
# folders. `#with` is a projection of `Data` only (`Struct` has no `#with`, #1335); it takes a block that
|
|
17
|
+
# builds the new instance from the merged member map, so the carrier constructor stays with the caller.
|
|
18
18
|
#
|
|
19
19
|
# Both folders `extend` this module so the projections resolve as their own module functions
|
|
20
20
|
# (matching their `module_function` style).
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# Issue #1393 — `min` / `max` / `min_by` / `max_by` take an OPTIONAL count, and Ruby reads a `nil` count as
|
|
9
|
+
# "no count" (except blockless `Range#min` / `Range#max`, which raise; those are left alone):
|
|
10
|
+
# `[1, 2].min_by(nil) { |s| s }` is `1`, not `[1]`, and `[1, 2].max(nil)` is `2`. The RBS
|
|
11
|
+
# declares only `() ...` and `(Integer) ...` arms, so a `nil` argument matched none of them and the selector
|
|
12
|
+
# fell back to the blockless `Enumerator` arm — a confidently wrong type, and a false `call.undefined-method`
|
|
13
|
+
# on `xs.min_by(limit) { … }.succ` where `limit` may be `nil`.
|
|
14
|
+
#
|
|
15
|
+
# This splits the count argument. A `nil` part is dispatched as the no-argument call (so every no-count
|
|
16
|
+
# answer, including the #1333 non-empty fold, applies); a non-`nil` part is dispatched as written; a union
|
|
17
|
+
# of both answers the union of the two results. A count that has no `nil` part is left to the caller
|
|
18
|
+
# untouched. Only the core receivers whose methods these are (`Array`, `Hash`, `Range`, `Set`, and their
|
|
19
|
+
# shape and constant carriers) are rewritten, so a user class's own `min(limit)` is not reinterpreted.
|
|
20
|
+
module OptionalCount
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
METHODS = Set[:min, :max, :min_by, :max_by].freeze
|
|
24
|
+
# `Range` defines its own blockless `min` / `max`, which raise `TypeError` for a `nil` count in Ruby 4.0;
|
|
25
|
+
# only with a block does it fall through to `Enumerable`'s, which read `nil` as no count.
|
|
26
|
+
COUNTED_BY_ENUMERABLE_ONLY = Set[:min, :max].freeze
|
|
27
|
+
RECEIVER_CLASSES = Set["Array", "Hash", "Range", "Set"].freeze
|
|
28
|
+
|
|
29
|
+
# @return nil when the call is not a nil-count call; otherwise a one-element list holding the non-`nil`
|
|
30
|
+
# part of the count (itself nil when the count is only `nil`).
|
|
31
|
+
def split(receiver, method_name, args, block: true)
|
|
32
|
+
return nil unless METHODS.include?(method_name) && args.size == 1 && core_receiver?(receiver)
|
|
33
|
+
return nil if !block && COUNTED_BY_ENUMERABLE_ONLY.include?(method_name) && range_receiver?(receiver)
|
|
34
|
+
|
|
35
|
+
arg = args.first
|
|
36
|
+
members = arg.is_a?(Type::Union) ? arg.members : [arg]
|
|
37
|
+
return nil unless members.any? { |m| nil_constant?(m) }
|
|
38
|
+
|
|
39
|
+
rest = members.reject { |m| nil_constant?(m) }
|
|
40
|
+
[rest.empty? ? nil : Type::Combinator.union(*rest)]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def range_receiver?(receiver)
|
|
44
|
+
(receiver.is_a?(Type::Constant) && receiver.value.is_a?(Range)) ||
|
|
45
|
+
(receiver.is_a?(Type::Nominal) && receiver.class_name == "Range")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def nil_constant?(type)
|
|
49
|
+
type.is_a?(Type::Constant) && type.value.nil?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def core_receiver?(receiver)
|
|
53
|
+
case receiver
|
|
54
|
+
when Type::Tuple, Type::HashShape then true
|
|
55
|
+
when Type::Constant then receiver.value.is_a?(Range) || receiver.value.is_a?(::Set)
|
|
56
|
+
when Type::Nominal then RECEIVER_CLASSES.include?(receiver.class_name)
|
|
57
|
+
else false
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -1019,7 +1019,7 @@ module Rigor
|
|
|
1019
1019
|
self_type = self_type_override ||
|
|
1020
1020
|
(kind == :singleton ? Type::Combinator.singleton_of(class_name) : instance_type)
|
|
1021
1021
|
|
|
1022
|
-
keywords_last =
|
|
1022
|
+
keywords_last = ArgumentNodes.keyword_hash_last?(call_node, args)
|
|
1023
1023
|
call_site = [class_name, method_name, kind]
|
|
1024
1024
|
if keywords_last && args.last.is_a?(Type::HashShape)
|
|
1025
1025
|
distributed = distributed_keyword_return(method_definition, self_type, instance_type, type_vars, args,
|
|
@@ -1129,21 +1129,9 @@ module Rigor
|
|
|
1129
1129
|
dynamic_value && !joined.is_a?(Type::Dynamic) ? Type::Combinator.dynamic(joined) : joined
|
|
1130
1130
|
end
|
|
1131
1131
|
|
|
1132
|
-
# Issue #1727 — whether `args`' last entry is the call's keyword hash (`f(a, k: 1)`), which the overload
|
|
1133
|
-
# selector then matches against an overload's keywords. Only the AST tells a keyword hash from a
|
|
1134
|
-
# braced positional one (`f(a, { k: 1 })`), so a dispatch without the call node, or one whose
|
|
1135
|
-
# arguments are not the node's own (a forwarded `...`, a `send` re-dispatch), answers false and keeps
|
|
1136
|
-
# reading every argument positionally.
|
|
1137
|
-
def keyword_arguments_last?(call_node, args)
|
|
1138
|
-
return false if args.empty? || !call_node.respond_to?(:arguments)
|
|
1139
|
-
|
|
1140
|
-
arguments = call_node.arguments&.arguments
|
|
1141
|
-
!arguments.nil? && arguments.size == args.size && arguments.last.is_a?(Prism::KeywordHashNode)
|
|
1142
|
-
end
|
|
1143
|
-
|
|
1144
1132
|
# Issue #1801 — the indices of `args` the call passes as splat arguments (`f(*xs, k: 1)`), whose positional
|
|
1145
|
-
# count is not known statically, or nil without one. As for {
|
|
1146
|
-
# node's arguments are exactly `args` answers.
|
|
1133
|
+
# count is not known statically, or nil without one. As for {ArgumentNodes.keyword_hash_last?}, only a call
|
|
1134
|
+
# whose node's arguments are exactly `args` answers.
|
|
1147
1135
|
def splat_indices(call_node, args)
|
|
1148
1136
|
return nil if args.empty? || !call_node.respond_to?(:arguments)
|
|
1149
1137
|
|
|
@@ -1729,7 +1717,7 @@ module Rigor
|
|
|
1729
1717
|
|
|
1730
1718
|
def probe_block_param_types(receiver:, method_name:, args:, environment:, scope: nil, call_node: nil)
|
|
1731
1719
|
args ||= []
|
|
1732
|
-
keywords_last =
|
|
1720
|
+
keywords_last = ArgumentNodes.keyword_hash_last?(call_node, args)
|
|
1733
1721
|
splat = splat_indices(call_node, args)
|
|
1734
1722
|
case receiver
|
|
1735
1723
|
when Type::Union
|