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.
Files changed (182) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/11-sig-gen.md +6 -0
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/03-configuration.md +1 -1
  27. data/docs/manual/04-diagnostics.md +2 -1
  28. data/docs/manual/06-baseline.md +2 -0
  29. data/docs/manual/07-plugins.md +1 -1
  30. data/docs/manual/18-removing-dead-code.md +32 -2
  31. data/docs/manual/plugins/README.md +6 -1
  32. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  33. data/docs/manual/plugins/rigor-alba.md +60 -0
  34. data/docs/manual/plugins/rigor-minitest.md +22 -4
  35. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  36. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  37. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  38. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  39. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  40. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  41. data/lib/rigor/analysis/check_rules.rb +289 -57
  42. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  43. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  44. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  45. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  46. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  47. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  48. data/lib/rigor/analysis/incremental.rb +29 -9
  49. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  50. data/lib/rigor/analysis/incremental_run_slot.rb +3 -1
  51. data/lib/rigor/analysis/incremental_session.rb +14 -1
  52. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  53. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  54. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  55. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  56. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
  57. data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
  58. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  59. data/lib/rigor/analysis/runner.rb +34 -3
  60. data/lib/rigor/analysis/worker_session.rb +7 -4
  61. data/lib/rigor/cache/incremental_snapshot.rb +22 -0
  62. data/lib/rigor/cli/annotate_command.rb +5 -0
  63. data/lib/rigor/cli/baseline_command.rb +7 -18
  64. data/lib/rigor/cli/check_command.rb +2 -1
  65. data/lib/rigor/cli/trace_command.rb +5 -0
  66. data/lib/rigor/cli/type_of_command.rb +5 -3
  67. data/lib/rigor/cli/unused_command.rb +13 -7
  68. data/lib/rigor/cli.rb +10 -2
  69. data/lib/rigor/configuration/severity_profile.rb +6 -0
  70. data/lib/rigor/configuration.rb +46 -2
  71. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  72. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  73. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  74. data/lib/rigor/environment/rbs_loader.rb +283 -14
  75. data/lib/rigor/environment/required_features.rb +206 -0
  76. data/lib/rigor/environment.rb +21 -3
  77. data/lib/rigor/inference/acceptance.rb +28 -3
  78. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  79. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  80. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  81. data/lib/rigor/inference/expression_typer.rb +215 -16
  82. data/lib/rigor/inference/global_write_census.rb +7 -4
  83. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  84. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  85. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  86. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  87. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  88. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  89. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  90. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  91. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  92. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  93. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  94. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  95. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  96. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  97. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  98. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  99. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  100. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +259 -28
  101. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  102. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  103. data/lib/rigor/inference/module_function_state.rb +2 -1
  104. data/lib/rigor/inference/narrowing.rb +20 -6
  105. data/lib/rigor/inference/object_mixins.rb +180 -0
  106. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  107. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  108. data/lib/rigor/inference/refine_census.rb +239 -0
  109. data/lib/rigor/inference/refine_self.rb +317 -0
  110. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  111. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  112. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  113. data/lib/rigor/inference/scope_indexer.rb +687 -60
  114. data/lib/rigor/inference/statement_evaluator.rb +71 -20
  115. data/lib/rigor/inference/string_mutation.rb +9 -3
  116. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  117. data/lib/rigor/inference/version_guard.rb +19 -10
  118. data/lib/rigor/language_server/project_context.rb +31 -0
  119. data/lib/rigor/language_server/server.rb +18 -0
  120. data/lib/rigor/plugin/inflector.rb +1 -1
  121. data/lib/rigor/plugin/io_boundary.rb +42 -3
  122. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  123. data/lib/rigor/project_environment.rb +29 -6
  124. data/lib/rigor/protection/mutation_cache.rb +1 -1
  125. data/lib/rigor/protection/mutator.rb +1 -1
  126. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  127. data/lib/rigor/reflection/constant_path.rb +13 -2
  128. data/lib/rigor/reflection.rb +40 -8
  129. data/lib/rigor/scope/discovery_index.rb +40 -1
  130. data/lib/rigor/scope/resolution_chain.rb +159 -11
  131. data/lib/rigor/scope.rb +189 -10
  132. data/lib/rigor/sig_gen/classification.rb +4 -1
  133. data/lib/rigor/sig_gen/generator.rb +55 -5
  134. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  135. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  136. data/lib/rigor/source/alias_names.rb +52 -0
  137. data/lib/rigor/source/literals.rb +3 -2
  138. data/lib/rigor/source/symbol_name.rb +20 -0
  139. data/lib/rigor/type/combinator.rb +34 -2
  140. data/lib/rigor/type/float_range.rb +4 -1
  141. data/lib/rigor/type/nominal.rb +6 -1
  142. data/lib/rigor/version.rb +1 -1
  143. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  144. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  145. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  146. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  147. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  148. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  149. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  150. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  151. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  152. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  153. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  154. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  156. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  157. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  158. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  159. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  160. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  161. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  162. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  163. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  164. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  165. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  166. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  167. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  168. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  169. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  170. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  171. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  172. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  173. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  174. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  175. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  176. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  177. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  178. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  179. data/sig/rigor/scope.rbs +27 -4
  180. data/sig/rigor/source.rbs +4 -0
  181. data/sig/rigor.rbs +3 -1
  182. 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
- narrowed = entry_self_type_for(entry, singleton_name, nominal_name, call_node,
51
- scope, environment)
52
- return narrowed if narrowed
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 narrowed `self_type` one entry contributes for this receiver, or nil on a miss. Nominal
58
- # receivers exist only inside an already-narrowed `instance_eval` body — they can only re-enter a
59
- # *named instance*-binding entry (`params`-family nesting on `Nominal[ParamsScope]`).
60
- # `:receiver_instance` and `singleton(...)` entries keep their Singleton-only contract.
61
- def entry_self_type_for(entry, singleton_name, nominal_name, call_node, scope, environment)
62
- return nil if singleton_name.nil? && !entry.named_instance_binding?
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
- return nil unless matched
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