rigortype 0.4.1 → 0.4.2
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/builtins/ruby_core/io.yml +1 -0
- data/data/builtins/ruby_core/proc.yml +4 -0
- data/data/builtins/ruby_core/random.yml +4 -0
- data/data/builtins/ruby_core/struct.yml +3 -0
- data/data/core_overlay/env.rbs +34 -0
- data/data/core_overlay/gc.rbs +50 -0
- data/data/core_overlay/hash.rbs +45 -0
- data/data/core_overlay/integer.rbs +31 -0
- data/data/core_overlay/io_buffer.rbs +27 -0
- data/data/core_overlay/kernel.rbs +40 -0
- data/data/core_overlay/match_data.rbs +28 -0
- data/data/core_overlay/module.rbs +51 -0
- data/data/core_overlay/object_space.rbs +29 -0
- data/data/core_overlay/proc.rbs +45 -0
- data/data/core_overlay/range.rbs +40 -0
- data/data/core_overlay/source_range.rbs +78 -0
- data/data/core_overlay/string.rbs +86 -0
- data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
- data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
- data/docs/handbook/01-getting-started.md +3 -3
- data/docs/handbook/06-classes.md +3 -1
- data/docs/handbook/11-sig-gen.md +6 -0
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/manual/03-configuration.md +1 -1
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/18-removing-dead-code.md +32 -2
- data/docs/manual/plugins/README.md +6 -1
- data/docs/manual/plugins/rigor-activerecord.md +21 -0
- data/docs/manual/plugins/rigor-alba.md +60 -0
- data/docs/manual/plugins/rigor-minitest.md +22 -4
- data/docs/manual/plugins/rigor-typelizer.md +71 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +289 -57
- data/lib/rigor/analysis/dependency_recorder.rb +15 -0
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
- data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
- data/lib/rigor/analysis/erb_template_detector.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +29 -9
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -1
- data/lib/rigor/analysis/incremental_session.rb +14 -1
- data/lib/rigor/analysis/reachability/graph.rb +123 -23
- data/lib/rigor/analysis/reachability/scan.rb +290 -36
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +16 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
- data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +34 -3
- data/lib/rigor/analysis/worker_session.rb +7 -4
- data/lib/rigor/cache/incremental_snapshot.rb +22 -0
- data/lib/rigor/cli/annotate_command.rb +5 -0
- data/lib/rigor/cli/baseline_command.rb +7 -18
- data/lib/rigor/cli/check_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +5 -3
- data/lib/rigor/cli/unused_command.rb +13 -7
- data/lib/rigor/cli.rb +10 -2
- data/lib/rigor/configuration/severity_profile.rb +6 -0
- data/lib/rigor/configuration.rb +46 -2
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +283 -14
- data/lib/rigor/environment/required_features.rb +206 -0
- data/lib/rigor/environment.rb +21 -3
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/def_node_resolver.rb +23 -1
- data/lib/rigor/inference/expression_typer.rb +215 -16
- data/lib/rigor/inference/global_write_census.rb +7 -4
- data/lib/rigor/inference/guard_rebinding.rb +1 -1
- data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
- data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
- data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
- data/lib/rigor/inference/in_effect_refinements.rb +256 -0
- data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
- data/lib/rigor/inference/key_presence_guard.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +59 -12
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
- data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +259 -28
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher.rb +44 -11
- data/lib/rigor/inference/module_function_state.rb +2 -1
- data/lib/rigor/inference/narrowing.rb +20 -6
- data/lib/rigor/inference/object_mixins.rb +180 -0
- data/lib/rigor/inference/project_patched_methods.rb +10 -5
- data/lib/rigor/inference/project_patched_scanner.rb +71 -3
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
- data/lib/rigor/inference/refined_dispatch.rb +272 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
- data/lib/rigor/inference/scope_indexer.rb +687 -60
- data/lib/rigor/inference/statement_evaluator.rb +71 -20
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +31 -0
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/plugin/inflector.rb +1 -1
- data/lib/rigor/plugin/io_boundary.rb +42 -3
- data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
- data/lib/rigor/project_environment.rb +29 -6
- data/lib/rigor/protection/mutation_cache.rb +1 -1
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/reflection/constant_ancestors.rb +127 -11
- data/lib/rigor/reflection/constant_path.rb +13 -2
- data/lib/rigor/reflection.rb +40 -8
- data/lib/rigor/scope/discovery_index.rb +40 -1
- data/lib/rigor/scope/resolution_chain.rb +159 -11
- data/lib/rigor/scope.rb +189 -10
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +55 -5
- data/lib/rigor/sig_gen/method_candidate.rb +2 -2
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
- data/lib/rigor/source/alias_names.rb +52 -0
- data/lib/rigor/source/literals.rb +3 -2
- data/lib/rigor/source/symbol_name.rb +20 -0
- data/lib/rigor/type/combinator.rb +34 -2
- data/lib/rigor/type/float_range.rb +4 -1
- data/lib/rigor/type/nominal.rb +6 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
- data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
- data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
- data/plugins/rigor-alba/sig/alba.rbs +4 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
- data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
- data/sig/rigor/plugin/io_boundary.rbs +3 -0
- data/sig/rigor/scope.rbs +27 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +3 -1
- metadata +66 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../type"
|
|
6
|
+
require_relative "mutation_widening"
|
|
7
|
+
require_relative "key_presence_guard/scan"
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module Inference
|
|
11
|
+
# Issue #1703 — `H.key?(k)` / `has_key?` / `include?` / `member?` with a non-literal key, followed by `H[k]`.
|
|
12
|
+
#
|
|
13
|
+
# A closed hash shape read by a computed key answers every value plus the miss `nil`
|
|
14
|
+
# (`ShapeDispatch#hash_dig_step`), so the guarded read in typelizer's
|
|
15
|
+
# `COLUMN_TYPE_MAP.key?(property.column_type) && …; COLUMN_TYPE_MAP[property.column_type].dup` kept the `nil`
|
|
16
|
+
# and its `[]=` reported `call.possible-nil-receiver`.
|
|
17
|
+
#
|
|
18
|
+
# **Fail-closed by construction.** The analysis every rule reads never records a guard: {.record} and
|
|
19
|
+
# {.guarded_read} act only inside {.with_guards}, which nothing but {.withholds_nil?} enters. When
|
|
20
|
+
# `call.possible-nil-receiver` is about to report on a file that holds a guard-shaped call, it re-walks the file
|
|
21
|
+
# once with guards on, from a scope whose side tables are private copies, and withholds the report only when the
|
|
22
|
+
# re-walk types the receiver as exactly the analysis's type without `nil`. No other type, verdict, summary or
|
|
23
|
+
# signature can change, so the guard can only remove that one report.
|
|
24
|
+
#
|
|
25
|
+
# Inside the re-walk a guard is recorded only when it is safe to rely on ({.eligible?}) and is dropped where the
|
|
26
|
+
# receiver or the key may have changed ({.invalidate_after_call}, {.invalidate_after_write}, a rebinding through
|
|
27
|
+
# `Scope#bind_local` / `#bind_ivar`, and the region check in {.guarded_read}).
|
|
28
|
+
module KeyPresenceGuard
|
|
29
|
+
# The key half of a guard's address: a local, an instance variable, or a chain of plain readers rooted at one
|
|
30
|
+
# (`prop.column_type`). `path` is `[[root_kind, root_name], method, …]`, so two spellings of one chain compare
|
|
31
|
+
# equal.
|
|
32
|
+
KeyExpr = Data.define(:path) do
|
|
33
|
+
def root = path.first
|
|
34
|
+
|
|
35
|
+
# Whether `other` reads a prefix of this chain (`prop` or `prop.column_type` for `prop.column_type`), so
|
|
36
|
+
# evaluating it re-reads this key rather than intervening.
|
|
37
|
+
def prefixed_by?(other) = path.first(other.path.size) == other.path
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# What a guard stores at its address. The fact is the entry's presence on every joined arm; the read
|
|
41
|
+
# recomputes its type from the receiver, so the value carries nothing.
|
|
42
|
+
PRESENT = Type::Combinator.constant_of(true)
|
|
43
|
+
|
|
44
|
+
PREDICATES = %i[key? has_key? include? member?].to_set.freeze
|
|
45
|
+
|
|
46
|
+
# A plain reader's name: lower-case, no operator, writer or bang.
|
|
47
|
+
READER_NAME = /\A[a-z_][a-z0-9_]*\??\z/
|
|
48
|
+
|
|
49
|
+
# Reader-shaped names that answer something new on each call or consume state, so `h.key?(q.shift)` and a later
|
|
50
|
+
# `h[q.shift]` read different keys. Every name the mutation tables know is excluded too.
|
|
51
|
+
NON_IDEMPOTENT_READERS = (
|
|
52
|
+
%i[
|
|
53
|
+
shift pop unshift push append prepend next succ pred gets read readline readlines readpartial read_nonblock
|
|
54
|
+
getc getbyte readchar readbyte each each_line each_char each_byte rand random sample shuffle tick now call
|
|
55
|
+
yield resume take take_while drop lazy to_enum enum_for peek rewind reload reset lock unlock clear
|
|
56
|
+
generate fetch_next dequeue deq enqueue enq poll receive recv accept consume
|
|
57
|
+
] + MutationWidening::SHAPE_MUTATORS.to_a
|
|
58
|
+
).to_set.freeze
|
|
59
|
+
|
|
60
|
+
STATE_KEY = :__rigor_key_presence_guard
|
|
61
|
+
CACHE_KEY = :__rigor_key_presence_guard_rewalk
|
|
62
|
+
private_constant :STATE_KEY, :CACHE_KEY
|
|
63
|
+
|
|
64
|
+
# The re-walk's state: the file's root, the end offset of each recorded guard, and the per-file scans.
|
|
65
|
+
Walk = Struct.new(:root, :guard_ends, :defs, :frozen_constants, :memo) do
|
|
66
|
+
# The innermost `def` holding `node`. The defs are collected once, in source order, so the innermost holder
|
|
67
|
+
# is the last one starting at or before the node that also ends after it.
|
|
68
|
+
def enclosing_def(node)
|
|
69
|
+
self.defs ||= collect_defs(root, [])
|
|
70
|
+
offset = node.location.start_offset
|
|
71
|
+
index = defs.bsearch_index { |d| d.location.start_offset > offset } || defs.size
|
|
72
|
+
(index - 1).downto(0) do |i|
|
|
73
|
+
candidate = defs[i]
|
|
74
|
+
return candidate if offset < candidate.location.end_offset
|
|
75
|
+
end
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def collect_defs(node, out)
|
|
80
|
+
out << node if node.is_a?(Prism::DefNode)
|
|
81
|
+
node.compact_child_nodes.each { |child| collect_defs(child, out) }
|
|
82
|
+
out
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
module_function
|
|
87
|
+
|
|
88
|
+
# Runs the block with guards recorded and read, for the re-walk of `root`. Thread-local, so it holds under a
|
|
89
|
+
# Ractor worker too.
|
|
90
|
+
def with_guards(root)
|
|
91
|
+
previous = Thread.current[STATE_KEY]
|
|
92
|
+
Thread.current[STATE_KEY] = Walk.new(root, {}, nil, nil, {})
|
|
93
|
+
yield
|
|
94
|
+
ensure
|
|
95
|
+
Thread.current[STATE_KEY] = previous
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Runs the block with guards off again: a callee's return summary, memoised for the whole run, must be the one
|
|
99
|
+
# the file's analysis reads.
|
|
100
|
+
def without_guards
|
|
101
|
+
previous = Thread.current[STATE_KEY]
|
|
102
|
+
Thread.current[STATE_KEY] = nil
|
|
103
|
+
yield
|
|
104
|
+
ensure
|
|
105
|
+
Thread.current[STATE_KEY] = previous
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def active? = !Thread.current[STATE_KEY].nil?
|
|
109
|
+
|
|
110
|
+
# Whether `call.possible-nil-receiver` should withhold its report on `call_node`, whose receiver the file's
|
|
111
|
+
# analysis types `receiver_type`: true only when the guarded re-walk types that receiver as exactly
|
|
112
|
+
# `receiver_type` without `nil`. False for a file with no guard-shaped call, while an effect or flow trace
|
|
113
|
+
# records the walk, and on any failure.
|
|
114
|
+
def withholds_nil?(call_node, receiver_type, root, scope_index)
|
|
115
|
+
return false if root.nil? || active?
|
|
116
|
+
return false if Effects::Collector.active? || FlowTracer.active?
|
|
117
|
+
|
|
118
|
+
guarded_index = rewalk(root, scope_index)
|
|
119
|
+
scope = guarded_index && guarded_index[call_node]
|
|
120
|
+
return false if scope.nil?
|
|
121
|
+
|
|
122
|
+
guarded_type = with_guards(root) { scope.type_of(call_node.receiver) }
|
|
123
|
+
expected = Narrowing.narrow_non_nil(receiver_type)
|
|
124
|
+
!expected.equal?(receiver_type) && expected == guarded_type && guarded_type != receiver_type
|
|
125
|
+
rescue StandardError
|
|
126
|
+
false
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# The guarded scope index of `root`, built once per file (the cache holds the last file only), or nil when the
|
|
130
|
+
# file holds no guard-shaped call.
|
|
131
|
+
def rewalk(root, scope_index)
|
|
132
|
+
cached = Thread.current[CACHE_KEY]
|
|
133
|
+
return cached.last if cached&.first.equal?(root)
|
|
134
|
+
|
|
135
|
+
# Cached before the walk, so a re-walk that raises is attempted once per file and then answers nil.
|
|
136
|
+
Thread.current[CACHE_KEY] = [root, nil]
|
|
137
|
+
return nil unless guard_shaped_call?(root)
|
|
138
|
+
|
|
139
|
+
base = scope_index[root]
|
|
140
|
+
index = base && with_guards(root) do
|
|
141
|
+
ScopeIndexer.index(root, default_scope: base.with_isolated_side_tables)
|
|
142
|
+
end
|
|
143
|
+
Thread.current[CACHE_KEY] = [root, index]
|
|
144
|
+
index
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def guard_shaped_call?(node)
|
|
148
|
+
if node.is_a?(Prism::CallNode) && PREDICATES.include?(node.name)
|
|
149
|
+
args = node.arguments&.arguments
|
|
150
|
+
return true if args&.size == 1 && address(node.receiver, args.first)
|
|
151
|
+
end
|
|
152
|
+
node.compact_child_nodes.any? { |child| guard_shaped_call?(child) }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The {KeyExpr} for `node`, or nil unless it is a local / ivar read or a chain of plain, idempotent readers
|
|
156
|
+
# (no argument, block or `&.`) rooted at one. A literal key is the shape path's business and answers nil.
|
|
157
|
+
def key_expr(node)
|
|
158
|
+
case node
|
|
159
|
+
when Prism::LocalVariableReadNode then KeyExpr.new(path: [[:local, node.name]].freeze)
|
|
160
|
+
when Prism::InstanceVariableReadNode then KeyExpr.new(path: [[:ivar, node.name]].freeze)
|
|
161
|
+
when Prism::CallNode
|
|
162
|
+
return nil unless plain_reader_call?(node)
|
|
163
|
+
|
|
164
|
+
inner = key_expr(node.receiver)
|
|
165
|
+
inner && KeyExpr.new(path: (inner.path + [node.name]).freeze)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def plain_reader_call?(node)
|
|
170
|
+
node.receiver && node.block.nil? && node.arguments.nil? && !node.safe_navigation? &&
|
|
171
|
+
READER_NAME.match?(node.name) && !NON_IDEMPOTENT_READERS.include?(node.name)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# The receiver half of a guard's address: a local, an instance variable or a constant read.
|
|
175
|
+
def receiver_ref(node)
|
|
176
|
+
case node
|
|
177
|
+
when Prism::LocalVariableReadNode then [:local, node.name]
|
|
178
|
+
when Prism::InstanceVariableReadNode then [:ivar, node.name]
|
|
179
|
+
when Prism::ConstantReadNode then [:const, node.name]
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# `[receiver_kind, receiver_name, KeyExpr]` for `receiver[key]` / `receiver.key?(key)`, or nil.
|
|
184
|
+
def address(receiver_node, key_node)
|
|
185
|
+
receiver = receiver_ref(receiver_node)
|
|
186
|
+
return nil if receiver.nil?
|
|
187
|
+
|
|
188
|
+
key = key_expr(key_node)
|
|
189
|
+
key && [receiver.first, receiver.last, key]
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# A closed, non-empty hash shape (or a union of them): the carrier whose computed-key read carries the miss
|
|
193
|
+
# `nil`. An RBS `Hash[K, V]` read is already nil-free; an open shape or a user class answering `key?` is left
|
|
194
|
+
# alone.
|
|
195
|
+
def receiver_type?(type)
|
|
196
|
+
if type.is_a?(Type::Union)
|
|
197
|
+
type.members.all? { |member| closed_shape?(member) }
|
|
198
|
+
else
|
|
199
|
+
closed_shape?(type)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def closed_shape?(type) = type.is_a?(Type::HashShape) && type.closed? && !type.pairs.empty?
|
|
204
|
+
|
|
205
|
+
# The truthy edge of `receiver.key?(key)` with a non-literal key, inside the re-walk: `scope` with the guard
|
|
206
|
+
# recorded, or nil when the guard does not apply.
|
|
207
|
+
def record(call_node, scope)
|
|
208
|
+
walk = Thread.current[STATE_KEY]
|
|
209
|
+
return nil if walk.nil?
|
|
210
|
+
|
|
211
|
+
args = call_node.arguments&.arguments
|
|
212
|
+
return nil unless args&.size == 1
|
|
213
|
+
|
|
214
|
+
guard = address(call_node.receiver, args.first)
|
|
215
|
+
return nil if guard.nil?
|
|
216
|
+
return nil unless receiver_type?(scope.type_of(call_node.receiver))
|
|
217
|
+
return nil unless Scan.eligible?(walk, call_node, guard)
|
|
218
|
+
|
|
219
|
+
walk.guard_ends[guard] = call_node.location.end_offset
|
|
220
|
+
scope.with_indexed_narrowing(*guard, PRESENT)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# `type`, the un-narrowed answer of the read `node`, with the miss `nil` dropped when, inside the re-walk, a
|
|
224
|
+
# guard on the same receiver and structurally the same key holds in `scope`, nothing between the guard and the
|
|
225
|
+
# read may have changed it ({.intervening?}), and no value of the receiver can be `nil` (the read cannot tell a
|
|
226
|
+
# value's own `nil` from the miss). Nil when no guard applies or nothing would change.
|
|
227
|
+
def guarded_read(node, type, scope)
|
|
228
|
+
walk = Thread.current[STATE_KEY]
|
|
229
|
+
guard = walk && held_guard(node, scope)
|
|
230
|
+
return nil if guard.nil? || Scan.intervening?(walk, guard, node)
|
|
231
|
+
|
|
232
|
+
receiver_type = scope.type_of(node.receiver)
|
|
233
|
+
return nil unless receiver_type?(receiver_type) && nil_free_values?(receiver_type)
|
|
234
|
+
|
|
235
|
+
narrowed = Narrowing.narrow_non_nil(type)
|
|
236
|
+
narrowed == type || narrowed.is_a?(Type::Bot) ? nil : narrowed
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# The guard `scope` holds at the address of the blockless single-key read `node`, or nil.
|
|
240
|
+
def held_guard(node, scope)
|
|
241
|
+
return nil if !node.block.nil? || !any_guard?(scope)
|
|
242
|
+
|
|
243
|
+
args = node.arguments&.arguments
|
|
244
|
+
guard = args&.size == 1 ? address(node.receiver, args.first) : nil
|
|
245
|
+
guard && !scope.indexed_narrowing(*guard).nil? ? guard : nil
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def nil_free_values?(receiver_type)
|
|
249
|
+
shapes = receiver_type.is_a?(Type::Union) ? receiver_type.members : [receiver_type]
|
|
250
|
+
shapes.none? { |shape| shape.pairs.each_value.any? { |value| value_may_be_nil?(value) } }
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Whether `scope` holds any guard. Walks the keys without allocating, so the common no-guard case costs a size
|
|
254
|
+
# check.
|
|
255
|
+
def any_guard?(scope)
|
|
256
|
+
table = scope.indexed_narrowings
|
|
257
|
+
return false if table.empty?
|
|
258
|
+
|
|
259
|
+
table.each_key { |key| return true if key.key.is_a?(KeyExpr) }
|
|
260
|
+
false
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def value_may_be_nil?(value)
|
|
264
|
+
case value
|
|
265
|
+
when Type::Top then true
|
|
266
|
+
when Type::Constant then value.value.nil?
|
|
267
|
+
when Type::Nominal then value.class_name == "NilClass"
|
|
268
|
+
when Type::Union then value.members.any? { |member| value_may_be_nil?(member) }
|
|
269
|
+
else false
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Drops the guards `call_node` may break. Always-safe (only forgets; never invents).
|
|
274
|
+
def invalidate_after_call(call_node, scope)
|
|
275
|
+
return scope unless call_node.is_a?(Prism::CallNode) && any_guard?(scope)
|
|
276
|
+
|
|
277
|
+
drop_guards(scope) { |guard| Scan.breaks?(call_node, triple(guard)) }
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Drops the guards a write aliases: one whose value holds the guarded receiver or the key's root variable
|
|
281
|
+
# (`g = h`, `j = k`, `@cache = [MAP]`), since the copy may then be mutated or rebound out of sight.
|
|
282
|
+
def invalidate_after_write(node, scope)
|
|
283
|
+
return scope unless Scan::WRITE_NODES.include?(node.class) && any_guard?(scope)
|
|
284
|
+
|
|
285
|
+
drop_guards(scope) do |guard|
|
|
286
|
+
Scan.aliases_guard?(node, triple(guard))
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def triple(guard) = [guard.receiver_kind, guard.receiver_name, guard.key]
|
|
291
|
+
|
|
292
|
+
def drop_guards(scope)
|
|
293
|
+
result = scope
|
|
294
|
+
scope.indexed_narrowings.each_key do |guard|
|
|
295
|
+
next unless guard.key.is_a?(KeyExpr) && yield(guard)
|
|
296
|
+
|
|
297
|
+
result = result.without_indexed_narrowing(guard.receiver_kind, guard.receiver_name, guard.key)
|
|
298
|
+
end
|
|
299
|
+
result
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
@@ -27,11 +27,26 @@ module Rigor
|
|
|
27
27
|
# parameter-typed block params, declared per-verb argument contracts — are ceiling concerns
|
|
28
28
|
# for later slices.
|
|
29
29
|
module MacroBlockSelfType
|
|
30
|
+
# A matched entry's effect on the block body: the `self_type` it runs with (nil keeps the entry scope's), the
|
|
31
|
+
# modules its `Proc#refined` puts in effect (issue #1667), and whether that `self` is itself unknown whatever
|
|
32
|
+
# the receiver: a `:lexical` entry copies the caller's `self`, which inside a block of unknown `self` is the
|
|
33
|
+
# enclosing method's guess (issue #1717).
|
|
34
|
+
Match = Data.define(:self_type, :refinements, :self_unknown) do
|
|
35
|
+
def initialize(self_type:, refinements:, self_unknown: false) = super
|
|
36
|
+
end
|
|
37
|
+
|
|
30
38
|
module_function
|
|
31
39
|
|
|
32
40
|
# @return the narrowed self-type, or
|
|
33
41
|
# `nil` when no registered entry matches the call shape.
|
|
34
42
|
def narrow_self_type_for(scope:, call_node:, receiver_type:)
|
|
43
|
+
match_for(scope: scope, call_node: call_node, receiver_type: receiver_type)&.self_type
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The block entry `call_node`'s first matching entry contributes, or nil when none matches; a later entry for the
|
|
47
|
+
# same call, another plugin's included, is not consulted. A `:lexical` entry (issue #1667) keeps the caller's
|
|
48
|
+
# `self`, so its `self_type` is the calling scope's.
|
|
49
|
+
def match_for(scope:, call_node:, receiver_type:)
|
|
35
50
|
return nil if receiver_type.nil?
|
|
36
51
|
|
|
37
52
|
environment = scope&.environment
|
|
@@ -47,19 +62,49 @@ module Rigor
|
|
|
47
62
|
# by the table key.
|
|
48
63
|
entries = registry.contribution_index.block_entries_for(call_node.name)
|
|
49
64
|
entries.each do |entry|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return
|
|
65
|
+
next unless entry_matches?(entry, singleton_name, nominal_name, call_node, scope, environment)
|
|
66
|
+
|
|
67
|
+
return entry_match(entry, singleton_name || nominal_name, scope, environment)
|
|
53
68
|
end
|
|
54
69
|
nil
|
|
55
70
|
end
|
|
56
71
|
|
|
57
|
-
# The
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
72
|
+
# The {Match} a matching entry contributes: a `:lexical` one keeps the calling scope's `self`, unknown where the
|
|
73
|
+
# caller's is.
|
|
74
|
+
def entry_match(entry, receiver_name, scope, environment)
|
|
75
|
+
if entry.lexical_self?
|
|
76
|
+
return Match.new(self_type: scope.self_type, refinements: entry.refinements,
|
|
77
|
+
self_unknown: scope.block_self_unknown?)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
Match.new(self_type: narrowed_self_type(entry, receiver_name, environment), refinements: entry.refinements)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The receiver an implicit-self call is matched on: the scope's `self`, or at the file's top level (no
|
|
84
|
+
# `self_type`) `Object`, as the value pass types it ({ExpressionTyper#call_receiver_type_for}), so the evaluator,
|
|
85
|
+
# the indexer and the value pass agree on whether a top-level `build { … }` matches (issue #1667).
|
|
86
|
+
def implicit_receiver_type(scope)
|
|
87
|
+
scope.self_type || scope.environment&.nominal_for_name("Object")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Applies `match` to a block body's entry scope: the narrowed `self` (with the #1717 mark `keeps_unknown`
|
|
91
|
+
# decides) and the declared refinements, appended to those the body already inherits.
|
|
92
|
+
def apply(block_scope, match, keeps_unknown:)
|
|
93
|
+
return block_scope if match.nil?
|
|
94
|
+
|
|
95
|
+
narrowed = block_scope
|
|
96
|
+
if match.self_type
|
|
97
|
+
narrowed = narrowed.with_block_self_type(match.self_type, keeps_unknown: keeps_unknown || match.self_unknown)
|
|
98
|
+
end
|
|
99
|
+
narrowed.with_declared_refinements(match.refinements)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Whether one entry matches this receiver. Nominal receivers re-enter a *named instance*-binding entry
|
|
103
|
+
# (`params`-family nesting on `Nominal[ParamsScope]`), and match a `:lexical` one, whose method is as often an
|
|
104
|
+
# instance method (`ActiveRecord::Relation#where`) as a class-level one. `:receiver_instance` and
|
|
105
|
+
# `singleton(...)` entries keep their Singleton-only contract.
|
|
106
|
+
def entry_matches?(entry, singleton_name, nominal_name, call_node, scope, environment)
|
|
107
|
+
return false if singleton_name.nil? && !entry.matches_instance_receivers?
|
|
63
108
|
|
|
64
109
|
method_name = call_node.name
|
|
65
110
|
receiver_name = singleton_name || nominal_name
|
|
@@ -73,9 +118,7 @@ module Rigor
|
|
|
73
118
|
matched = singleton_extends_reach?(receiver_name, entry.receiver_constraint, method_name,
|
|
74
119
|
call_node, scope, environment)
|
|
75
120
|
end
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
narrowed_self_type(entry, receiver_name, environment)
|
|
121
|
+
matched
|
|
79
122
|
end
|
|
80
123
|
|
|
81
124
|
# The match contract stays narrow: `Singleton[X]` receivers (class-level DSL calls) for every
|
|
@@ -113,6 +156,10 @@ module Rigor
|
|
|
113
156
|
def receiver_class_inherits_from?(class_name, constraint, environment, scope = nil)
|
|
114
157
|
name = class_name.to_s
|
|
115
158
|
return true if name == constraint
|
|
159
|
+
# Every receiver here is an `Object`: a class's instance or class object, or a module object. A class the
|
|
160
|
+
# project declares without a superclass reaches `Object` through no recorded edge (issue #1667: a
|
|
161
|
+
# top-level helper's `receiver_constraint: "Object"`).
|
|
162
|
+
return true if constraint == "Object"
|
|
116
163
|
return true if rbs_inherits?(name, constraint, environment)
|
|
117
164
|
|
|
118
165
|
# Source-side ancestry — `class API < Grape::API` lives on the scope's discovery tables, not in
|
|
@@ -30,6 +30,42 @@ module Rigor
|
|
|
30
30
|
"::range" => ["Range"]
|
|
31
31
|
})
|
|
32
32
|
private_constant :ALIAS_STRICT_NOMINALS
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Checks the param's RBS type against an arg using alias-strict-arm matching. Optional / Union
|
|
38
|
+
# wrappers are flattened; alias resolution is one level deep (the canonical core aliases all have
|
|
39
|
+
# non-alias strict arms).
|
|
40
|
+
def alias_param_accepts?(rbs_type, arg)
|
|
41
|
+
nominal_names = strict_nominal_names_for(rbs_type)
|
|
42
|
+
return false if nominal_names.nil? || nominal_names.empty?
|
|
43
|
+
|
|
44
|
+
nominal_names.any? do |class_name|
|
|
45
|
+
result = Type::Combinator.nominal_of(class_name).accepts(arg, mode: :gradual)
|
|
46
|
+
result.yes? || result.maybe?
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns the candidate class names a param's RBS type accepts under alias-resolved strict
|
|
51
|
+
# matching, or nil when the shape cannot be reduced to a closed set of nominals (e.g. an
|
|
52
|
+
# Interface or an unrecognised alias).
|
|
53
|
+
def strict_nominal_names_for(rbs_type)
|
|
54
|
+
case rbs_type
|
|
55
|
+
when RBS::Types::ClassInstance
|
|
56
|
+
[rbs_type.name.to_s.delete_prefix("::")]
|
|
57
|
+
when RBS::Types::Alias
|
|
58
|
+
ALIAS_STRICT_NOMINALS[rbs_type.name.to_s]
|
|
59
|
+
when RBS::Types::Optional
|
|
60
|
+
strict_nominal_names_for(rbs_type.type)
|
|
61
|
+
when RBS::Types::Union
|
|
62
|
+
parts = rbs_type.types.map { |t| strict_nominal_names_for(t) }
|
|
63
|
+
return nil if parts.any?(&:nil?)
|
|
64
|
+
|
|
65
|
+
parts.flatten
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
33
69
|
end
|
|
34
70
|
end
|
|
35
71
|
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
require_relative "../../reflection"
|
|
5
|
+
require_relative "integer_step_block_params"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
module MethodDispatcher
|
|
10
|
+
# The elements of the `Enumerator::ArithmeticSequence` that block-less `Integer#step` returns (issue #1794).
|
|
11
|
+
#
|
|
12
|
+
# `ruby/rbs` declares `class Enumerator::ArithmeticSequence < Enumerator[Numeric]`, a class with no type
|
|
13
|
+
# parameter, so every element it yields reads as `Numeric` and `1.step(n, 2).map { |i| i.even? }` reported
|
|
14
|
+
# `even?` on correct code. The block form's operand rule ({IntegerStepBlockParams.element_type}) decides the
|
|
15
|
+
# elements here too, and the answer rides on the sequence as a Rigor-private type argument,
|
|
16
|
+
# `Enumerator::ArithmeticSequence[Integer]`, which RBS has no slot for and {Type::Nominal#erase_to_rbs} drops.
|
|
17
|
+
#
|
|
18
|
+
# The two forms differ only past an infinite Float limit: `1.step(Float::INFINITY, 2) { … }` yields Integers
|
|
19
|
+
# but the sequence yields Floats (`.first(2) #=> [1.0, 3.0]`, through `each` too). A Float limit is a
|
|
20
|
+
# non-Integer operand, which the rule declines, and an untyped one answers `Dynamic[Numeric]`, so both forms
|
|
21
|
+
# are sound under the shared rule.
|
|
22
|
+
#
|
|
23
|
+
# The type argument is read where the `Numeric` would be: a method the sequence inherits from `Enumerator`
|
|
24
|
+
# or `Enumerable` (`map`, `select`, `to_a`, `first`, `each_slice`, `lazy`, …) dispatches as
|
|
25
|
+
# `Enumerator[elem]`, with `self` still the sequence, and the block of the sequence's own `each` yields the
|
|
26
|
+
# element. The rest of its own surface (`begin`, `end`, `last`, `step`, `size`) keeps its RBS return.
|
|
27
|
+
module ArithmeticSequenceElements
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
CLASS_NAME = "Enumerator::ArithmeticSequence"
|
|
31
|
+
ELEMENT_OWNERS = %w[::Enumerator ::Enumerable].freeze
|
|
32
|
+
private_constant :ELEMENT_OWNERS
|
|
33
|
+
|
|
34
|
+
# The block-less `Integer#step` return, or nil to leave the call to the RBS tier.
|
|
35
|
+
def try_dispatch(context)
|
|
36
|
+
return nil unless context.method_name == :step
|
|
37
|
+
return nil unless context.block_type.nil?
|
|
38
|
+
return nil if context.call_node.respond_to?(:block) && context.call_node.block
|
|
39
|
+
|
|
40
|
+
element = IntegerStepBlockParams.element_type(context.receiver, context.args)
|
|
41
|
+
element && Type::Combinator.nominal_of(CLASS_NAME, type_args: [element])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The receiver `method_name` reads the sequence's element through: `Enumerator[elem]` for a method the
|
|
45
|
+
# sequence inherits from `Enumerator` / `Enumerable` (and, with `block: true`, for the block of its own
|
|
46
|
+
# `each`), or nil to dispatch on the receiver as it is.
|
|
47
|
+
def element_receiver(receiver, method_name, environment, block: false)
|
|
48
|
+
element = element_of(receiver)
|
|
49
|
+
return nil if element.nil?
|
|
50
|
+
return nil unless (block && method_name == :each) || inherited_element_method?(method_name, environment)
|
|
51
|
+
|
|
52
|
+
Type::Combinator.nominal_of("Enumerator", type_args: [element])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def element_of(receiver)
|
|
56
|
+
return nil unless receiver.is_a?(Type::Nominal) && receiver.class_name == CLASS_NAME
|
|
57
|
+
return nil unless receiver.type_args.size == 1
|
|
58
|
+
|
|
59
|
+
receiver.type_args.first
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def inherited_element_method?(method_name, environment)
|
|
63
|
+
definition = Rigor::Reflection.instance_method_definition(CLASS_NAME, method_name, environment: environment)
|
|
64
|
+
return false if definition.nil?
|
|
65
|
+
|
|
66
|
+
ELEMENT_OWNERS.include?(definition.defined_in.to_s)
|
|
67
|
+
rescue StandardError
|
|
68
|
+
false
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# Which arguments `OverloadSelector` may not discriminate on. Issue #1021 — the strict and alias passes must not
|
|
9
|
+
# key on an argument whose type cannot rule an overload out: the bare untyped carrier, or a union with an untyped
|
|
10
|
+
# member. That member may reach any overload at runtime, so a strict match is decided by the union's other
|
|
11
|
+
# members alone — `Dynamic[top] | nil` pinned `Regexp#match?(nil) -> false` and typed a live predicate as the
|
|
12
|
+
# literal `false`. The gradual pass still accepts against the whole union, so `Dynamic[top] | nil` keeps both
|
|
13
|
+
# `match?` overloads and the #521 join answers `Dynamic[bool]`.
|
|
14
|
+
#
|
|
15
|
+
# A `Dynamic` with a concrete static facet stays out: its facet discriminates. One whose facet itself holds the
|
|
16
|
+
# untyped carrier does not (#1675): `Array[untyped] | Array[Integer]` indexed at an untyped position answers
|
|
17
|
+
# `Dynamic[Array[untyped] | untyped | nil] | Dynamic[Array[Integer] | Integer | nil]`, and read as precise,
|
|
18
|
+
# `0 + @data[i]` took `(Integer) -> Integer`.
|
|
19
|
+
module ImpreciseArgument
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# The literal `untyped` carrier, `Dynamic[top]`.
|
|
23
|
+
def untyped?(type) = type.is_a?(Type::Dynamic) && type.static_facet.is_a?(Type::Top)
|
|
24
|
+
|
|
25
|
+
# The untyped carrier itself, or a union or `Dynamic` holding it at any depth.
|
|
26
|
+
def imprecise?(type)
|
|
27
|
+
case type
|
|
28
|
+
when Type::Dynamic then type.static_facet.is_a?(Type::Top) || imprecise?(type.static_facet)
|
|
29
|
+
when Type::Union then type.members.any? { |member| imprecise?(member) }
|
|
30
|
+
else false
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Whether the call holds an imprecise argument: a positional one, or (#1737) a value of the call's keyword
|
|
35
|
+
# hash when `keywords_last`. An untyped keyword value reaches every overload's keyword just as an untyped
|
|
36
|
+
# positional reaches every positional parameter, so `m(1, mode: untyped)` must not pick `mode: Symbol` over
|
|
37
|
+
# `mode: Integer` by declaration order.
|
|
38
|
+
def any_in?(arg_types, keywords_last)
|
|
39
|
+
return true if arg_types.any? { |arg| imprecise?(arg) }
|
|
40
|
+
return false unless keywords_last
|
|
41
|
+
|
|
42
|
+
keywords = arg_types.last
|
|
43
|
+
keywords.is_a?(Type::HashShape) && keywords.pairs.each_value.any? { |value| imprecise?(value) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Issue #1675 — the argument list with each imprecise argument replaced by the bare untyped carrier, or nil
|
|
47
|
+
# when no argument holds the carrier beside something precise. Through its untyped part such an argument
|
|
48
|
+
# reaches every overload the bare carrier reaches, including one a precise member rules out of the
|
|
49
|
+
# whole-argument match: `0 + (1 | untyped)` kept only `(Integer) -> Integer`, because `(Float)` refuses the
|
|
50
|
+
# `1`, and typed the sum `Integer` where the untyped part may be a `Float` or a project `Numeric`.
|
|
51
|
+
def untyped_stand_ins(arg_types)
|
|
52
|
+
return nil unless arg_types.any? { |arg| !untyped?(arg) && imprecise?(arg) }
|
|
53
|
+
|
|
54
|
+
arg_types.map { |arg| imprecise?(arg) ? Type::Combinator.untyped : arg }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# `matches` joined by the overloads the selector's `shared` bundle reaches with its arguments' untyped parts
|
|
58
|
+
# standing in for them ({.untyped_stand_ins}), which the block selects for that bundle.
|
|
59
|
+
def with_stand_ins(matches, shared)
|
|
60
|
+
stand_ins = untyped_stand_ins(shared[:arg_types])
|
|
61
|
+
return matches if stand_ins.nil?
|
|
62
|
+
|
|
63
|
+
(matches + yield(shared.merge(arg_types: stand_ins, positional_arguments: nil))).uniq
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# `Integer#step`'s block parameter, reached through {IteratorDispatch.block_param_types} (issue #1783).
|
|
9
|
+
#
|
|
10
|
+
# `ruby/rbs` declares every `Numeric#step` overload with a `{ (Numeric) -> void }` block and `Integer` does
|
|
11
|
+
# not redeclare `step`, so `1.step(n, 2) { |i| i.even? }` reported `even?` on correct code. The block form
|
|
12
|
+
# yields Integer only while the limit and the step are Integer too: a Float step or a finite Float limit
|
|
13
|
+
# makes CRuby's `ruby_float_step` yield Floats (`1.step(10, 0.5)`, `1.step(10.0)`; an infinite limit with
|
|
14
|
+
# an Integer step still yields Integers, which this rule leaves on the RBS binding), and a Rational step yields
|
|
15
|
+
# Rationals after the first value. So:
|
|
16
|
+
#
|
|
17
|
+
# - every operand provably Integer (an absent one is the Integer default; a nil limit means "no limit")
|
|
18
|
+
# binds `Integer`;
|
|
19
|
+
# - an operand that is `Dynamic` (an untyped parameter, `1.step(n, 2)`) could be either, so the binding is
|
|
20
|
+
# `Dynamic[Numeric]`: Integer is unsound for `n = 0.5`, and the RBS `Numeric` reports `even?` on correct
|
|
21
|
+
# Integer code, so the answer that holds for both declines the check;
|
|
22
|
+
# - anything else (a Float, a Rational, a union) falls through to the RBS `Numeric` binding, which a Float
|
|
23
|
+
# or Rational receiver also keeps.
|
|
24
|
+
#
|
|
25
|
+
# The keyword forms (`step(by: 2, to: 10)`, `step(10, by: 2)`) arrive as a trailing hash shape.
|
|
26
|
+
module IntegerStepBlockParams
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
KEYWORD_ROLES = { to: :limit, by: :step }.freeze
|
|
30
|
+
private_constant :KEYWORD_ROLES
|
|
31
|
+
|
|
32
|
+
# @return the block-param types, or nil to fall through to the RBS tier.
|
|
33
|
+
def block_param_types(receiver, args)
|
|
34
|
+
element = element_type(receiver, args)
|
|
35
|
+
element && [element]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The type of every value the call yields, or nil when the rule does not decide it. The block-less form
|
|
39
|
+
# shares it: the `Enumerator::ArithmeticSequence` it returns yields the same values as the block would,
|
|
40
|
+
# except past an infinite Float limit, which this rule already declines ({ArithmeticSequenceElements}).
|
|
41
|
+
def element_type(receiver, args)
|
|
42
|
+
return nil unless IteratorDispatch.integer_rooted?(receiver)
|
|
43
|
+
|
|
44
|
+
operands = operands(args)
|
|
45
|
+
return nil if operands.nil?
|
|
46
|
+
|
|
47
|
+
verdicts = operands.map { |role, type| verdict(role, type) }
|
|
48
|
+
return nil if verdicts.include?(:decline)
|
|
49
|
+
return Type::Combinator.nominal_of("Integer") if verdicts.all?(:integer)
|
|
50
|
+
|
|
51
|
+
Type::Combinator.dynamic(Type::Combinator.nominal_of("Numeric"))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The `[role, type]` pairs of the call's limit and step operands, or nil when the argument list is not
|
|
55
|
+
# one of `step`'s shapes.
|
|
56
|
+
def operands(args)
|
|
57
|
+
positional = args
|
|
58
|
+
keywords = {}
|
|
59
|
+
if args.last.is_a?(Type::HashShape)
|
|
60
|
+
positional = args[0...-1]
|
|
61
|
+
keywords = args.last.pairs
|
|
62
|
+
return nil unless keywords.keys.all? { |key| KEYWORD_ROLES.key?(key) }
|
|
63
|
+
end
|
|
64
|
+
return nil if positional.size > 2
|
|
65
|
+
|
|
66
|
+
pairs = positional.zip(%i[limit step]).map { |type, role| [role, type] }
|
|
67
|
+
keywords.each { |key, type| pairs << [KEYWORD_ROLES.fetch(key), type] }
|
|
68
|
+
pairs
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def verdict(role, type)
|
|
72
|
+
return :integer if IteratorDispatch.integer_rooted?(type)
|
|
73
|
+
return :integer if role == :limit && type.is_a?(Type::Constant) && type.value.nil?
|
|
74
|
+
return :unknown if type.is_a?(Type::Dynamic)
|
|
75
|
+
|
|
76
|
+
:decline
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|