rigortype 0.4.4 → 0.4.6

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 (233) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/docs/handbook/11-sig-gen.md +100 -18
  4. data/docs/manual/02-cli-reference.md +85 -16
  5. data/docs/manual/03-configuration.md +12 -9
  6. data/docs/manual/04-diagnostics.md +17 -6
  7. data/docs/manual/05-inspecting-types.md +2 -2
  8. data/docs/manual/06-baseline.md +1 -1
  9. data/docs/manual/07-plugins.md +2 -1
  10. data/docs/manual/08-skills.md +7 -0
  11. data/docs/manual/10-mcp-server.md +17 -2
  12. data/docs/manual/12-caching.md +8 -1
  13. data/docs/manual/18-removing-dead-code.md +18 -7
  14. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  15. data/docs/manual/plugins/rigor-actionpack.md +30 -0
  16. data/docs/manual/plugins/rigor-devise.md +15 -0
  17. data/docs/manual/plugins/rigor-rspec.md +102 -1
  18. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  19. data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
  20. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
  21. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
  22. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +115 -7
  23. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +253 -0
  24. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  25. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  26. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  27. data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
  28. data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
  29. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +64 -4
  30. data/lib/rigor/analysis/check_rules.rb +319 -21
  31. data/lib/rigor/analysis/dependency_recorder.rb +11 -0
  32. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  33. data/lib/rigor/analysis/edit_replay.rb +67 -0
  34. data/lib/rigor/analysis/incremental.rb +86 -0
  35. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
  36. data/lib/rigor/analysis/incremental_session.rb +336 -46
  37. data/lib/rigor/analysis/path_expansion.rb +44 -1
  38. data/lib/rigor/analysis/reachability/graph.rb +90 -13
  39. data/lib/rigor/analysis/reachability/scan.rb +279 -39
  40. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  41. data/lib/rigor/analysis/rule_catalog.rb +22 -5
  42. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  43. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  44. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  45. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +82 -16
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
  48. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  49. data/lib/rigor/analysis/runner.rb +222 -111
  50. data/lib/rigor/analysis/severity_stamp.rb +11 -0
  51. data/lib/rigor/analysis/worker_session.rb +24 -1
  52. data/lib/rigor/cache/descriptor.rb +40 -7
  53. data/lib/rigor/cache/engine_source.rb +3 -2
  54. data/lib/rigor/cache/incremental_snapshot.rb +49 -8
  55. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  56. data/lib/rigor/cache/store.rb +4 -3
  57. data/lib/rigor/cli/check_command.rb +93 -4
  58. data/lib/rigor/cli/command.rb +3 -1
  59. data/lib/rigor/cli/diff_command.rb +2 -1
  60. data/lib/rigor/cli/docs_command.rb +3 -2
  61. data/lib/rigor/cli/doctor_command.rb +1 -1
  62. data/lib/rigor/cli/explain_command.rb +3 -2
  63. data/lib/rigor/cli/lens_command.rb +5 -5
  64. data/lib/rigor/cli/mcp_command.rb +2 -1
  65. data/lib/rigor/cli/options.rb +20 -3
  66. data/lib/rigor/cli/plugin_command.rb +2 -1
  67. data/lib/rigor/cli/plugins_command.rb +7 -1
  68. data/lib/rigor/cli/plugins_renderer.rb +24 -2
  69. data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
  70. data/lib/rigor/cli/sig_gen_command.rb +73 -13
  71. data/lib/rigor/cli/skill_command.rb +2 -1
  72. data/lib/rigor/cli/skill_describe.rb +3 -2
  73. data/lib/rigor/cli/type_of_command.rb +2 -1
  74. data/lib/rigor/cli/type_of_project_scope.rb +27 -0
  75. data/lib/rigor/cli/unused_command.rb +10 -7
  76. data/lib/rigor/cli.rb +3 -2
  77. data/lib/rigor/config_audit.rb +72 -16
  78. data/lib/rigor/configuration/severity_profile.rb +20 -2
  79. data/lib/rigor/configuration.rb +43 -7
  80. data/lib/rigor/effects/signature_sources.rb +3 -1
  81. data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
  82. data/lib/rigor/environment/gated_signature_guard.rb +2 -1
  83. data/lib/rigor/environment/gated_signature_plan.rb +2 -1
  84. data/lib/rigor/environment/installed_gem_set.rb +2 -1
  85. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
  86. data/lib/rigor/environment/rbs_loader.rb +136 -7
  87. data/lib/rigor/environment/required_features.rb +136 -29
  88. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  89. data/lib/rigor/environment.rb +4 -2
  90. data/lib/rigor/inference/block_call_timing.rb +7 -1
  91. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  92. data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
  93. data/lib/rigor/inference/captured_locals.rb +44 -13
  94. data/lib/rigor/inference/clause_widening.rb +90 -0
  95. data/lib/rigor/inference/deep_erasure.rb +138 -0
  96. data/lib/rigor/inference/def_return_typer.rb +159 -9
  97. data/lib/rigor/inference/definer_resolution.rb +101 -20
  98. data/lib/rigor/inference/error_info.rb +17 -3
  99. data/lib/rigor/inference/expression_typer.rb +97 -18
  100. data/lib/rigor/inference/global_write_census.rb +36 -1
  101. data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
  102. data/lib/rigor/inference/ivar_writer_census.rb +195 -0
  103. data/lib/rigor/inference/loop_value_record.rb +53 -0
  104. data/lib/rigor/inference/macro_block_self_type.rb +20 -6
  105. data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
  106. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +278 -42
  107. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
  108. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
  109. data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
  110. data/lib/rigor/inference/method_dispatcher.rb +113 -19
  111. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  112. data/lib/rigor/inference/module_function_state.rb +149 -42
  113. data/lib/rigor/inference/mutation_widening.rb +5 -2
  114. data/lib/rigor/inference/narrowing.rb +116 -11
  115. data/lib/rigor/inference/object_mixins.rb +13 -1
  116. data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
  117. data/lib/rigor/inference/project_patched_methods.rb +168 -4
  118. data/lib/rigor/inference/project_patched_mixins.rb +162 -0
  119. data/lib/rigor/inference/project_patched_scanner.rb +51 -12
  120. data/lib/rigor/inference/project_patched_visibility.rb +214 -0
  121. data/lib/rigor/inference/project_seed_tables.rb +121 -0
  122. data/lib/rigor/inference/project_yielder.rb +128 -0
  123. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
  124. data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
  125. data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
  126. data/lib/rigor/inference/return_barrier.rb +16 -0
  127. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
  128. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  129. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
  130. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  131. data/lib/rigor/inference/scope_indexer.rb +1144 -197
  132. data/lib/rigor/inference/self_call_ivar_writes.rb +29 -12
  133. data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
  134. data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
  135. data/lib/rigor/inference/statement_evaluator.rb +1088 -213
  136. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  137. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  138. data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
  139. data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
  140. data/lib/rigor/language_server/buffer_table.rb +6 -0
  141. data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
  142. data/lib/rigor/language_server/project_context.rb +59 -3
  143. data/lib/rigor/language_server/server.rb +5 -3
  144. data/lib/rigor/lens/contract_reader.rb +45 -2
  145. data/lib/rigor/lens/document.rb +2 -1
  146. data/lib/rigor/lens/file_reader.rb +12 -2
  147. data/lib/rigor/lens/members.rb +16 -2
  148. data/lib/rigor/lens/run.rb +34 -7
  149. data/lib/rigor/mcp/server.rb +11 -3
  150. data/lib/rigor/plugin/base.rb +29 -13
  151. data/lib/rigor/plugin/io_boundary.rb +42 -6
  152. data/lib/rigor/plugin/isolation.rb +4 -3
  153. data/lib/rigor/plugin/manifest.rb +29 -3
  154. data/lib/rigor/plugin/registry.rb +38 -0
  155. data/lib/rigor/protection/discovery_seed.rb +64 -36
  156. data/lib/rigor/protection/mutation_cache.rb +8 -2
  157. data/lib/rigor/rooted_glob.rb +36 -0
  158. data/lib/rigor/scope/discovery_index.rb +5 -0
  159. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
  160. data/lib/rigor/scope/resolution_chain.rb +40 -5
  161. data/lib/rigor/scope.rb +59 -7
  162. data/lib/rigor/sig_gen/alias_index.rb +2 -1
  163. data/lib/rigor/sig_gen/classification.rb +4 -1
  164. data/lib/rigor/sig_gen/generator.rb +465 -49
  165. data/lib/rigor/sig_gen/layout_index.rb +5 -2
  166. data/lib/rigor/sig_gen/observation_collector.rb +141 -12
  167. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  168. data/lib/rigor/sig_gen/renderer.rb +16 -155
  169. data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
  170. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  171. data/lib/rigor/sig_gen/write_reporter.rb +203 -0
  172. data/lib/rigor/sig_gen/write_result.rb +3 -1
  173. data/lib/rigor/sig_gen/writer.rb +5 -3
  174. data/lib/rigor/signature_path_audit.rb +4 -2
  175. data/lib/rigor/type/combinator.rb +4 -3
  176. data/lib/rigor/type/constant.rb +54 -3
  177. data/lib/rigor/type/hash_shape.rb +14 -9
  178. data/lib/rigor/type/struct_instance.rb +3 -2
  179. data/lib/rigor/version.rb +1 -1
  180. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  181. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +61 -44
  182. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
  183. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
  184. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
  185. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  186. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
  187. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
  188. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
  189. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
  190. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
  191. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
  192. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
  193. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
  194. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  195. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  196. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  197. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
  198. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
  199. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
  200. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
  201. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
  202. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
  203. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
  204. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  205. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  206. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
  207. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
  208. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
  209. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
  210. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
  211. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  212. data/sig/rigor/analysis/baseline.rbs +3 -2
  213. data/sig/rigor/effects/file_collection.rbs +5 -6
  214. data/sig/rigor/effects/registry.rbs +8 -6
  215. data/sig/rigor/effects/summary.rbs +6 -5
  216. data/sig/rigor/environment.rbs +5 -3
  217. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  218. data/sig/rigor/inference.rbs +6 -0
  219. data/sig/rigor/plugin/additional_initializer.rbs +5 -5
  220. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  221. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  222. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  223. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  224. data/sig/rigor/plugin/io_boundary.rbs +5 -2
  225. data/sig/rigor/plugin/manifest.rbs +16 -18
  226. data/sig/rigor/plugin/protocol_contract.rbs +10 -9
  227. data/sig/rigor/plugin/registry.rbs +1 -1
  228. data/sig/rigor/rooted_glob.rbs +7 -0
  229. data/sig/rigor/scope.rbs +34 -52
  230. data/sig/rigor/type.rbs +0 -6
  231. data/skills/rigor-doctor/references/01-checks.md +23 -11
  232. data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
  233. metadata +32 -1
@@ -5,12 +5,14 @@ require "prism"
5
5
  require_relative "../type"
6
6
  require_relative "../reflection"
7
7
  require_relative "../ast"
8
+ require_relative "clause_widening"
8
9
  require_relative "../source/constant_path"
9
10
  require_relative "../source/node_children"
10
11
  require_relative "../source/node_walker"
11
12
  require_relative "../analysis/self_call_resolution_recorder"
12
13
  require_relative "block_call_timing"
13
14
  require_relative "block_repetition"
15
+ require_relative "project_yielder"
14
16
  require_relative "block_parameter_binder"
15
17
  require_relative "method_parameter_binder"
16
18
  require_relative "body_fixpoint"
@@ -41,6 +43,7 @@ require_relative "receiver_alias"
41
43
  require_relative "repeated_or_writes"
42
44
  require_relative "singleton_object_constant"
43
45
  require_relative "refined_dispatch"
46
+ require_relative "singleton_external_ahead"
44
47
  require_relative "stored_block_call"
45
48
  require_relative "struct_fold_safety"
46
49
  require_relative "unknown_store_widening"
@@ -1049,7 +1052,7 @@ module Rigor
1049
1052
  # optimistic-carrier decline) and one terminating-arm rule, and cannot drift apart again. Only the
1050
1053
  # value is read; the post-scope belongs to whichever walk owns the statement.
1051
1054
  def type_of_conditional(node)
1052
- scope.evaluate(node, tracer: tracer).first
1055
+ LoopValueRecord.fetch(node, scope) || scope.evaluate(node, tracer: tracer).first
1053
1056
  end
1054
1057
 
1055
1058
  def type_of_else(node)
@@ -1208,17 +1211,22 @@ module Rigor
1208
1211
  Type::Combinator.constant_of(nil)
1209
1212
  end
1210
1213
 
1211
- rescue_types = rescue_chain_types(rescue_clause)
1214
+ rescue_types = rescue_chain_types(rescue_clause, node.statements)
1212
1215
  Type::Combinator.union(primary_type, *rescue_types)
1213
1216
  end
1214
1217
 
1215
1218
  # Issue #1360 — the evaluator binds `$!` in a rescue clause it enters; a clause typed here, such as one in a
1216
1219
  # `begin` passed as an argument, reads `$!`, `$@` and `$?` unbound rather than an enclosing clause's.
1217
- def rescue_chain_types(rescue_node)
1218
- arm_typer = typer_under(rescue_arm_scope)
1220
+ #
1221
+ # Issue #1839 — it also reads each name the `body` writes as `Dynamic[top]` ({ClauseWidening}), as the evaluator's
1222
+ # rescue clause does: `[begin; conn = make; work; rescue; conn; end]` holds the connection, not the entry's `nil`.
1223
+ def rescue_chain_types(rescue_node, body)
1224
+ arm_scope = rescue_arm_scope
1225
+ writes = ClauseWidening.writes([body])
1219
1226
  types = []
1220
1227
  current = rescue_node
1221
1228
  while current
1229
+ arm_typer = typer_under(ClauseWidening.widen(arm_scope, writes, current.statements) || arm_scope)
1222
1230
  types << arm_typer.send(:statements_or_nil, current.statements)
1223
1231
  current = current.subsequent
1224
1232
  end
@@ -1265,11 +1273,22 @@ module Rigor
1265
1273
  Type::Combinator.bot
1266
1274
  end
1267
1275
 
1268
- # `while` and `until` loops produce nil unless interrupted by `break VALUE`; the expression value of
1269
- # `break VALUE` is not yet modeled (scope break-path propagation landed in `eval_loop`). Returning
1270
- # Constant[nil] is safe and matches Ruby semantics for the common case.
1271
- def type_of_loop(_node)
1272
- Type::Combinator.constant_of(nil)
1276
+ # A `while` / `until` / `for` loop in value position. Like {#type_of_conditional}, it delegates to the
1277
+ # statement evaluator, which owns the loop's value: `nil` or a `while` / `until` loop's `break` values
1278
+ # (issue #2031), and a `for` loop's collection or its `break` values (issue #1216).
1279
+ #
1280
+ # A `while` / `until` loop no `break` leaves is `nil` without evaluating its body, as
1281
+ # {StatementEvaluator#loop_value} answers it: a loop that is a call's receiver or argument is typed again by
1282
+ # every narrowing and rule that types that operand, and evaluating it each time re-ran the passes of every loop
1283
+ # nested in it.
1284
+ def type_of_loop(node)
1285
+ return Type::Combinator.constant_of(nil) if break_free_condition_loop?(node)
1286
+
1287
+ LoopValueRecord.fetch(node, scope) || scope.evaluate(node, tracer: tracer).first
1288
+ end
1289
+
1290
+ def break_free_condition_loop?(node)
1291
+ !node.is_a?(Prism::ForNode) && !block_level_jump?(node.statements, Prism::BreakNode)
1273
1292
  end
1274
1293
 
1275
1294
  def type_of_lambda(_node)
@@ -1279,11 +1298,23 @@ module Rigor
1279
1298
  def type_of_range(node)
1280
1299
  left_static, left = static_range_endpoint(node.left)
1281
1300
  right_static, right = static_range_endpoint(node.right)
1282
- return Type::Combinator.constant_of(Range.new(left, right, node.exclude_end?)) if left_static && right_static
1301
+ if left_static && right_static
1302
+ range = static_range_value(left, right, node.exclude_end?)
1303
+ return Type::Combinator.constant_of(range) if range
1304
+ end
1283
1305
 
1284
1306
  nominal_range_for_endpoints(node.left, node.right)
1285
1307
  end
1286
1308
 
1309
+ # The `Range` the static endpoints denote, or nil when Ruby would refuse to build it (`1.."a"`,
1310
+ # `"a"..1` raise `ArgumentError: bad value for range`). The literal is a runtime error,
1311
+ # not a constant, so the fold declines and the caller types it as a plain Range.
1312
+ def static_range_value(left, right, exclude_end)
1313
+ Range.new(left, right, exclude_end)
1314
+ rescue ArgumentError
1315
+ nil
1316
+ end
1317
+
1287
1318
  # Derives `Nominal[Range, [T]]` from the endpoint expression types when at least one endpoint is
1288
1319
  # statically typeable. The element parameter is the union of the endpoint types (lifted from
1289
1320
  # `Constant<v>` to `Nominal<v.class>` so the carrier matches what `Range#each` would yield). Falls back
@@ -2583,6 +2614,10 @@ module Rigor
2583
2614
 
2584
2615
  def_node, owner = resolve_singleton_def_with_owner(receiver.class_name, method_name)
2585
2616
  return nil if def_node.nil?
2617
+ # Issue #1936 — a contested slot holds a `def` a later definer may have replaced
2618
+ # (`singleton_class.alias_method :x, :y` after `def self.x`), or one that may not have run: its body is not
2619
+ # proven to be the method Ruby calls, so the call falls through to `Dynamic`.
2620
+ return nil if scope.discovery.contested?(:discovered_singleton_def_nodes, [owner, method_name.to_sym])
2586
2621
 
2587
2622
  result = infer_user_method_return(def_node, receiver, arg_types, yield_type: block_type)
2588
2623
  return result if result.nil?
@@ -2597,6 +2632,10 @@ module Rigor
2597
2632
  # `discovered_def_nodes` / `discovered_superclasses` / `discovered_includes`, and this walk reads a
2598
2633
  # fourth table those three do not imply. Keying on what the answer actually depends on is the #682
2599
2634
  # lesson — a memo that outlives its inputs serves one scope's answer to another.
2635
+ #
2636
+ # It is the TYPING read only, so it also withholds (`[nil, nil]`, the call types `Dynamic`) where an external
2637
+ # ancestor may answer ahead of the definer found (#1572: `extend M; extend Enumerable` makes `C.to_a`
2638
+ # `Enumerable#to_a`, which the reader passes over). The existence reads keep the reader's own answer.
2600
2639
  def resolve_singleton_def_with_owner(class_name, method_name)
2601
2640
  by_singleton = (class_graph_buckets[:singleton_def] ||= {}.compare_by_identity)
2602
2641
  cache = (by_singleton[scope.discovered_singleton_def_nodes] ||= {})
@@ -2604,7 +2643,10 @@ module Rigor
2604
2643
  key = method_name.to_sym
2605
2644
  return table[key] if table.key?(key)
2606
2645
 
2607
- table[key] = scope.singleton_def_through_ancestors(class_name, method_name)
2646
+ found = scope.singleton_def_through_ancestors(class_name, method_name)
2647
+ owner = found.last
2648
+ found = [nil, nil] if owner && SingletonExternalAhead.withhold?(scope, class_name, method_name, owner)
2649
+ table[key] = found
2608
2650
  end
2609
2651
 
2610
2652
  # The project-side singleton-method band: a `Foo.bar` call resolved against a `class << …` / `def self.…`
@@ -3288,21 +3330,34 @@ module Rigor
3288
3330
  cache = class_graph_buckets[:yields]
3289
3331
  return cache[def_node] if cache.key?(def_node)
3290
3332
 
3291
- cache[def_node] = yield_reachable?(def_node.body)
3333
+ cache[def_node] = yield_reachable?(def_node.body, def_node.parameters&.block&.name)
3292
3334
  end
3293
3335
 
3294
- def yield_reachable?(node)
3336
+ def yield_reachable?(node, block_name)
3295
3337
  return false if node.nil?
3296
- return true if node.is_a?(Prism::YieldNode)
3338
+ return true if node.is_a?(Prism::YieldNode) || forwards_block_argument?(node, block_name)
3297
3339
 
3298
3340
  node.rigor_each_child do |child|
3299
3341
  next if YIELD_BOUNDARY_NODES.include?(child.class)
3300
3342
 
3301
- return true if yield_reachable?(child)
3343
+ return true if yield_reachable?(child, block_name)
3302
3344
  end
3303
3345
  false
3304
3346
  end
3305
3347
 
3348
+ # Issue #1157 — a call that hands on its method's block (`&`, or `&blk` naming the block parameter) reaches the
3349
+ # caller's block as a `yield` does. Whether `blk` is still the method's own block (not rebound or shadowed) is
3350
+ # decided where it is typed ({#forwarded_block_return_type}); this only keeps the call-site dimension for a def
3351
+ # that might.
3352
+ def forwards_block_argument?(node, block_name)
3353
+ block = node.respond_to?(:block) ? node.block : nil
3354
+ return false unless block.is_a?(Prism::BlockArgumentNode)
3355
+
3356
+ expression = block.expression
3357
+ expression.nil? || (!block_name.nil? && expression.is_a?(Prism::LocalVariableReadNode) &&
3358
+ expression.name == block_name)
3359
+ end
3360
+
3306
3361
  # The candidate-frame memo path: consult the current run generation's bucket, and on a miss compute
3307
3362
  # and store a FINAL result. Reached only when `memo_candidate?` held (see `infer_user_method_return`).
3308
3363
  # The store gate (ADR-84 WD3): a result is stored when no transient-machinery event during the
@@ -4298,9 +4353,13 @@ module Rigor
4298
4353
 
4299
4354
  # Whether the call may run its block more than once, so a later run reads a captured binding an earlier
4300
4355
  # run rebound — the premise of laying the #587 (b) binding ({#captured_block_bindings}) under the block.
4301
- # {BlockRepetition.may_repeat?} holds the rule, which the statement pass shares (issue #1412).
4356
+ # {BlockRepetition.may_repeat?} holds the rule, which the statement pass shares (issue #1412). Issue #2301 — a
4357
+ # project callee that hands the block on ({ProjectYielder.hands_block_on?}) also counts: the block's value is what
4358
+ # the forward's target `yield`s, and that target may run it many times, so a captured local the block rebinds must
4359
+ # not read its entry binding.
4302
4360
  def block_may_repeat?(call_node, receiver_type)
4303
- BlockRepetition.may_repeat?(method_name: call_node.name, receiver_type: receiver_type, scope: scope)
4361
+ BlockRepetition.may_repeat?(method_name: call_node.name, receiver_type: receiver_type, scope: scope) ||
4362
+ ProjectYielder.hands_block_on?(call_node, receiver_type, scope)
4304
4363
  end
4305
4364
 
4306
4365
  EMPTY_BREAK_ARMS = [].freeze
@@ -4396,10 +4455,22 @@ module Rigor
4396
4455
  )
4397
4456
  type_block_body(block_arg, entry, captured: captured)
4398
4457
  when Prism::BlockArgumentNode
4399
- symbol_block_return_type(block_arg, expected)
4458
+ forwarded_block_return_type(block_arg) || symbol_block_return_type(block_arg, expected)
4400
4459
  end
4401
4460
  end
4402
4461
 
4462
+ # Issue #1157 — `&` / `&blk` hands the callee the block this method was itself called with, so the callee's
4463
+ # `yield` is worth what the CALLER's block returns: the value {.current_yield_value_type} already holds for
4464
+ # this frame (the one a `yield` written here reads). Without it a name resolver that passes its block on
4465
+ # (`def walk(x, &) = first_hit(x, &)`) read as `Dynamic[top]` through the forward and was skipped by
4466
+ # `sig-gen`. Nil when the argument is not a forwarded block or the call site passed no block.
4467
+ def forwarded_block_return_type(block_arg)
4468
+ frame = scope.match_frame
4469
+ return nil unless frame&.forwards_block?(block_arg)
4470
+
4471
+ ExpressionTyper.current_yield_value_type
4472
+ end
4473
+
4403
4474
  # The scope a block body is typed under: the surrounding scope plus the parameter bindings the receiving
4404
4475
  # method's signature implies, with the #587 (b) `captured` binding ({#repeating_captured_bindings}) laid
4405
4476
  # between the two, so the body reads what a captured local, instance variable, class variable or global
@@ -4494,6 +4565,14 @@ module Rigor
4494
4565
  # A failure in the re-answer falls back to the plain tail-only answer, for the reason
4495
4566
  # {#threaded_block_body_type} gives: a raise reaching `block_return_type_for` would report "no block".
4496
4567
  def tail_only_block_body_type(body, block_scope, captured)
4568
+ StatementEvaluator.provisional_returns { tail_only_block_body_type_unrecorded(body, block_scope, captured) }
4569
+ end
4570
+
4571
+ # Issue #2299 — the tail alone, typed from `block_scope`, is not the walk the method's returns are collected
4572
+ # from: a `return` in it reads a name the skipped prefix binds as unbound. Its records are provisional
4573
+ # ({StatementEvaluator.provisional_returns}): the statement walk's record of the same `return` replaces one,
4574
+ # and one stands only for a block the walk never enters (a block in expression position).
4575
+ def tail_only_block_body_type_unrecorded(body, block_scope, captured)
4497
4576
  return block_scope.type_of(body) unless block_body_threading_suppressed? && body.is_a?(Prism::StatementsNode)
4498
4577
 
4499
4578
  answered =
@@ -4,6 +4,7 @@ require "prism"
4
4
 
5
5
  require_relative "../source/constant_path"
6
6
  require_relative "../source/node_children"
7
+ require_relative "ivar_writer_census"
7
8
 
8
9
  module Rigor
9
10
  module Inference
@@ -26,6 +27,8 @@ module Rigor
26
27
  # kept apart from `[:defines, …]`.
27
28
  # - `[:main_mixin, name]` — a module a top-level `include`, `prepend` or `extend` mixes into `Object` (or
28
29
  # `main`), as written. A module RBS does not know may carry any method, so it reaches every class.
30
+ # - the instance-variable writer entries of {IvarWriterCensus} (issue #1154): `[:ivar_set, name]`,
31
+ # `[:ivar_set_class, class_name]` and `[:own_write, class_name, name, path]`.
29
32
  #
30
33
  # The census is a `Set` of those frozen entries, merged by union across files, the project pre-pass and the
31
34
  # `pre_eval:` files. It is recorded inside descents that already visit every node ({Collector#visit}), never
@@ -74,12 +77,40 @@ module Rigor
74
77
  census.include?(DEFINES_ANY) || names.any? { |name| census.include?([:defines, name]) }
75
78
  end
76
79
 
80
+ # #1154 — the {IvarWriterCensus} entries of `census`, for the incremental diff and its readers.
81
+ IVAR_KINDS = %i[ivar_set ivar_set_class own_write].to_set.freeze
82
+
83
+ def ivar_set_entries(census) = census.select { |entry| IVAR_KINDS.include?(entry.first) }.to_set
84
+
85
+ # The negative-dependency key an {IvarWriterCensus} entry fires, the one `UnseenIvarWriters` records.
86
+ def ivar_key(entry)
87
+ case entry.first
88
+ when :ivar_set then "ivar_set:#{entry[1]}"
89
+ when :ivar_set_class then "ivar_set_class:#{entry[1]}"
90
+ else "own_write:#{entry[1]}##{entry[2]}"
91
+ end
92
+ end
93
+
77
94
  # Whether some refinement may add `write`.
78
95
  def refines_write?(census) = census.include?(REFINES_ANY) || census.include?(%i[refines write])
79
96
 
80
97
  # The module names the top-level mixins spell.
81
98
  def main_mixins(census) = census.filter_map { |entry| entry[1] if entry[0] == :main_mixin }
82
99
 
100
+ # Issue #2158 — the `global_census:` dependency key a census entry moves, without the kind prefix: `alias:$x`,
101
+ # `defines:write`, `refines:write`, `defines_any`, `refines_any`, and `main_mixin` for every top-level mixin: its
102
+ # reader asks about each, and one shared key is a deliberate precision trade-off (a mixin moving anywhere
103
+ # re-checks every write that read them). The rules that read the census record these keys
104
+ # (`CheckRules#record_global_census`), and `Analysis::Incremental.changed_global_census_keys` fires them for
105
+ # every entry that appeared in or vanished from a file's census.
106
+ def dependency_key(entry)
107
+ case entry
108
+ when DEFINES_ANY then "defines_any"
109
+ when REFINES_ANY then "refines_any"
110
+ else entry[0] == :main_mixin ? "main_mixin" : "#{entry[0]}:#{entry[1]}"
111
+ end
112
+ end
113
+
83
114
  # A standalone walk, for a file no existing descent visits (a `pre_eval:` entry).
84
115
  def scan(root)
85
116
  collector = Collector.new(InEffectRefinements.new(root))
@@ -107,12 +138,16 @@ module Rigor
107
138
  def initialize(in_effect)
108
139
  @census = Set.new
109
140
  @refine_ranges = []
141
+ @ivar_writers = IvarWriterCensus.new(@census)
110
142
  @in_effect = in_effect
111
143
  end
112
144
 
113
145
  # A node the collector cannot read must not fail the analysis of its file, nor of every file the pre-pass
114
146
  # feeds: the census then counts it as a definition of every name, which only declines a report.
115
- def visit(node, top_level:)
147
+ # `owner` is the qualified name of the class whose body the node sits in, for {IvarWriterCensus} (nil where
148
+ # the host walk does not track it, or no class owns an instance there).
149
+ def visit(node, top_level:, owner: nil)
150
+ @ivar_writers.visit(node, owner)
116
151
  case node
117
152
  when Prism::AliasGlobalVariableNode then visit_global_alias(node)
118
153
  when Prism::DefNode then record(node, node.name)
@@ -0,0 +1,303 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../type"
6
+ require_relative "reflective_ivar_write"
7
+ require_relative "refined_dispatch/own_method"
8
+ require_relative "self_call_ivar_writes"
9
+
10
+ module Rigor
11
+ module Inference
12
+ # The intervening-call reset of instance variables. An implicit-self or `self.` call may write any instance
13
+ # variable of `self`, so past one each instance variable whose binding is narrower than its class-ivar seed reads
14
+ # the union of the two again; a call proven to write only some ({SelfCallIvarWrites.of}, issue #1596) widens only
15
+ # those. A call on another receiver cannot reach `self`'s instance variables and widens nothing.
16
+ #
17
+ # {.widen} is the reset itself, which the statement evaluator applies past a statement call
18
+ # (`StatementEvaluator#invalidate_ivars_for_intervening_call`). Issue #1963 — {.after_operand} applies it past
19
+ # the calls in a condition operand, where a narrowing an earlier operand or clause made would otherwise survive
20
+ # them: `if @x.nil? && reset!` read `@x` as `nil` in the body although `reset!` assigns it.
21
+ module InterveningIvarReset
22
+ # Nodes whose body does not run where it is written, or runs on another `self`: a `def`, and a class or
23
+ # module body, where `@x` is the class's own instance variable.
24
+ NON_RUNNING_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
25
+ # Issue #1402 — the nodes that run code the program does not show on `self`'s behalf: `yield` runs the
26
+ # method's block, and `super` the superclass's method on `self`.
27
+ UNKNOWN_CODE_NODES = [Prism::YieldNode, Prism::SuperNode, Prism::ForwardingSuperNode].freeze
28
+ # Issue #1402 — the names of a proc's call (`blk.call`, `blk.()`, `blk.yield`), which runs the block the method
29
+ # was given as `yield` does.
30
+ PROC_CALL_NAMES = Set[:call, :yield].freeze
31
+ # Issue #1402 — the calls whose block runs with `self` bound to the receiver: on a receiver that is provably
32
+ # another object ({.foreign_self_block?}), an instance variable the block writes, and a method it calls
33
+ # without a receiver, are that object's.
34
+ SELF_REBINDING_BLOCK_CALLS = Set[
35
+ :instance_eval, :instance_exec, :class_eval, :class_exec, :module_eval, :module_exec, :define_method,
36
+ :define_singleton_method
37
+ ].freeze
38
+ # The classes whose instances may be a class or module object: a `self` of one of them may be the receiver of
39
+ # `Foo.class_eval`, and an instance of one of them may be a `self` that is a class object.
40
+ CLASS_OBJECT_CLASSES = Set["Class", "Module", "Object", "BasicObject"].freeze
41
+ private_constant :NON_RUNNING_NODES, :UNKNOWN_CODE_NODES, :PROC_CALL_NAMES, :SELF_REBINDING_BLOCK_CALLS,
42
+ :CLASS_OBJECT_CLASSES
43
+
44
+ module_function
45
+
46
+ # True when `node` is a call that may write `self`'s instance variables: an implicit-self or `self.` call
47
+ # ({.self_call?}). Issue #1402 — and a `yield`, a `super`, or a proc's call on a local ({.proc_call?}), which
48
+ # run code the program does not show; {SelfCallIvarWrites.of} proves no write set for any of them.
49
+ def candidate?(node)
50
+ return true if UNKNOWN_CODE_NODES.any? { |klass| node.is_a?(klass) }
51
+ return false unless node.is_a?(Prism::CallNode)
52
+
53
+ self_call?(node) || proc_call?(node)
54
+ end
55
+
56
+ # Whether `node`, a call, has no receiver or `self` as its receiver.
57
+ def self_call?(node)
58
+ receiver = node.receiver
59
+ receiver.nil? || receiver.is_a?(Prism::SelfNode)
60
+ end
61
+
62
+ # Issue #1402 — `blk.call` / `blk.()` / `blk.yield` on a local: the method's own `&blk`, or a proc it was
63
+ # handed, runs code that may call back into `self`, as `yield` does. Read by name, since an unannotated
64
+ # `&blk` binds `Dynamic`.
65
+ def proc_call?(node)
66
+ PROC_CALL_NAMES.include?(node.name) && node.receiver.is_a?(Prism::LocalVariableReadNode)
67
+ end
68
+
69
+ # Issue #1402 — whether `call_node`'s block runs on an object that is provably not `self`: a
70
+ # {SELF_REBINDING_BLOCK_CALLS} call whose receiver, typed in `scope`, cannot be `self` ({.other_object?}).
71
+ # A receiver that may be `self` at run time (`obj = self; obj.instance_eval { … }`, a block parameter, an
72
+ # `Object` or `Dynamic` value, a class the analysis cannot order against `self`'s) is not foreign.
73
+ def foreign_self_block?(call_node, scope)
74
+ return false unless call_node.is_a?(Prism::CallNode) && SELF_REBINDING_BLOCK_CALLS.include?(call_node.name)
75
+ return false if self_call?(call_node)
76
+
77
+ other_object?(scope.type_of(call_node.receiver), scope)
78
+ rescue StandardError
79
+ false
80
+ end
81
+
82
+ # Whether a value of `type` cannot be `scope`'s `self`. Only an instance method of a class answers: `self` is
83
+ # then an instance of that class (one other than `Class`, `Module`, `Object` and `BasicObject`, and no module,
84
+ # declared in source or RBS), and a class object (`Singleton[X]`: `self.class`, a constant naming a class or
85
+ # module) is not one. Where `self` is a class object, or a module's instance (`module_function`, `extend
86
+ # self`), any receiver may be `self` — a subclass, the module itself — and none is foreign. An instance
87
+ # receiver is never foreign: its class may be a subclass of `self`'s that the environment cannot order.
88
+ def other_object?(type, scope)
89
+ self_type = scope.self_type
90
+ return false unless self_type.is_a?(Type::Nominal) && plain_class?(self_type.class_name, scope)
91
+
92
+ type.is_a?(Type::Singleton)
93
+ end
94
+
95
+ # Whether `class_name` names a class whose instances are no class or module object, and not a module.
96
+ def plain_class?(class_name, scope)
97
+ name = class_name.to_s.delete_prefix("::")
98
+ !CLASS_OBJECT_CLASSES.include?(name) && !RefinedDispatch::OwnMethod.module_name?(scope, name) &&
99
+ !module_subclass?(name, scope)
100
+ end
101
+
102
+ # Whether `name` may be a subclass of `Module` (`class Meta < Module`), whose instance is a module object.
103
+ # Read off the class's instance `Scope::ResolutionChain`, which resolves a superclass written relative to the
104
+ # subclass's namespace (`class Meta < Base` in `module NS`) as Ruby does: true when an entry is one the
105
+ # environment orders `:subclass` or `:equal` to `Module` (`Module` itself, `Class`, a project class whose
106
+ # signature declares it, an external superclass), when an external superclass names no class the
107
+ # environment knows (it may be `Module`), or when the chain is truncated or unsettled.
108
+ def module_subclass?(name, scope)
109
+ chain = Scope::ResolutionChain.for(scope, name, :instance, :methods)
110
+ return true if chain.truncated? || chain.unsettled?
111
+
112
+ environment = scope.environment
113
+ chain.search(scope) { |entry| module_entry?(entry, environment) || nil } ? true : false
114
+ end
115
+ private_class_method :module_subclass?
116
+
117
+ # Whether a chain entry makes the class a possible `Module` subclass: a project entry {.module_like?}, or an
118
+ # external superclass that names no class the environment knows, or one {.module_like?}. An external mixin
119
+ # (`include Enumerable`) does not.
120
+ def module_entry?(entry, environment)
121
+ return module_like?(entry.name.to_s, environment) unless entry.external?
122
+ return false unless entry.superclass_edge
123
+
124
+ known = entry.candidates.map { |candidate| candidate.to_s.delete_prefix("::") }
125
+ .select { |candidate| environment.class_known?(candidate) }
126
+ known.empty? || known.any? { |candidate| module_like?(candidate, environment) }
127
+ end
128
+ private_class_method :module_entry?
129
+
130
+ def module_like?(name, environment)
131
+ %i[subclass equal].include?(environment.class_ordering(name, "Module"))
132
+ end
133
+ private_class_method :module_like?
134
+ private_class_method :plain_class?
135
+
136
+ # Issue #1402 — `scope` past a statement call whose receiver or arguments run code the program does not
137
+ # show (`@items << yield`, `puts(yield)`, `log(blk.call)`, `items.each(&blk)`): {.widen} over every narrowed
138
+ # name, as past a statement `yield`. Every other call in them is the operand walk's.
139
+ def after_operands(call_node, scope)
140
+ return scope unless narrowed_ivars?(scope)
141
+
142
+ found = []
143
+ collect_calls(call_node.receiver, false, found, scope, unknown_only: true)
144
+ collect_calls(call_node.arguments, false, found, scope, unknown_only: true)
145
+ block = call_node.block
146
+ found << block if block.is_a?(Prism::BlockArgumentNode) && block.expression.is_a?(Prism::LocalVariableReadNode)
147
+ return scope if found.empty?
148
+
149
+ widen(scope, mark: true) { nil }
150
+ end
151
+
152
+ # Issue #1402 — `scope` past a call on another receiver whose literal block may make a {.candidate?} call
153
+ # (`[1].each { reset }`, `items.each { yield }`): {.widen} over the names those calls may write, as
154
+ # {.after_operand} widens past the calls in a condition. In a block that runs on another object
155
+ # ({.foreign_self_block?}) only a `yield`, a `super` or a proc's call counts: its implicit-self calls are that
156
+ # object's, while the method's block runs as it always does. A literal-name
157
+ # `instance_variable_set` on `self` ({ReflectiveIvarWrite.literal_name}) writes the one ivar it names, which
158
+ # the block's captured-binding write-back already joins, so it is not counted.
159
+ def after_block(call_node, scope)
160
+ block = call_node.block
161
+ return scope unless block.is_a?(Prism::BlockNode)
162
+ return scope unless narrowed_ivars?(scope)
163
+
164
+ calls = []
165
+ if foreign_self_block?(call_node, scope)
166
+ collect_calls(block, false, calls, scope, unknown_only: true)
167
+ else
168
+ collect_calls(block, false, calls, scope)
169
+ calls.reject! { |call| ReflectiveIvarWrite.literal_name(call) }
170
+ end
171
+ return scope if calls.empty?
172
+
173
+ widen(scope, mark: true) { proven_writes(calls, scope) }
174
+ end
175
+
176
+ # `scope` with each instance variable narrowed below its class-ivar seed widened back over the seed. The block
177
+ # answers the names the call may write, or nil for any; it runs at most once, and only when an instance
178
+ # variable is narrowed, since a proof costs a read of the callee. An instance variable whose binding carries
179
+ # the declaration mark keeps it (issue #1889). With `mark: true` a binding whose narrowing had ruled `nil` out
180
+ # takes the mark when widened, so a `nil` only the seed brings back is not fuel for
181
+ # `call.possible-nil-receiver` (ADR-58), as a declared one is not. A narrowing that kept `nil`
182
+ # (`return if @tag`, `@tag.nil?`) widens unmarked, so a read the program really makes on `nil` still reports.
183
+ def widen(scope, mark: false)
184
+ seed = class_ivar_seed(scope)
185
+ return scope if seed.nil? || seed.empty?
186
+
187
+ computed = false
188
+ writes = nil
189
+ seed.reduce(scope) do |widened, (name, seed_type)|
190
+ local_type = scope.ivar(name)
191
+ next widened if local_type.nil? || local_type == seed_type
192
+
193
+ unless computed
194
+ writes = yield
195
+ computed = true
196
+ end
197
+ next widened if writes && !writes.include?(name)
198
+
199
+ joined = Type::Combinator.union(local_type, seed_type)
200
+ widened = widened.with_ivar(name, joined)
201
+ marked = (mark && nil_free?(local_type)) || scope.declaration_sourced?(:ivar, name)
202
+ marked ? widened.seed_declaration_sourced_ivar(name, joined) : widened
203
+ end
204
+ end
205
+
206
+ # Issue #1963 — `scope` past the calls `node`, a condition operand, makes: {.widen} over the names every
207
+ # candidate call in it may write, or every name when one of them is not proven. A call's literal block may run,
208
+ # so its calls count; a `def`, a class or module body and (unless `run_lambdas`) a lambda literal run nothing
209
+ # on this `self` there.
210
+ #
211
+ # A binding whose narrowing ruled `nil` out takes the declaration mark when widened ({.widen}'s `mark:`): the
212
+ # reset forgets the narrowing without making the variable newly nil-reportable, since a call the proof cannot
213
+ # clear is far more often one that leaves the variable alone (`if @tag && items.any?` then `t = @tag;
214
+ # t.upcase`) than one that assigns `nil`. A `when` test passes `run_lambdas: true`, since its `===` runs a
215
+ # lambda literal's body.
216
+ def after_operand(node, scope, run_lambdas: false)
217
+ return scope unless narrowed_ivars?(scope)
218
+
219
+ calls = candidate_calls(node, run_lambdas, scope)
220
+ return scope if calls.empty?
221
+
222
+ widen(scope, mark: true) { proven_writes(calls, scope) }
223
+ end
224
+
225
+ # Whether a candidate call in `node` may write the instance variable `name`. A `when` test passes
226
+ # `run_lambdas: true`, since its `===` runs a lambda literal's body. A write to it in `node` is the caller's
227
+ # to check.
228
+ def may_write?(node, name, scope, run_lambdas: false)
229
+ calls = candidate_calls(node, run_lambdas, scope)
230
+ return false if calls.empty?
231
+
232
+ proven = proven_writes(calls, scope)
233
+ proven.nil? || proven.include?(name)
234
+ end
235
+
236
+ # The union of the names each call may write ({SelfCallIvarWrites.of}), or nil when one is not proven.
237
+ def proven_writes(calls, scope)
238
+ calls.reduce(Set.new) do |acc, call|
239
+ writes = SelfCallIvarWrites.of(call, scope)
240
+ break nil if writes.nil?
241
+
242
+ acc.merge(writes)
243
+ end
244
+ end
245
+
246
+ # The candidate calls under `node`, in source order.
247
+ def candidate_calls(node, run_lambdas, scope)
248
+ found = []
249
+ collect_calls(node, run_lambdas, found, scope)
250
+ found
251
+ end
252
+
253
+ # Issue #1402 — with `unknown_only: true` only the `yield`s, `super`s and proc calls ({.unknown_code?}) count,
254
+ # wherever they sit. A block that runs on another object ({.foreign_self_block?}) is read that way: its
255
+ # implicit-self calls are that object's, while the method's block runs as anywhere else.
256
+ def collect_calls(node, run_lambdas, found, scope, unknown_only: false)
257
+ return unless node.is_a?(Prism::Node)
258
+ return if NON_RUNNING_NODES.any? { |klass| node.is_a?(klass) }
259
+ return if !run_lambdas && node.is_a?(Prism::LambdaNode)
260
+
261
+ found << node if unknown_only ? unknown_code?(node) : candidate?(node)
262
+ node.compact_child_nodes.each do |child|
263
+ foreign = !unknown_only && child.is_a?(Prism::BlockNode) && foreign_self_block?(node, scope)
264
+ collect_calls(child, run_lambdas, found, scope, unknown_only: unknown_only || foreign)
265
+ end
266
+ end
267
+ private_class_method :collect_calls
268
+
269
+ # Issue #1402 — a `yield`, a `super` or a proc's call ({.proc_call?}).
270
+ def unknown_code?(node)
271
+ UNKNOWN_CODE_NODES.any? { |klass| node.is_a?(klass) } || (node.is_a?(Prism::CallNode) && proc_call?(node))
272
+ end
273
+ private_class_method :unknown_code?
274
+
275
+ def nil_free?(type)
276
+ Narrowing.narrow_non_nil(type) == type
277
+ end
278
+ private_class_method :nil_free?
279
+
280
+ # Whether any instance variable is bound narrower than its class-ivar seed: the only state {.widen} changes.
281
+ def narrowed_ivars?(scope)
282
+ seed = class_ivar_seed(scope)
283
+ return false if seed.nil? || seed.empty?
284
+
285
+ seed.any? do |name, seed_type|
286
+ local_type = scope.ivar(name)
287
+ !local_type.nil? && local_type != seed_type
288
+ end
289
+ end
290
+ private_class_method :narrowed_ivars?
291
+
292
+ def class_ivar_seed(scope)
293
+ self_type = scope.self_type
294
+ class_name =
295
+ case self_type
296
+ when Type::Nominal, Type::Singleton then self_type.class_name
297
+ end
298
+ class_name && scope.class_ivars_for(class_name)
299
+ end
300
+ private_class_method :class_ivar_seed
301
+ end
302
+ end
303
+ end