rigortype 0.4.2 → 0.4.4
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/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +28 -5
- data/docs/manual/02-cli-reference.md +23 -4
- data/docs/manual/03-configuration.md +21 -8
- data/docs/manual/04-diagnostics.md +44 -2
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +32 -3
- 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 +26 -9
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/rule_ids.rb +64 -0
- data/lib/rigor/analysis/check_rules.rb +68 -57
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/incremental.rb +12 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
- data/lib/rigor/analysis/incremental_session.rb +207 -40
- 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 +139 -15
- data/lib/rigor/analysis/reachability/scan.rb +374 -173
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/rule_catalog.rb +6 -1
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +6 -4
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +74 -13
- data/lib/rigor/analysis/runner/pool_coordinator.rb +133 -19
- data/lib/rigor/analysis/runner/project_pre_passes.rb +46 -1
- data/lib/rigor/analysis/runner.rb +62 -18
- data/lib/rigor/analysis/severity_stamp.rb +34 -1
- data/lib/rigor/analysis/worker_session.rb +58 -2
- data/lib/rigor/cache/descriptor.rb +40 -6
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +56 -12
- 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/mcp_command.rb +1 -1
- 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/config_audit.rb +111 -15
- data/lib/rigor/configuration/dependencies.rb +19 -11
- 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 +79 -10
- data/lib/rigor/environment/required_features.rb +4 -2
- data/lib/rigor/environment.rb +12 -5
- data/lib/rigor/inference/block_call_timing.rb +5 -0
- data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
- data/lib/rigor/inference/captured_locals.rb +69 -6
- data/lib/rigor/inference/definer_resolution.rb +16 -7
- data/lib/rigor/inference/expression_typer.rb +117 -29
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- 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/module_function_state.rb +80 -13
- data/lib/rigor/inference/narrowing.rb +184 -68
- 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/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +416 -1
- data/lib/rigor/inference/scope_indexer.rb +565 -61
- data/lib/rigor/inference/self_call_ivar_writes.rb +341 -0
- data/lib/rigor/inference/statement_evaluator.rb +307 -60
- 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 +133 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +802 -0
- data/lib/rigor/lens/members.rb +203 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +322 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +41 -0
- data/lib/rigor/mcp/server.rb +61 -20
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/project_environment.rb +23 -1
- data/lib/rigor/protection/mutation_cache.rb +2 -1
- data/lib/rigor/scope.rb +33 -17
- data/lib/rigor/sig_gen/generator.rb +45 -5
- data/lib/rigor/sig_gen/layout_index.rb +36 -0
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/writer.rb +47 -34
- 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/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +28 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- 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 +21 -1
|
@@ -39,6 +39,9 @@ module Rigor
|
|
|
39
39
|
Hit = Data.define(:answer, :owner, :index, :side)
|
|
40
40
|
|
|
41
41
|
QUESTIONS = %i[definer visibility arity override own_side].freeze
|
|
42
|
+
# The questions whose answer reads a recorded visibility: only they treat a contested visibility slot as a
|
|
43
|
+
# possible definer ({#possible?}).
|
|
44
|
+
VISIBILITY_QUESTIONS = %i[visibility override].freeze
|
|
42
45
|
NONE = :rigor_definer_resolution_none
|
|
43
46
|
STRAY = :rigor_definer_resolution_stray
|
|
44
47
|
private_constant :NONE, :STRAY
|
|
@@ -58,12 +61,13 @@ module Rigor
|
|
|
58
61
|
flavor = question == :arity ? :arity : :methods
|
|
59
62
|
chain = Scope::ResolutionChain.for(scope, class_name.to_s, side, flavor)
|
|
60
63
|
|
|
61
|
-
|
|
64
|
+
reads_visibility = VISIBILITY_QUESTIONS.include?(question)
|
|
65
|
+
hits = candidates(scope, chain, method_name, from, answer_in, reads_visibility)
|
|
62
66
|
return UNKNOWN if hits.nil?
|
|
63
67
|
|
|
64
68
|
verdict = chain.settle(scope, outcomes(hits), owner: hits.first&.owner, unknown_for: method_name) do |retro|
|
|
65
69
|
retro_from = retro_position(chain, retro, from)
|
|
66
|
-
outcomes(retro_from && candidates(scope, retro, method_name, retro_from, answer_in))
|
|
70
|
+
outcomes(retro_from && candidates(scope, retro, method_name, retro_from, answer_in, reads_visibility))
|
|
67
71
|
end
|
|
68
72
|
return UNKNOWN unless verdict == :chain
|
|
69
73
|
return UNKNOWN if side == :singleton && SingletonHookDecline.decline?(scope, chain, method_name, from, hits)
|
|
@@ -101,7 +105,7 @@ module Rigor
|
|
|
101
105
|
# follows (the absent candidate). A cut chain, and a chain on which an external ancestor ahead of the
|
|
102
106
|
# candidate (or, with no candidate, the implicit `Object`) may define the name, is a decline, answered by
|
|
103
107
|
# nil instead of a set.
|
|
104
|
-
def candidates(scope, chain, method_name, from, answer_in)
|
|
108
|
+
def candidates(scope, chain, method_name, from, answer_in, reads_visibility)
|
|
105
109
|
hits = []
|
|
106
110
|
position = from
|
|
107
111
|
loop do
|
|
@@ -113,7 +117,9 @@ module Rigor
|
|
|
113
117
|
return nil if hit.nil? && chain.truncated?
|
|
114
118
|
|
|
115
119
|
hits << hit
|
|
116
|
-
|
|
120
|
+
if hit.nil? || !possible?(scope, hit, method_name, reads_visibility)
|
|
121
|
+
return decided(scope, chain, method_name, from, hits)
|
|
122
|
+
end
|
|
117
123
|
|
|
118
124
|
position = hit.index + 1
|
|
119
125
|
end
|
|
@@ -188,15 +194,18 @@ module Rigor
|
|
|
188
194
|
end
|
|
189
195
|
|
|
190
196
|
# A definer that rests on a fact the walk could not prove executes: the member's `possible` sibling, or a
|
|
191
|
-
# contested slot of one of the single-valued members.
|
|
192
|
-
|
|
197
|
+
# contested slot of one of the single-valued members. A contested visibility slot counts only for a question
|
|
198
|
+
# that reads the visibility (`reads_visibility`): a slot is contested there for a visibility call the walk
|
|
199
|
+
# cannot place (an uncertain toggle, `private(*names)`, `private def self.x`), which leaves the definer and its
|
|
200
|
+
# `def` certain; a possible `def` or `attr_*` call contests its def-node slot or its name's `possible` side too.
|
|
201
|
+
def possible?(scope, hit, method_name, reads_visibility)
|
|
193
202
|
discovery = scope.discovery
|
|
194
203
|
owner = hit.owner
|
|
195
204
|
kind = hit.side == :singleton ? :singleton : :instance
|
|
196
205
|
nodes = kind == :singleton ? :discovered_singleton_def_nodes : :discovered_def_nodes
|
|
197
206
|
discovery.possible_method?(owner, method_name, kind) ||
|
|
198
207
|
discovery.contested?(nodes, [owner, method_name.to_sym]) ||
|
|
199
|
-
discovery.contested?(:discovered_method_visibilities, [owner, method_name.to_sym]) ||
|
|
208
|
+
(reads_visibility && discovery.contested?(:discovered_method_visibilities, [owner, method_name.to_sym])) ||
|
|
200
209
|
discovery.contested?(:discovered_parameter_envelopes, [owner, [kind, method_name.to_sym]])
|
|
201
210
|
end
|
|
202
211
|
|
|
@@ -321,6 +321,37 @@ module Rigor
|
|
|
321
321
|
try_project_singleton_inference(receiver, node, arg_types, method_name: :[])
|
|
322
322
|
end
|
|
323
323
|
|
|
324
|
+
# Issue #1889 — the type a block-form `inject` / `reduce` accumulator parameter holds across the block's runs,
|
|
325
|
+
# or nil when the call is not one {#try_block_inject_fold} folds. The RBS tier binds the parameter to the seed
|
|
326
|
+
# alone, so the statement pass and the ADR-56 write-back typed every run's accumulator as `0` for `inject(0)`,
|
|
327
|
+
# and a capture assigned from it (`m = a`) was written back as `0`.
|
|
328
|
+
#
|
|
329
|
+
# The fold records the answer as it types the call ({#record_inject_accumulator}): the constant arm the
|
|
330
|
+
# accumulator values the block receives (`0 | 1` for `[1, 2].inject(0) { |a, e| a + e }`), the nominal
|
|
331
|
+
# arm its seed joined with the converged accumulator. The statement pass types the call from the same scope
|
|
332
|
+
# just before it enters the block, so the recorded answer is read back rather than computed again — each
|
|
333
|
+
# computation types the body, and a nested `inject` multiplied that by its depth.
|
|
334
|
+
def inject_accumulator_type(call_node, receiver, arg_types)
|
|
335
|
+
block = call_node.block
|
|
336
|
+
return nil unless INJECT_METHODS.include?(call_node.name) && block.is_a?(Prism::BlockNode)
|
|
337
|
+
|
|
338
|
+
recorded = self.class.recorded_inject_accumulator(block, scope)
|
|
339
|
+
return recorded if recorded
|
|
340
|
+
|
|
341
|
+
try_block_inject_fold(call_node, receiver, arg_types)
|
|
342
|
+
self.class.recorded_inject_accumulator(block, scope)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
INJECT_ACCUMULATOR_KEY = :__rigor_inject_accumulator__
|
|
346
|
+
private_constant :INJECT_ACCUMULATOR_KEY
|
|
347
|
+
|
|
348
|
+
# The accumulator type {#record_inject_accumulator} stored for `block` when the call was typed from `scope`
|
|
349
|
+
# itself (by identity), else nil.
|
|
350
|
+
def self.recorded_inject_accumulator(block, scope)
|
|
351
|
+
slot = Thread.current[INJECT_ACCUMULATOR_KEY]&.[](block)
|
|
352
|
+
slot && slot[0].equal?(scope) ? slot[1] : nil
|
|
353
|
+
end
|
|
354
|
+
|
|
324
355
|
# ADR-89 WD2 — the current run's return memo bucket as `{ def_node => [MemoEntry, …] }` (only entries
|
|
325
356
|
# that carry a call descriptor, i.e. every stored entry). Read by the incremental session right after a
|
|
326
357
|
# recording run to harvest each analyzed callee's observed call keys → return descriptors. Returns an
|
|
@@ -1047,12 +1078,15 @@ module Rigor
|
|
|
1047
1078
|
#
|
|
1048
1079
|
# The `case ... in` pattern-matching form (`CaseMatchNode`) and the predicate-less form (`case; when
|
|
1049
1080
|
# c1; ...`) bypass the `===` analysis: pattern matching has richer semantics, and a predicate-less
|
|
1050
|
-
# `case` reduces to a `if c1; ...; elsif c2` chain
|
|
1081
|
+
# `case` reduces to a `if c1; ...; elsif c2` chain. Issue #1495 — like {#type_of_conditional}, that
|
|
1082
|
+
# chain delegates to the statement evaluator, which narrows each arm by its conditions' edges, so
|
|
1083
|
+
# `[case; when x.nil? then "n"; else x end]` reads `"n" | String` as the statement form does.
|
|
1051
1084
|
# Issue #1429 — each arm is typed under the subject's clause narrowing (`Narrowing.case_when_scopes`), the
|
|
1052
1085
|
# scope the statement evaluator runs the arm in: `when Symbol then n` on `n: Integer | Symbol` answers
|
|
1053
1086
|
# `Symbol`, and the `else` arm reads the subject every earlier clause has ruled out.
|
|
1054
1087
|
def type_of_case(node)
|
|
1055
|
-
return type_of_case_simple_union(node) if node.is_a?(Prism::CaseMatchNode)
|
|
1088
|
+
return type_of_case_simple_union(node) if node.is_a?(Prism::CaseMatchNode)
|
|
1089
|
+
return scope.evaluate(node, tracer: tracer).first if node.predicate.nil?
|
|
1056
1090
|
|
|
1057
1091
|
subject_type = type_of(node.predicate)
|
|
1058
1092
|
candidates = []
|
|
@@ -1952,19 +1986,20 @@ module Rigor
|
|
|
1952
1986
|
# replaces (a refined `String#upcase` must not fold, a refined `map` must not fold per element, a top-level
|
|
1953
1987
|
# `def` must not bind ahead of a refinement of `Object`, a plugin models the class's own method). An unreadable
|
|
1954
1988
|
# 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
|
-
#
|
|
1989
|
+
# A union receiver is decided per member; a `Dynamic` receiver and a call no refinement wins fall through. A
|
|
1990
|
+
# class object walks its own lookup (issue #1752): under `refine(Object) { def oo = :o }` a refinement that wins
|
|
1991
|
+
# `Integer.oo` answers `Dynamic[top]` ({#refined_class_object_result}). A call site no refinement is in effect
|
|
1992
|
+
# at answers nil at once and records nothing: its answer cannot change without an edit to its own file.
|
|
1958
1993
|
#
|
|
1959
1994
|
# 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
|
-
#
|
|
1995
|
+
# ({InEffectRefinements.opaque_module?}), any method of any object may be replaced, so every call on an instance
|
|
1996
|
+
# or a class object answers `Dynamic[top]`.
|
|
1962
1997
|
def try_refined_dispatch(node, receiver, arg_types, block_type)
|
|
1963
1998
|
list = scope.in_effect_refinements(node)
|
|
1964
1999
|
return nil if list.empty?
|
|
1965
2000
|
|
|
1966
2001
|
InEffectRefinements.record_refinement_names(node.name)
|
|
1967
|
-
return dynamic_top if
|
|
2002
|
+
return dynamic_top if refined_receiver?(receiver) && opaque_refinement_span?(node, list)
|
|
1968
2003
|
return nil if scope.discovered_refinements.empty?
|
|
1969
2004
|
|
|
1970
2005
|
refined_indirect_result(node, receiver, list) ||
|
|
@@ -2010,6 +2045,9 @@ module Rigor
|
|
|
2010
2045
|
def refined_name_for?(receiver, name, list)
|
|
2011
2046
|
members = receiver.is_a?(Type::Union) ? receiver.members : [receiver]
|
|
2012
2047
|
members.any? do |member|
|
|
2048
|
+
next !RefinedDispatch.winner(scope, member.class_name, name, list, kind: :singleton).nil? if
|
|
2049
|
+
member.is_a?(Type::Singleton)
|
|
2050
|
+
|
|
2013
2051
|
class_name = refined_receiver_class_name(member)
|
|
2014
2052
|
!class_name.nil? && !RefinedDispatch.winner(scope, class_name, name, list).nil?
|
|
2015
2053
|
end
|
|
@@ -2033,11 +2071,11 @@ module Rigor
|
|
|
2033
2071
|
InEffectRefinements.opaque_in?(scope, lexical)
|
|
2034
2072
|
end
|
|
2035
2073
|
|
|
2036
|
-
# Does `receiver` (or a member of a union) have a
|
|
2037
|
-
# object
|
|
2038
|
-
def
|
|
2074
|
+
# Does `receiver` (or a member of a union) have a lookup the refined arm answers for: an instance of a class,
|
|
2075
|
+
# or a class object (issue #1752), not `Dynamic`?
|
|
2076
|
+
def refined_receiver?(receiver)
|
|
2039
2077
|
members = receiver.is_a?(Type::Union) ? receiver.members : [receiver]
|
|
2040
|
-
members.any? { |member| !refined_receiver_class_name(member).nil? }
|
|
2078
|
+
members.any? { |member| member.is_a?(Type::Singleton) || !refined_receiver_class_name(member).nil? }
|
|
2041
2079
|
end
|
|
2042
2080
|
|
|
2043
2081
|
def refined_receiver_result(node, receiver, list, arg_types, block_type)
|
|
@@ -2053,6 +2091,9 @@ module Rigor
|
|
|
2053
2091
|
end
|
|
2054
2092
|
|
|
2055
2093
|
def refined_member_result(node, receiver, list, arg_types, block_type)
|
|
2094
|
+
return refined_class_object_result(node, receiver, list, arg_types, block_type) if
|
|
2095
|
+
receiver.is_a?(Type::Singleton)
|
|
2096
|
+
|
|
2056
2097
|
class_name = refined_receiver_class_name(receiver)
|
|
2057
2098
|
return nil if class_name.nil?
|
|
2058
2099
|
|
|
@@ -2064,6 +2105,16 @@ module Rigor
|
|
|
2064
2105
|
dynamic_top
|
|
2065
2106
|
end
|
|
2066
2107
|
|
|
2108
|
+
# Issue #1752 — a call on a class object, walked through its singleton levels. A refinement that wins answers
|
|
2109
|
+
# `Dynamic[top]`, never its refine body's type: a hook on any ancestor (an RBS-declared `include Singleton`, a
|
|
2110
|
+
# gem concern, `define_singleton_method(:included)`, a `Module#included` patch) can install the class object's
|
|
2111
|
+
# own method where no table sees it, and the body's type then fires downstream on correct code. A walk that finds
|
|
2112
|
+
# no winner keeps the class object's own answer (`Integer.sqrt(16)` stays `Integer`).
|
|
2113
|
+
def refined_class_object_result(node, receiver, list, _arg_types, _block_type)
|
|
2114
|
+
winner = RefinedDispatch.winner(scope, receiver.class_name, node.name, list, kind: :singleton)
|
|
2115
|
+
dynamic_top unless winner.nil?
|
|
2116
|
+
end
|
|
2117
|
+
|
|
2067
2118
|
# The winning refine body's inferred return, re-typed with the receiver's class as `self` and the call's
|
|
2068
2119
|
# argument types bound as an undeclared method's are; nil when the body is not readable here.
|
|
2069
2120
|
#
|
|
@@ -2082,8 +2133,9 @@ module Rigor
|
|
|
2082
2133
|
nil
|
|
2083
2134
|
end
|
|
2084
2135
|
|
|
2085
|
-
# The class an instance-side receiver is an instance of, or nil for one
|
|
2086
|
-
# class object (
|
|
2136
|
+
# The class an instance-side receiver is an instance of, or nil for one this instance walk does not answer: a
|
|
2137
|
+
# class object (it walks its own lookup, {#refined_class_object_result}), `Dynamic`, `top`, or a shape with no
|
|
2138
|
+
# class.
|
|
2087
2139
|
def refined_receiver_class_name(receiver)
|
|
2088
2140
|
case receiver
|
|
2089
2141
|
when Type::Nominal, Type::StructInstance, Type::DataInstance then receiver.class_name
|
|
@@ -2453,7 +2505,7 @@ module Rigor
|
|
|
2453
2505
|
return every_definer_raises_type(raising, receiver, call_node, arg_types, block_type) if def_node.nil?
|
|
2454
2506
|
|
|
2455
2507
|
result = infer_user_method_return(def_node, receiver, arg_types,
|
|
2456
|
-
self_fold_safe: fold_safe_call_receiver?(call_node
|
|
2508
|
+
self_fold_safe: fold_safe_call_receiver?(call_node),
|
|
2457
2509
|
yield_type: block_type)
|
|
2458
2510
|
return result if result.nil?
|
|
2459
2511
|
|
|
@@ -2488,11 +2540,11 @@ module Rigor
|
|
|
2488
2540
|
# This is only a property of the receiver expression; whether the grant is actually issued also needs
|
|
2489
2541
|
# a `StructInstance` carrier and a body that survives the self-use scan, both decided in
|
|
2490
2542
|
# {#build_user_method_body_scope}.
|
|
2491
|
-
def fold_safe_call_receiver?(call_node
|
|
2543
|
+
def fold_safe_call_receiver?(call_node)
|
|
2492
2544
|
return false if call_node.nil?
|
|
2493
2545
|
|
|
2494
2546
|
case call_node.receiver
|
|
2495
|
-
when Prism::CallNode then materialization_call?(call_node.receiver
|
|
2547
|
+
when Prism::CallNode then materialization_call?(call_node.receiver)
|
|
2496
2548
|
when Prism::LocalVariableReadNode then scope.struct_fold_safe?(call_node.receiver.name)
|
|
2497
2549
|
when nil, Prism::SelfNode then scope.struct_fold_safe?(:self)
|
|
2498
2550
|
else false
|
|
@@ -2502,10 +2554,10 @@ module Rigor
|
|
|
2502
2554
|
# Issue #595 — the materialisation test lives ONCE, in
|
|
2503
2555
|
# [`struct_materialization.rb`](method_dispatcher/struct_materialization.rb), because the direct
|
|
2504
2556
|
# 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,
|
|
2557
|
+
# see {MethodDispatcher::StructMaterialization.materialization_call?}. A `.with` call is never one
|
|
2558
|
+
# (`Struct` has no `#with`, #1335), so the test needs no lookup from either consumer.
|
|
2559
|
+
def materialization_call?(node)
|
|
2560
|
+
MethodDispatcher::StructMaterialization.materialization_call?(node, scope)
|
|
2509
2561
|
end
|
|
2510
2562
|
|
|
2511
2563
|
# Module-singleton call resolution (ADR-57 follow-up) — resolves `Foo.<name>` on a `Singleton[Foo]`
|
|
@@ -2674,7 +2726,7 @@ module Rigor
|
|
|
2674
2726
|
def every_definer_raises_type(defs, receiver, call_node, arg_types, block_type)
|
|
2675
2727
|
return nil if defs.nil?
|
|
2676
2728
|
|
|
2677
|
-
self_fold_safe = fold_safe_call_receiver?(call_node
|
|
2729
|
+
self_fold_safe = fold_safe_call_receiver?(call_node)
|
|
2678
2730
|
defs.each do |def_node|
|
|
2679
2731
|
result = infer_user_method_return(def_node, receiver, arg_types,
|
|
2680
2732
|
self_fold_safe: self_fold_safe, yield_type: block_type)
|
|
@@ -3865,6 +3917,12 @@ module Rigor
|
|
|
3865
3917
|
# default — or `Dynamic` when the shape is open or absent; `**kwrest`, `&block`, and destructured
|
|
3866
3918
|
# positionals bind `Dynamic`. Binding `Dynamic` is monotone-safe (wider in → wider out), so the only
|
|
3867
3919
|
# declines left are the shapes where positional CORRESPONDENCE itself is unknowable.
|
|
3920
|
+
#
|
|
3921
|
+
# The scope carries no `source_path:`: the body is re-typed for call sites in any file, so a rule that
|
|
3922
|
+
# asks "did this file write it?" by comparing `scope.source_path` with census paths answers wrongly
|
|
3923
|
+
# through it. Record a per-file write kind in the census instead, as {ScopeIndexer::CONSTANT_MEMO} does
|
|
3924
|
+
# (#1276). Setting `source_path` here was rejected: plugins read it, and the return memo would become
|
|
3925
|
+
# caller-dependent.
|
|
3868
3926
|
def build_user_method_body_scope(def_node, receiver, arg_types, self_fold_safe: false)
|
|
3869
3927
|
params = def_node.parameters
|
|
3870
3928
|
locals = bind_params_from_call_types(params, arg_types)
|
|
@@ -3875,7 +3933,7 @@ module Rigor
|
|
|
3875
3933
|
# allocated a fresh frozen Scope per field, run per user-method-call inference (ADR-44). The
|
|
3876
3934
|
# discovery index is inherited whole by reference (ADR-53 Track A); the hand-copied per-field list
|
|
3877
3935
|
# this replaces had silently dropped `data_member_layouts` and `discovered_method_visibilities`.
|
|
3878
|
-
Scope.new(
|
|
3936
|
+
body_scope = Scope.new(
|
|
3879
3937
|
environment: scope.environment,
|
|
3880
3938
|
locals: locals.freeze,
|
|
3881
3939
|
self_type: receiver,
|
|
@@ -3893,6 +3951,7 @@ module Rigor
|
|
|
3893
3951
|
# Issue #1358 — the callee runs in a frame of its own ({Scope#with_match_frame}).
|
|
3894
3952
|
match_frame: MatchRebinding::Frame.new(def_node.body, def_node.parameters)
|
|
3895
3953
|
)
|
|
3954
|
+
body_scope.with_repeated_or_writes(RepeatedOrWrites.straight_line_sites(def_node.body, body_scope))
|
|
3896
3955
|
end
|
|
3897
3956
|
|
|
3898
3957
|
# Issue #681 / #707 — the ONE place in `lib/` that answers "what `Module.nesting` was recorded for this
|
|
@@ -5417,13 +5476,39 @@ module Rigor
|
|
|
5417
5476
|
acc, rest = inject_constant_start(members, seed, has_seed)
|
|
5418
5477
|
return nil if acc.nil?
|
|
5419
5478
|
|
|
5420
|
-
rest.
|
|
5421
|
-
|
|
5479
|
+
captured = inject_captured_bindings(block, rest.map { |value| Type::Combinator.constant_of(value) })
|
|
5480
|
+
received = [acc]
|
|
5481
|
+
rest.each_with_index do |element_value, position|
|
|
5482
|
+
acc = inject_constant_step(block, acc, element_value, captured, position)
|
|
5422
5483
|
return nil if acc.nil?
|
|
5484
|
+
|
|
5485
|
+
received << acc
|
|
5423
5486
|
end
|
|
5487
|
+
received.pop if received.size > 1
|
|
5488
|
+
record_inject_accumulator(block, Type::Combinator.union(*received))
|
|
5424
5489
|
acc
|
|
5425
5490
|
end
|
|
5426
5491
|
|
|
5492
|
+
# Records `type` as the accumulator `block` receives when typed from this typer's scope
|
|
5493
|
+
# ({.recorded_inject_accumulator}). The keys are weak, so a finished file's nodes do not stay reachable, and
|
|
5494
|
+
# the values strong, so a record outlives a collection while its node does. Prism nodes compare by identity.
|
|
5495
|
+
def record_inject_accumulator(block, type)
|
|
5496
|
+
(Thread.current[INJECT_ACCUMULATOR_KEY] ||= ObjectSpace::WeakKeyMap.new)[block] = [scope, type].freeze
|
|
5497
|
+
end
|
|
5498
|
+
|
|
5499
|
+
# Issue #1332 — the #587 (b) captured-block binding ({#captured_block_bindings}) both inject arms lay under
|
|
5500
|
+
# every step, as the Tuple / Range per-element folds and the per-pair `HashShape` fold do. Without it each
|
|
5501
|
+
# step reads a captured variable at its entry binding, so a body that rebinds or mutates one (`n += 1`,
|
|
5502
|
+
# `log << e`, `cache[:k] ||= e`) pinned the result: `n = 0; [1, 2].inject(0) { |a, e| r = a + n; n += 1;
|
|
5503
|
+
# r }` folded to `0` where Ruby answers `1`. The fixpoint binds the accumulator parameter to `Dynamic[top]`
|
|
5504
|
+
# rather than to a type the inject fixpoint has not converged yet: a captured name that reads the
|
|
5505
|
+
# accumulator widens rather than pins. `element_types` is the constant arm's steps, one position per block
|
|
5506
|
+
# run, which {RepeatedOrWrites} reads; nil for the nominal arm, which marks every repeated site.
|
|
5507
|
+
def inject_captured_bindings(block, element_types, element: nil)
|
|
5508
|
+
element ||= Type::Combinator.union(*element_types)
|
|
5509
|
+
captured_block_bindings(block, [Type::Combinator.untyped, element], element_types: element_types)
|
|
5510
|
+
end
|
|
5511
|
+
|
|
5427
5512
|
# Extracts the receiver's foldable constant values, size-capped before enumeration, or nil to decline.
|
|
5428
5513
|
def inject_constant_members(receiver)
|
|
5429
5514
|
case receiver
|
|
@@ -5475,9 +5560,9 @@ module Rigor
|
|
|
5475
5560
|
# Evaluates the block body once with the running constant accumulator + the next constant element
|
|
5476
5561
|
# bound to the block params, returning the result when it is a foldable `Constant` within the
|
|
5477
5562
|
# magnitude cap, else nil to decline the whole fold.
|
|
5478
|
-
def inject_constant_step(block, acc, element_value)
|
|
5563
|
+
def inject_constant_step(block, acc, element_value, captured, position)
|
|
5479
5564
|
element = Type::Combinator.constant_of(element_value)
|
|
5480
|
-
result = type_block_body_with_param(block, [acc, element])
|
|
5565
|
+
result = type_block_body_with_param(block, [acc, element], captured: captured, position: position)
|
|
5481
5566
|
return nil unless result.is_a?(Type::Constant)
|
|
5482
5567
|
return nil unless inject_foldable?(result.value)
|
|
5483
5568
|
return nil if inject_magnitude_too_large?(result.value)
|
|
@@ -5507,17 +5592,20 @@ module Rigor
|
|
|
5507
5592
|
seed_acc = has_seed ? seed : element
|
|
5508
5593
|
return nil if seed_acc.nil?
|
|
5509
5594
|
|
|
5595
|
+
captured = inject_captured_bindings(block, nil, element: element)
|
|
5510
5596
|
converged = BodyFixpoint.converge(
|
|
5511
5597
|
names: [:__inject_acc__],
|
|
5512
5598
|
seed_bindings: { __inject_acc__: seed_acc },
|
|
5513
5599
|
widen: method(:widen_value_pinned),
|
|
5514
5600
|
evaluate_body: lambda do |bindings|
|
|
5515
5601
|
acc = bindings[:__inject_acc__]
|
|
5516
|
-
result = type_block_body_with_param(block, [acc, element])
|
|
5602
|
+
result = type_block_body_with_param(block, [acc, element], captured: captured)
|
|
5517
5603
|
result.nil? ? {} : { __inject_acc__: result }
|
|
5518
5604
|
end
|
|
5519
5605
|
)
|
|
5520
|
-
converged[:__inject_acc__] || seed_acc
|
|
5606
|
+
result = converged[:__inject_acc__] || seed_acc
|
|
5607
|
+
record_inject_accumulator(block, Type::Combinator.union(seed_acc, result))
|
|
5608
|
+
result
|
|
5521
5609
|
end
|
|
5522
5610
|
|
|
5523
5611
|
def assemble_per_element_result(method_name, per_position, element_types)
|
|
@@ -223,6 +223,12 @@ module Rigor
|
|
|
223
223
|
end || Type::Combinator.untyped
|
|
224
224
|
end
|
|
225
225
|
|
|
226
|
+
# Issue #1596 — true when `name` runs code chosen by name or by a String, or a block or String it is given
|
|
227
|
+
# ({CODE_RUNNING_NAMES}, {BLOCK_OR_CODE_NAMES}), whatever its receiver.
|
|
228
|
+
def code_running_name?(name)
|
|
229
|
+
CODE_RUNNING_NAMES.include?(name) || BLOCK_OR_CODE_NAMES.include?(name)
|
|
230
|
+
end
|
|
231
|
+
|
|
226
232
|
# True when `method_name` on a value of `type` may run foreign code ({.foreign_target?}).
|
|
227
233
|
def type_method_foreign?(type, method_name, scope)
|
|
228
234
|
targets = ProjectMethodOwnership.targets(type)
|
|
@@ -326,8 +332,7 @@ module Rigor
|
|
|
326
332
|
private_class_method :scan, :guarded_ivar_write?, :scan_call, :receiver_targets, :foreign_target?,
|
|
327
333
|
:universal_delegate_foreign?, :method_owner, :deferred_block_call?,
|
|
328
334
|
:compound_write_foreign?,
|
|
329
|
-
:compound_receiver_type, :compound_accessors, :compound_read_type, :variable_type
|
|
330
|
-
:type_method_foreign?
|
|
335
|
+
:compound_receiver_type, :compound_accessors, :compound_read_type, :variable_type
|
|
331
336
|
end
|
|
332
337
|
|
|
333
338
|
module GuardRebinding
|
|
@@ -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
|
|
|
@@ -45,7 +45,7 @@ module Rigor
|
|
|
45
45
|
# `X.send(:define_method, :m, blk)`), whose body may be a block the file wrote anywhere, an eval of a
|
|
46
46
|
# String, whose code may do any of these, and a `const_set`, which may make a class a `class` body reopens;
|
|
47
47
|
# - one name: a `def` of it anywhere (`def self.gets` at the top level defines it on `main`, which
|
|
48
|
-
# {BlockCallTiming.project_defines_anywhere?} does not
|
|
48
|
+
# {BlockCallTiming.project_defines_anywhere?} does not count), and a Symbol or String literal naming it,
|
|
49
49
|
# which a macro may define it through (`def_delegators :@io, :gets`, `attr_reader :gets`,
|
|
50
50
|
# `undef_method :gets`), other than `&:gets` and the first argument of a call that only names a method
|
|
51
51
|
# (`respond_to?(:gets)`, `send(:gets)`).
|
|
@@ -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
|