rigortype 0.4.0 → 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 (237) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  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/08-understanding-errors.md +3 -2
  25. data/docs/handbook/11-sig-gen.md +6 -0
  26. data/docs/handbook/appendix-liskov.md +5 -0
  27. data/docs/install.md +87 -15
  28. data/docs/manual/02-cli-reference.md +2 -2
  29. data/docs/manual/03-configuration.md +6 -1
  30. data/docs/manual/04-diagnostics.md +2 -1
  31. data/docs/manual/06-baseline.md +2 -0
  32. data/docs/manual/07-plugins.md +1 -1
  33. data/docs/manual/08-skills.md +4 -3
  34. data/docs/manual/12-caching.md +43 -4
  35. data/docs/manual/14-rails-quickstart.md +19 -11
  36. data/docs/manual/18-removing-dead-code.md +32 -2
  37. data/docs/manual/plugins/README.md +6 -1
  38. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  39. data/docs/manual/plugins/rigor-alba.md +60 -0
  40. data/docs/manual/plugins/rigor-minitest.md +22 -4
  41. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  42. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  43. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  44. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  45. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
  47. data/lib/rigor/analysis/check_rules.rb +407 -144
  48. data/lib/rigor/analysis/dependency_recorder.rb +215 -18
  49. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  50. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  51. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  52. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  53. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  54. data/lib/rigor/analysis/incremental.rb +29 -9
  55. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
  56. data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
  57. data/lib/rigor/analysis/incremental_session.rb +287 -50
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  59. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  60. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +43 -9
  64. data/lib/rigor/analysis/run_stats.rb +2 -2
  65. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
  68. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  69. data/lib/rigor/analysis/runner.rb +371 -49
  70. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  71. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  72. data/lib/rigor/analysis/template_unit_paths.rb +6 -4
  73. data/lib/rigor/analysis/template_units.rb +15 -5
  74. data/lib/rigor/analysis/worker_session.rb +11 -7
  75. data/lib/rigor/cache/descriptor.rb +40 -1
  76. data/lib/rigor/cache/file_digest.rb +28 -0
  77. data/lib/rigor/cache/incremental_snapshot.rb +92 -8
  78. data/lib/rigor/cache/store.rb +63 -3
  79. data/lib/rigor/cache/value_digest.rb +325 -0
  80. data/lib/rigor/cli/annotate_command.rb +5 -0
  81. data/lib/rigor/cli/baseline_command.rb +7 -18
  82. data/lib/rigor/cli/check_command.rb +85 -14
  83. data/lib/rigor/cli/doctor_command.rb +1 -1
  84. data/lib/rigor/cli/effects_report.rb +2 -2
  85. data/lib/rigor/cli/trace_command.rb +5 -0
  86. data/lib/rigor/cli/type_of_command.rb +5 -3
  87. data/lib/rigor/cli/unused_command.rb +16 -10
  88. data/lib/rigor/cli.rb +11 -3
  89. data/lib/rigor/config_audit.rb +3 -2
  90. data/lib/rigor/config_path.rb +29 -0
  91. data/lib/rigor/configuration/severity_profile.rb +11 -0
  92. data/lib/rigor/configuration.rb +52 -7
  93. data/lib/rigor/effects/plugin_facts.rb +6 -6
  94. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  95. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  96. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  97. data/lib/rigor/environment/lockfile_resolver.rb +10 -1
  98. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  99. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  100. data/lib/rigor/environment/rbs_loader.rb +284 -15
  101. data/lib/rigor/environment/required_features.rb +206 -0
  102. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  103. data/lib/rigor/environment.rb +34 -73
  104. data/lib/rigor/inference/acceptance.rb +28 -3
  105. data/lib/rigor/inference/block_parameter_binder.rb +29 -13
  106. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  107. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  108. data/lib/rigor/inference/captured_locals.rb +7 -1
  109. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  110. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  111. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  112. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  113. data/lib/rigor/inference/declaration_walk.rb +7 -0
  114. data/lib/rigor/inference/def_node_resolver.rb +26 -3
  115. data/lib/rigor/inference/definer_resolution.rb +266 -0
  116. data/lib/rigor/inference/expression_typer.rb +351 -61
  117. data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
  118. data/lib/rigor/inference/global_write_census.rb +7 -4
  119. data/lib/rigor/inference/guard_rebinding.rb +54 -13
  120. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  121. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  122. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  123. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  124. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  125. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  126. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  127. data/lib/rigor/inference/macro_block_self_type.rb +73 -14
  128. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  129. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  130. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  131. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  132. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  133. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  134. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  135. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  136. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
  137. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  138. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  139. data/lib/rigor/inference/module_function_state.rb +214 -0
  140. data/lib/rigor/inference/multi_target_binder.rb +21 -16
  141. data/lib/rigor/inference/narrowing.rb +166 -22
  142. data/lib/rigor/inference/object_mixins.rb +180 -0
  143. data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
  144. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  145. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  146. data/lib/rigor/inference/refine_census.rb +239 -0
  147. data/lib/rigor/inference/refine_self.rb +317 -0
  148. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  149. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  150. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  151. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  152. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  153. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  154. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  155. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
  156. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  157. data/lib/rigor/inference/scope_indexer.rb +2129 -354
  158. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  159. data/lib/rigor/inference/statement_evaluator.rb +149 -32
  160. data/lib/rigor/inference/string_mutation.rb +9 -3
  161. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  162. data/lib/rigor/inference/version_guard.rb +19 -10
  163. data/lib/rigor/language_server/project_context.rb +31 -0
  164. data/lib/rigor/language_server/server.rb +18 -0
  165. data/lib/rigor/plugin/base.rb +28 -8
  166. data/lib/rigor/plugin/inflector.rb +1 -1
  167. data/lib/rigor/plugin/io_boundary.rb +124 -7
  168. data/lib/rigor/plugin/isolation.rb +1 -1
  169. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  170. data/lib/rigor/plugin/trust_policy.rb +3 -3
  171. data/lib/rigor/project_environment.rb +29 -6
  172. data/lib/rigor/protection/discovery_seed.rb +18 -9
  173. data/lib/rigor/protection/mutation_cache.rb +4 -4
  174. data/lib/rigor/protection/mutator.rb +1 -1
  175. data/lib/rigor/reflection/constant_ancestors.rb +211 -46
  176. data/lib/rigor/reflection/constant_path.rb +33 -7
  177. data/lib/rigor/reflection.rb +41 -10
  178. data/lib/rigor/scope/discovery_index.rb +258 -3
  179. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  180. data/lib/rigor/scope/resolution_chain.rb +1032 -0
  181. data/lib/rigor/scope.rb +480 -328
  182. data/lib/rigor/sig_gen/alias_index.rb +1 -1
  183. data/lib/rigor/sig_gen/classification.rb +4 -1
  184. data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
  185. data/lib/rigor/sig_gen/generator.rb +77 -21
  186. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  187. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  188. data/lib/rigor/source/alias_names.rb +52 -0
  189. data/lib/rigor/source/literals.rb +3 -2
  190. data/lib/rigor/source/symbol_name.rb +20 -0
  191. data/lib/rigor/type/combinator.rb +59 -3
  192. data/lib/rigor/type/float_range.rb +4 -1
  193. data/lib/rigor/type/nominal.rb +6 -1
  194. data/lib/rigor/version.rb +1 -1
  195. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  196. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  197. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  198. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  199. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  200. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  201. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  202. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  203. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  204. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  205. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  206. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  207. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  208. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  209. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  210. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  211. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  212. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  213. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  214. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  215. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  216. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  217. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
  218. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  219. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  220. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  221. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  222. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  223. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  224. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  225. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  226. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  227. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  228. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  229. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  230. data/sig/rigor/environment.rbs +2 -0
  231. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  232. data/sig/rigor/scope.rbs +69 -4
  233. data/sig/rigor/source.rbs +4 -0
  234. data/sig/rigor.rbs +3 -1
  235. data/skills/rigor-project-init/SKILL.md +37 -11
  236. data/skills/rigor-project-init/references/02-configure.md +7 -2
  237. metadata +90 -1
@@ -9,6 +9,7 @@ require_relative "../source/node_walker"
9
9
  require_relative "../source/node_children"
10
10
  require_relative "../source/constant_path"
11
11
  require_relative "anonymous_meta_class"
12
+ require_relative "definer_resolution"
12
13
  require_relative "block_repetition"
13
14
  require_relative "block_parameter_binder"
14
15
  require_relative "body_fixpoint"
@@ -30,6 +31,7 @@ require_relative "match_rebinding"
30
31
  require_relative "element_read_widening"
31
32
  require_relative "hash_lookup_mutation"
32
33
  require_relative "indexed_narrowing"
34
+ require_relative "key_presence_guard"
33
35
  require_relative "index_write_widening"
34
36
  require_relative "method_dispatcher"
35
37
  require_relative "method_parameter_binder"
@@ -272,10 +274,16 @@ module Rigor
272
274
  # {OperandWalk} rooted inside an operand still records its later operands ({#walk_recorder}).
273
275
  # @param operand_types — the later operands' own values ({OperandWalk#types}) for the evaluator that runs a
274
276
  # call from the scope its operands left, read by {#type_operand}.
277
+ # @param opaque_class — true while the walk is inside the body of a class nothing names (issue #1518:
278
+ # a declined `class self::X`, a parse-error header), where `class_context` is empty without the code
279
+ # being at the top level. A `def` (or `class << self`) there runs under an untyped `self` instead of
280
+ # the top level's nil one, so its implicit-self calls answer `Dynamic` and no top-level rule fires.
275
281
  def initialize(scope:, tracer: nil, on_enter: nil, class_context: [].freeze, # rubocop:disable Metrics/ParameterLists
276
282
  lexical_nesting: EMPTY_NESTING, converged_loop_recording: false, next_scope_sink: nil,
277
- operand_scope: nil, in_operand: false, operand_recorder: nil, operand_types: nil)
283
+ operand_scope: nil, in_operand: false, operand_recorder: nil, operand_types: nil,
284
+ opaque_class: false)
278
285
  @scope = scope
286
+ @opaque_class = opaque_class
279
287
  @tracer = tracer
280
288
  @on_enter = on_enter
281
289
  @class_context = class_context.freeze
@@ -345,7 +353,7 @@ module Rigor
345
353
  @on_enter&.call(node, @scope)
346
354
 
347
355
  handler = HANDLERS[node.class]
348
- return forget_implicit_call_guards(node, send(handler, node)) if handler
356
+ return forget_aliased_key_guards(node, forget_implicit_call_guards(node, send(handler, node))) if handler
349
357
 
350
358
  # Default: the node is treated as a pure expression. Type it through the existing expression typer (which
351
359
  # observes the current scope's locals) and leave the scope unchanged, but for the match globals a call in it
@@ -366,6 +374,16 @@ module Rigor
366
374
  [type, after.forget_guard_narrowings]
367
375
  end
368
376
 
377
+ # Issue #1703 — a write that stores a `key?` guard's receiver or key root elsewhere (`g = h`) aliases it, so
378
+ # the guard ends ({KeyPresenceGuard.invalidate_after_write}).
379
+ def forget_aliased_key_guards(node, result)
380
+ type, after = result
381
+ return result unless after.is_a?(Scope)
382
+
383
+ guarded = KeyPresenceGuard.invalidate_after_write(node, after)
384
+ guarded.equal?(after) ? result : [type, guarded]
385
+ end
386
+
369
387
  # Issue #1359 — the nodes whose handler runs their parts in order, each from the scope the parts before it
370
388
  # left, so a `$_` reader among them forgets it where it runs ({LastLine}) and a read before it keeps the
371
389
  # narrowing.
@@ -625,7 +643,8 @@ module Rigor
625
643
  # lands.
626
644
  def eval_ivar_write(node)
627
645
  rhs_type, post_rhs = sub_eval(node.value, scope)
628
- bound = post_rhs.with_ivar(node.name, rhs_type)
646
+ # Issue #1446 — a write ends a class guard's narrowing of the variable, as it does a global's.
647
+ bound = post_rhs.with_ivar(node.name, rhs_type).without_ivar_guard(node.name)
629
648
  bound = bound.with_ivar_origin(node.name, rhs_origin(node.value, post_rhs, rhs_type))
630
649
  cause = optimistic_rhs_origin(node.value, post_rhs)
631
650
  bound = bound.with_optimistic_ivar(node.name, cause, miss: optimistic_rhs_miss(node.value, post_rhs)) if cause
@@ -806,7 +825,8 @@ module Rigor
806
825
  end
807
826
 
808
827
  def rebind_variable(target_scope, kind, name, type)
809
- target_scope.public_send(VAR_KIND_BUILDERS.fetch(kind), name, type)
828
+ bound = target_scope.public_send(VAR_KIND_BUILDERS.fetch(kind), name, type)
829
+ kind == :ivar ? bound.without_ivar_guard(name) : bound
810
830
  end
811
831
 
812
832
  def compound_result_type(current, rhs, operator)
@@ -2431,6 +2451,12 @@ module Rigor
2431
2451
  case index_node
2432
2452
  when Prism::LocalVariableTargetNode
2433
2453
  scope.with_local(index_node.name, element_type)
2454
+ when Prism::InstanceVariableTargetNode
2455
+ # Issue #1446 — `for @io in xs` writes the ivar each iteration, which ends a class guard's narrowing of it,
2456
+ # and `for $out in xs` the global, which ends any guard's narrowing of it.
2457
+ scope.with_ivar(index_node.name, element_type).without_ivar_guard(index_node.name)
2458
+ when Prism::GlobalVariableTargetNode
2459
+ scope.with_global(index_node.name, element_type)
2434
2460
  when Prism::IndexTargetNode
2435
2461
  widen_index_target(index_node, element_type, scope, type_scope: scope)
2436
2462
  when Prism::MultiTargetNode
@@ -2698,13 +2724,31 @@ module Rigor
2698
2724
  # that half rather than this one deriving it from the frames.
2699
2725
  def eval_class_or_module(node)
2700
2726
  path = node.constant_path
2701
- frame = ClassFrame.new(name: Source::ConstantPath.qualified_name(path), singleton: false)
2727
+ name = Source::ConstantPath.qualified_name(path)
2728
+ # A header Prism recovered without a constant (`class foo`, a parse error) names no class: its body
2729
+ # runs under an untyped `self` and no class frame, so nothing is filed under an empty name.
2730
+ return eval_unnameable_class_body(node) if name.nil?
2731
+ # `class self::X` the indexer declined (opaque `self`, e.g. `REGISTRY.first.class_eval`) is not the
2732
+ # lexical `X` that `qualified_name` renders; a nameable `self::` header is in the identity table.
2733
+ return eval_unnameable_class_body(node) if declined_self_header?(path)
2734
+
2735
+ frame = ClassFrame.new(name: name, singleton: false)
2702
2736
  new_context = Source::ConstantPath.rooted?(path) ? [frame] : @class_context + [frame]
2703
2737
  body_type, _body_scope = eval_class_body(node, new_context,
2704
2738
  Source::ConstantPath.pushed_nesting(@lexical_nesting, path))
2705
2739
  [body_type, scope]
2706
2740
  end
2707
2741
 
2742
+ def declined_self_header?(path)
2743
+ path.is_a?(Prism::ConstantPathNode) && path.parent.is_a?(Prism::SelfNode) &&
2744
+ !scope.declared_types.key?(path)
2745
+ end
2746
+
2747
+ def eval_unnameable_class_body(node)
2748
+ body_type, _body_scope = eval_class_body(node, [], @lexical_nesting, opaque_self: true)
2749
+ [body_type, scope]
2750
+ end
2751
+
2708
2752
  # `class << expr; body; end`. When `expr` is `self`, the body defines class methods on the immediate enclosing
2709
2753
  # class — the innermost frame flips to `singleton: true` so a nested `def foo` resolves through `singleton_method`
2710
2754
  # rather than `instance_method`. For non-`self` expressions we cannot statically resolve the receiver, so we keep
@@ -2961,6 +3005,8 @@ module Rigor
2961
3005
  # outer receiver is itself a chain node (e.g. `x.last << y`) do NOT drop narrowings keyed on `x` — only direct
2962
3006
  # calls against the root variable invalidate the chain.
2963
3007
  post_scope = IndexedNarrowing.invalidate_chain_after_call(call_node: node, current_scope: post_scope)
3008
+ # Issue #1703 — a non-literal-key `key?` guard the call may break ({KeyPresenceGuard.breaks?}).
3009
+ post_scope = KeyPresenceGuard.invalidate_after_call(node, post_scope)
2964
3010
  # B2.2 — intervening method call ivar invalidation. An implicit-self / self-receiver call could mutate any ivar
2965
3011
  # of the enclosing class (we cannot prove purity without an effect system). Reset each ivar whose current local
2966
3012
  # binding has narrowed below the class-ivar seed back to the seed itself, so a subsequent `if @flag` predicate
@@ -3526,15 +3572,19 @@ module Rigor
3526
3572
  # does not type as a class object (a gem class with no RBS) keeps the name as written: the body is still some
3527
3573
  # class's body, and leaving it on the enclosing `self` made a `refine` at the file's top level report every
3528
3574
  # implicit-self call in it as `call.unresolved-toplevel`.
3575
+ #
3576
+ # Issue #1689 — a `refine` call the file's {InEffectRefinements} places in a class body is the class's own
3577
+ # method, not `Module#refine`, so its block is entered as any other block.
3529
3578
  def refined_class_context(node)
3530
3579
  target = ScopeIndexer.refine_target(node)
3531
3580
  return nil if target.nil?
3532
3581
 
3533
- refined = scope.type_of(target)
3534
- name = refined.is_a?(Type::Singleton) ? refined.class_name : Source::ConstantPath.qualified_name(target)
3582
+ return nil if InEffectRefinements.class_body_refine?(scope, node)
3583
+
3584
+ name = ScopeIndexer.refined_class_name(target, scope)
3535
3585
  return nil if name.nil?
3536
3586
 
3537
- [ClassFrame.new(name: name.delete_prefix("::"), singleton: false, refinement: true)]
3587
+ [ClassFrame.new(name: name, singleton: false, refinement: true)]
3538
3588
  end
3539
3589
 
3540
3590
  # The block calls whose body `return` leaves only the block ({ReturnBarrier.block_call?}). Like a `->` body
@@ -3568,7 +3618,9 @@ module Rigor
3568
3618
  narrowed = DefineMethodBlockSelf.narrow_self_type_for(
3569
3619
  scope: scope, call_node: call_node
3570
3620
  )
3571
- narrowed ? block_entry.with_self_type(narrowed) : block_entry
3621
+ return block_entry unless narrowed
3622
+
3623
+ block_entry.with_block_self_type(narrowed, keeps_unknown: scope.block_self_narrowing_unknown?(call_node))
3572
3624
  end
3573
3625
 
3574
3626
  # Enters a meta-new `block` as the body of the class `class_context` names: `self_type` is that class's
@@ -3583,8 +3635,17 @@ module Rigor
3583
3635
  # record a `Outer::K` entry: that is a rung Ruby's constant lookup does not have. The `class_context`
3584
3636
  # frame is still pushed — it is what a nested `def` registers its method under — which is exactly the
3585
3637
  # divergence that makes the chain a separate record rather than a view of the frame stack.
3638
+ #
3639
+ # Issue #1759 — a `refine X do … end` block runs on the refinement module, not on X, which Rigor does not
3640
+ # model: the body keeps X's frame and singleton so its `def`s and implicit-self calls resolve as before, but
3641
+ # carries the unknown-`self` mark ({Scope#block_self_unknown?}), so an explicit `self.import_methods(M)` is
3642
+ # not checked against `singleton(X)`. The indexer leaves a value-position refine block unknown the same way.
3586
3643
  def enter_meta_class_body(block, block_entry, class_context)
3587
- entry = block_entry.with_self_type(self_type_for_class_body(class_context)).with_singleton_class_body(false)
3644
+ # Issue #1717 — the body runs on the new class whatever encloses it, so its `self` is known.
3645
+ entry = block_entry.with_block_self_type(self_type_for_class_body(class_context),
3646
+ keeps_unknown: class_context.last.refinement &&
3647
+ Scope::REFINEMENT_BLOCK_SELF)
3648
+ .with_singleton_class_body(false)
3588
3649
  sub_eval(block, stamp_nesting(entry, @lexical_nesting), class_context: class_context)
3589
3650
  end
3590
3651
 
@@ -5107,27 +5168,31 @@ module Rigor
5107
5168
  # ({ExpressionTyper#block_body_self_narrowing}) already applies the same contract to block-return
5108
5169
  # typing; without it here the recorded per-node scopes — what `dump_type`/`assert_type` and the
5109
5170
  # survey read — keep the enclosing `self_type` and every DSL call inside stays `Dynamic[top]`.
5110
- narrowed = call_node && narrow_macro_block_self(call_node)
5111
- scope_with_params = scope_with_params.with_self_type(narrowed) if narrowed
5171
+ # Issue #1667 — the entry's `refinements:` are put in effect in the body alongside.
5172
+ match = call_node && macro_block_match(call_node)
5173
+ if match
5174
+ keeps_unknown = scope.block_self_narrowing_unknown?(call_node)
5175
+ scope_with_params = MacroBlockSelfType.apply(scope_with_params, match, keeps_unknown: keeps_unknown)
5176
+ end
5112
5177
  block_local_names(block_node).reduce(scope_with_params) do |acc, name|
5113
5178
  acc.with_local(name, Type::Combinator.constant_of(nil))
5114
5179
  end
5115
5180
  end
5116
5181
 
5117
- # The receiver an ADR-16 `block_as_methods:` match is keyed on: the explicit receiver's type, or the
5182
+ # The ADR-16 `block_as_methods:` match for `call_node`, keyed on the explicit receiver's type, or on the
5118
5183
  # current `self_type` for an implicit-self DSL call (the `params do` / `namespace do` shapes, whose
5119
5184
  # receiver is the enclosing `Singleton[X]`). A miss leaves the entry scope as built — the false-
5120
5185
  # positive-safe direction.
5121
- def narrow_macro_block_self(call_node)
5186
+ def macro_block_match(call_node)
5122
5187
  receiver_type =
5123
5188
  if call_node.receiver
5124
5189
  explicit_receiver_type(call_node)
5125
5190
  else
5126
- scope.self_type
5191
+ MacroBlockSelfType.implicit_receiver_type(scope)
5127
5192
  end
5128
5193
  return nil if receiver_type.nil?
5129
5194
 
5130
- MacroBlockSelfType.narrow_self_type_for(
5195
+ MacroBlockSelfType.match_for(
5131
5196
  scope: scope, call_node: call_node, receiver_type: receiver_type
5132
5197
  )
5133
5198
  rescue StandardError
@@ -5160,7 +5225,8 @@ module Rigor
5160
5225
  method_name: call_node.name,
5161
5226
  arg_types: arg_types,
5162
5227
  environment: scope.environment,
5163
- scope: scope
5228
+ scope: scope,
5229
+ call_node: call_node
5164
5230
  )
5165
5231
  rescue StandardError
5166
5232
  []
@@ -5175,7 +5241,7 @@ module Rigor
5175
5241
 
5176
5242
  # ----- def/class helpers -----
5177
5243
 
5178
- def eval_class_body(node, new_context, new_nesting = @lexical_nesting)
5244
+ def eval_class_body(node, new_context, new_nesting = @lexical_nesting, opaque_self: false)
5179
5245
  return [Type::Combinator.constant_of(nil), scope] if node.body.nil?
5180
5246
 
5181
5247
  # Class/module bodies run in a fresh scope: the outer scope's locals are NOT visible inside `class Foo; ...
@@ -5183,15 +5249,24 @@ module Rigor
5183
5249
  # A-engine: `self` inside a class body is the class object itself, so we set `self_type` to
5184
5250
  # `Singleton[<qualified>]`.
5185
5251
  fresh = build_fresh_body_scope
5186
- body_self = self_type_for_class_body(new_context)
5252
+ # Issue #1518 — `class << self` inside an unnameable class body keeps that body's opacity: it opens the
5253
+ # singleton of a class nothing names, and its empty frame stack is not the top level.
5254
+ singleton_body = node.is_a?(Prism::SingletonClassNode)
5255
+ opaque_self ||= singleton_body && @opaque_class && new_context.empty?
5256
+ # Issue #1763 — a `class << self` in a `refine X` block (the refinement frame {#singleton_context_for} built)
5257
+ # opens the refinement module's singleton class, which Rigor does not model, as the `def`s in it already read
5258
+ # ({#self_type_for_method_body}); `Singleton[X]` there checked `self.m` and a local holding `self` against X's
5259
+ # class methods. A `class << X` names X's real singleton class and takes a frame without the flag.
5260
+ untyped_self = opaque_self || (singleton_body && new_context.last&.refinement)
5261
+ body_self = untyped_self ? Type::Combinator.untyped : self_type_for_class_body(new_context)
5187
5262
  fresh = fresh.with_self_type(body_self) if body_self
5188
5263
  # Issue #963 — `self` in a `class << ...` body is the SINGLETON class, which shares the `Singleton[X]`
5189
5264
  # carrier a `class X` body gets. The mark is the only thing that tells the two apart downstream, and a
5190
5265
  # `def` reached from this body clears it by starting from a fresh scope.
5191
- fresh = fresh.with_singleton_class_body(node.is_a?(Prism::SingletonClassNode))
5266
+ fresh = fresh.with_singleton_class_body(singleton_body)
5192
5267
  fresh = fresh.with_match_frame(node.body)
5193
5268
  fresh = stamp_nesting(fresh, new_nesting)
5194
- sub_eval(node.body, fresh, class_context: new_context, lexical_nesting: new_nesting)
5269
+ sub_eval(node.body, fresh, class_context: new_context, lexical_nesting: new_nesting, opaque_class: opaque_self)
5195
5270
  end
5196
5271
 
5197
5272
  def build_method_entry_scope(def_node) # rubocop:disable Metrics/AbcSize
@@ -5218,7 +5293,7 @@ module Rigor
5218
5293
  # then receive the parameter bindings. Slice 7 phase 2: instance defs ALSO seed their `ivars` map from the
5219
5294
  # class-level accumulator so `def get; @x; end` reads the type that a sibling `def init; @x = 1; end` wrote.
5220
5295
  fresh = build_fresh_body_scope
5221
- body_self = self_type_for_method_body(singleton: singleton)
5296
+ body_self = self_type_for_method_body(singleton: singleton, receiver: def_node.receiver)
5222
5297
  fresh = fresh.with_self_type(body_self) if body_self
5223
5298
  # A `def` opens no new `Module.nesting` entry — the body resolves constants against the chain of the
5224
5299
  # declaration that encloses it, which is what the evaluator is already carrying (#652).
@@ -5353,12 +5428,16 @@ module Rigor
5353
5428
  # chain's throwaway intermediate Scopes were a top `Scope#rebuild` source (ADR-44). Local-empty by design; the
5354
5429
  # discovery index is inherited whole by reference (ADR-53 Track A), so a table added to the index can no longer
5355
5430
  # be dropped here by a missed per-field copy.
5431
+ #
5432
+ # Issue #1667 — a plugin-declared refined block's modules carry into a `def` or class body written in it: CRuby
5433
+ # defines the method with the block's cref, and a class body's cref inherits the outer one's refinements.
5356
5434
  Scope.new(
5357
5435
  environment: scope.environment,
5358
5436
  locals: {}.freeze,
5359
5437
  source_path: scope.source_path,
5360
5438
  discovery: scope.discovery,
5361
- dynamic_origins: scope.dynamic_origins
5439
+ dynamic_origins: scope.dynamic_origins,
5440
+ declared_refinements: scope.declared_refinements
5362
5441
  )
5363
5442
  end
5364
5443
 
@@ -5408,9 +5487,15 @@ module Rigor
5408
5487
  # `Nominal[Foo]`.
5409
5488
  #
5410
5489
  # Returns nil for top-level defs that have no enclosing class.
5411
- def self_type_for_method_body(singleton:)
5490
+ def self_type_for_method_body(singleton:, receiver: nil)
5412
5491
  path = current_class_path
5413
- return nil if path.nil?
5492
+ # Issue #1518 — a `def` in a class nothing names has a receiver, just not one Rigor can type; nil would
5493
+ # read its body as the top level and its implicit-self calls as unresolved top-level calls.
5494
+ return (Type::Combinator.untyped if @opaque_class) if path.nil?
5495
+ # Issue #1759 — a `def self.m` in a `refine X` block (or a `def m` in a `class << self` there) is a method
5496
+ # of the refinement module, which Rigor does not model; reading it as `Singleton[X]` checked its
5497
+ # explicit-self calls against X's class methods. A `def X.m` there does define X's class method.
5498
+ return Type::Combinator.untyped if singleton && @class_context.last.refinement && refinement_self_def?(receiver)
5414
5499
 
5415
5500
  if singleton
5416
5501
  Type::Combinator.singleton_of(path)
@@ -5419,6 +5504,10 @@ module Rigor
5419
5504
  end
5420
5505
  end
5421
5506
 
5507
+ # Issue #1759 — whether a singleton `def` under a refinement frame defines a method on the refinement module
5508
+ # (`def self.m`, or `def m` in a `class << self`) rather than on an explicitly named receiver (`def X.m`).
5509
+ def refinement_self_def?(receiver) = receiver.nil? || receiver.is_a?(Prism::SelfNode)
5510
+
5422
5511
  def singleton_context_for(node)
5423
5512
  case node.expression
5424
5513
  when Prism::SelfNode
@@ -5426,7 +5515,7 @@ module Rigor
5426
5515
 
5427
5516
  outer = @class_context[0..-2]
5428
5517
  last = @class_context.last
5429
- outer + [ClassFrame.new(name: last.name, singleton: true)]
5518
+ outer + [ClassFrame.new(name: last.name, singleton: true, refinement: last.refinement)]
5430
5519
  when Prism::ConstantReadNode, Prism::ConstantPathNode
5431
5520
  target = singleton_constant_target(node.expression)
5432
5521
  return @class_context unless target
@@ -5549,10 +5638,14 @@ module Rigor
5549
5638
  # `on_enter: nil` evaluates without recording into the per-node scope index — for a pass whose scopes are not
5550
5639
  # the ones the index should keep. `next_scope_sink:` is replaced only by {#evaluate_invocation} and
5551
5640
  # {#loop_iteration}.
5552
- def sub_eval(node, with_scope, class_context: @class_context, lexical_nesting: @lexical_nesting,
5553
- on_enter: @on_enter, next_scope_sink: @next_scope_sink, operand_recorder: @operand_recorder)
5641
+ # `opaque_class:` survives only while the frame stack stays empty: a named class opened inside an
5642
+ # unnameable body gets a frame, and its `def`s a nominal `self`, as they did before #1518.
5643
+ def sub_eval(node, with_scope, class_context: @class_context, lexical_nesting: @lexical_nesting, # rubocop:disable Metrics/ParameterLists
5644
+ on_enter: @on_enter, next_scope_sink: @next_scope_sink, operand_recorder: @operand_recorder,
5645
+ opaque_class: @opaque_class && class_context.empty?)
5554
5646
  evaluator_at(with_scope, class_context: class_context, lexical_nesting: lexical_nesting, on_enter: on_enter,
5555
- next_scope_sink: next_scope_sink, operand_recorder: operand_recorder).evaluate(node)
5647
+ next_scope_sink: next_scope_sink, operand_recorder: operand_recorder,
5648
+ opaque_class: opaque_class).evaluate(node)
5556
5649
  end
5557
5650
 
5558
5651
  # An evaluator over `with_scope` that inherits everything else from this one. `operand_scope:` and
@@ -5560,7 +5653,8 @@ module Rigor
5560
5653
  # operands left.
5561
5654
  def evaluator_at(with_scope, class_context: @class_context, lexical_nesting: @lexical_nesting, # rubocop:disable Metrics/ParameterLists
5562
5655
  on_enter: @on_enter, next_scope_sink: @next_scope_sink, operand_scope: nil,
5563
- in_operand: @in_operand, operand_recorder: @operand_recorder, operand_types: nil)
5656
+ in_operand: @in_operand, operand_recorder: @operand_recorder, operand_types: nil,
5657
+ opaque_class: @opaque_class)
5564
5658
  StatementEvaluator.new(
5565
5659
  scope: with_scope,
5566
5660
  tracer: tracer,
@@ -5572,7 +5666,8 @@ module Rigor
5572
5666
  operand_scope: operand_scope,
5573
5667
  in_operand: in_operand,
5574
5668
  operand_recorder: operand_recorder,
5575
- operand_types: operand_types
5669
+ operand_types: operand_types,
5670
+ opaque_class: opaque_class
5576
5671
  )
5577
5672
  end
5578
5673
 
@@ -5675,6 +5770,12 @@ module Rigor
5675
5770
  case ref
5676
5771
  when Prism::LocalVariableTargetNode
5677
5772
  scope.with_local(ref.name, exception_type)
5773
+ when Prism::InstanceVariableTargetNode
5774
+ # Issue #1446 — `rescue => @io` writes the ivar, which ends a class guard's narrowing of it, and `rescue =>
5775
+ # $out` the global, which ends any guard's narrowing of it.
5776
+ scope.with_ivar(ref.name, exception_type).without_ivar_guard(ref.name)
5777
+ when Prism::GlobalVariableTargetNode
5778
+ scope.with_global(ref.name, exception_type)
5678
5779
  when Prism::IndexTargetNode
5679
5780
  widen_index_target(ref, exception_type, scope, type_scope: scope)
5680
5781
  else
@@ -5818,7 +5919,7 @@ module Rigor
5818
5919
  def source_decomposition_projection(subject_type, method_name, arg_types, scope)
5819
5920
  return nil unless subject_type.is_a?(Type::Nominal)
5820
5921
 
5821
- def_node = scope.discovered_def_nodes[subject_type.class_name]&.[](method_name)
5922
+ def_node = own_definer_node(scope, subject_type.class_name, method_name)
5822
5923
  return nil if def_node.nil?
5823
5924
 
5824
5925
  result = scope.user_method_return(def_node, subject_type, arg_types)
@@ -5827,6 +5928,22 @@ module Rigor
5827
5928
  result
5828
5929
  end
5829
5930
 
5931
+ # The `def` node the receiver's OWN class defines `method_name` with, or nil. ADR-119 C2-e (#1615): asked of
5932
+ # `DefinerResolution`, so a slot rebuilt from an ADR-85 seed bundle is resolved to a node and the read files
5933
+ # its ADR-46 edge. UNKNOWN answers nil: an unsettled or forked chain, or a contested slot once ADR-119 C1d
5934
+ # fills the siblings. Conditional or reopened defs are not contested until C1d. A definer further up the
5935
+ # chain (a prepended or included module, a superclass) is not the own class's, so the owner check declines it.
5936
+ def own_definer_node(scope, class_name, method_name)
5937
+ case DefinerResolution.resolve(scope, class_name, method_name, :instance, question: :definer)
5938
+ in DefinerResolution::Known(answer: [node, owner])
5939
+ owner == class_name ? node : nil
5940
+ in DefinerResolution::UNKNOWN # the case/in contract needs one arm per answer
5941
+ nil
5942
+ in DefinerResolution::ABSENT # rubocop:disable Lint/DuplicateBranch -- see above
5943
+ nil
5944
+ end
5945
+ end
5946
+
5830
5947
  # A `StructInstance`'s own projection — `Tuple` of its member values for `deconstruct`, `HashShape` of
5831
5948
  # its members for `deconstruct_keys` — or nil when the subject is another carrier or the projection would
5832
5949
  # be unsound.
@@ -22,6 +22,9 @@ module Rigor
22
22
  # the effect classifier and the effect catalogue's `mutators: string` cite it rather than keep a list of their
23
23
  # own (ADR-103 WD3). A spec holds it to every bang method `String` defines plus the non-bang mutators reflection
24
24
  # cannot find by name.
25
+ #
26
+ # The last row is Ruby 4.1's bit operations (#1691), which `data/core_overlay/string.rbs` declares whatever Ruby
27
+ # runs the analysis; they rewrite bytes in place and keep the byte length.
25
28
  MUTATORS = %i[
26
29
  << concat insert prepend replace clear []= slice!
27
30
  setbyte bytesplice append_as_bytes force_encoding
@@ -29,18 +32,21 @@ module Rigor
29
32
  upcase! downcase! capitalize! swapcase! reverse!
30
33
  strip! lstrip! rstrip! chomp! chop!
31
34
  delete_prefix! delete_suffix! encode! scrub! unicode_normalize!
35
+ bit_set bit_clear bit_flip bitwise_not! bitwise_and! bitwise_or! bitwise_xor!
32
36
  ].to_set.freeze
33
37
 
34
38
  # The {MUTATORS} that CANNOT leave a non-empty receiver empty, and so keep a `non-empty-string` refinement:
35
39
  # appenders (`<<`, `concat`, `insert`, `prepend`, `append_as_bytes`), and rewrites that keep at least one
36
40
  # character (`setbyte`, `force_encoding`, the case mappings, `reverse!`, `succ!` / `next!`, `squeeze!`,
37
- # `unicode_normalize!`). A whitelist, as `RefinementMutation::EMPTY_PRESERVING` is for Array and Hash: a mutator
38
- # missing here retracts the witness, which costs precision, where a mutator missing from a list of emptiers kept
39
- # the witness of a string it had just emptied — `tr!` and `tr_s!` did, since `"a".tr!("a", "")` is `""`.
41
+ # `unicode_normalize!`, and the bit operations, which keep every byte). A whitelist, as
42
+ # `RefinementMutation::EMPTY_PRESERVING` is for Array and Hash: a mutator missing here retracts the witness, which
43
+ # costs precision, where a mutator missing from a list of emptiers kept the witness of a string it had just
44
+ # emptied — `tr!` and `tr_s!` did, since `"a".tr!("a", "")` is `""`.
40
45
  EMPTY_PRESERVING = %i[
41
46
  << concat insert prepend append_as_bytes
42
47
  setbyte force_encoding
43
48
  upcase! downcase! capitalize! swapcase! reverse! succ! next! squeeze! unicode_normalize!
49
+ bit_set bit_clear bit_flip bitwise_not! bitwise_and! bitwise_or! bitwise_xor!
44
50
  ].to_set.freeze
45
51
 
46
52
  # `tr!` / `tr_s!` map every character they match to one character of the replacement (padded with its last), so
@@ -333,7 +333,8 @@ module Rigor
333
333
 
334
334
  def collect_included_do_calls(body)
335
335
  body.body.flat_map do |stmt|
336
- next [] unless stmt.is_a?(Prism::CallNode) && stmt.receiver.nil? && stmt.name == :included && stmt.block
336
+ included_do = stmt.is_a?(Prism::CallNode) && stmt.receiver.nil? && stmt.name == :included
337
+ next [] unless included_do && stmt.block.is_a?(Prism::BlockNode)
337
338
 
338
339
  block_body = stmt.block.body
339
340
  next [] unless block_body.respond_to?(:body)
@@ -33,6 +33,10 @@ module Rigor
33
33
  # Prism *parse* version (it decides which syntax is accepted), it is not threaded to the inference
34
34
  # layer, and its default (`"4.0"`) is a default rather than a user statement about the runtime.
35
35
  #
36
+ # The one exception is the Ruby-deprecation rule (the ADR-47 WD5 amendment, #1692). An explicit
37
+ # `target_ruby` is a user statement about the runtime, and that rule passes it as `stated_ruby:` to ask
38
+ # whether a deprecated call can run on the stated Ruby. Narrowing and the dead-arm filter never pass it.
39
+ #
36
40
  # ## What is folded (and what is deliberately not)
37
41
  #
38
42
  # * `RUBY_VERSION <cmp> "x.y.z"` — compared with **String** semantics, because that is what runs.
@@ -91,9 +95,13 @@ module Rigor
91
95
  private_constant :GEM_VERSION_PATH
92
96
 
93
97
  # @param node — an `if` / `unless` predicate
94
- # @return `:truthy` / `:falsey` when the guard is decidable on the analyzer's Ruby,
98
+ # @param stated_ruby — ADR-47 WD5 amendment (#1692): a `x.y.z` version to read `RUBY_VERSION` as instead of
99
+ # the analyzer's own, for the Ruby-deprecation rules only ({Configuration#stated_runtime_ruby}). Under it
100
+ # nothing else is readable — `RUBY_ENGINE` and the default gems' `VERSION`s belong to the analyzer's
101
+ # interpreter, not to the stated one — so a guard on them answers nil.
102
+ # @return `:truthy` / `:falsey` when the guard is decidable on the analyzer's Ruby (or on `stated_ruby`),
95
103
  # otherwise nil (both arms stay live)
96
- def verdict(node)
104
+ def verdict(node, stated_ruby: nil)
97
105
  return nil unless node.is_a?(Prism::CallNode)
98
106
 
99
107
  operator = node.name
@@ -103,19 +111,19 @@ module Rigor
103
111
  arguments = node.arguments&.arguments
104
112
  return nil unless arguments && arguments.length == 1
105
113
 
106
- left = read_operand(node.receiver)
107
- right = read_operand(arguments.first)
114
+ left = read_operand(node.receiver, stated_ruby)
115
+ right = read_operand(arguments.first, stated_ruby)
108
116
  return nil if left.nil? || right.nil?
109
117
 
110
118
  decide(left, right, operator)
111
119
  end
112
120
 
113
121
  # Reads one side of the comparison into `[kind, value]`, or nil when it is not readable.
114
- def read_operand(node)
122
+ def read_operand(node, stated_ruby = nil)
115
123
  case node
116
124
  when Prism::StringNode then [:literal_string, node.unescaped]
117
- when Prism::ConstantReadNode, Prism::ConstantPathNode then read_constant(node)
118
- when Prism::CallNode then read_gem_version(node)
125
+ when Prism::ConstantReadNode, Prism::ConstantPathNode then read_constant(node, stated_ruby)
126
+ when Prism::CallNode then read_gem_version(node, stated_ruby)
119
127
  end
120
128
  end
121
129
  private_class_method :read_operand
@@ -129,9 +137,10 @@ module Rigor
129
137
  #
130
138
  # The foldable set is unchanged and stays closed: an unqualified name folds only when it is one of
131
139
  # {PREDEFINED}, a qualified one only when it is in {VERSION_CONSTANTS}.
132
- def read_constant(node)
140
+ def read_constant(node, stated_ruby = nil)
133
141
  path = Source::ConstantPath.qualified_name_or_nil(node)
134
142
  return nil unless path
143
+ return (path == "RUBY_VERSION" ? [:string, stated_ruby] : nil) if stated_ruby
135
144
 
136
145
  kind =
137
146
  if path.include?("::")
@@ -149,14 +158,14 @@ module Rigor
149
158
  # `Gem::Version.new(<readable>)`. The inner operand must be a plain version String — an engine name
150
159
  # is not a version, and `Gem::Version.new` raises on anything `Gem::Version.correct?` rejects, so a
151
160
  # malformed literal keeps both arms live instead of folding a guard the program cannot even reach.
152
- def read_gem_version(node)
161
+ def read_gem_version(node, stated_ruby = nil)
153
162
  return nil unless node.name == :new && node.block.nil?
154
163
  return nil unless Source::ConstantPath.qualified_name_or_nil(node.receiver) == GEM_VERSION_PATH
155
164
 
156
165
  arguments = node.arguments&.arguments
157
166
  return nil unless arguments && arguments.length == 1
158
167
 
159
- inner = read_operand(arguments.first)
168
+ inner = read_operand(arguments.first, stated_ruby)
160
169
  return nil unless inner && STRING_KINDS.include?(inner.first)
161
170
  return nil unless ::Gem::Version.correct?(inner.last)
162
171
 
@@ -46,6 +46,9 @@ module Rigor
46
46
 
47
47
  def initialize(configuration:)
48
48
  @configuration = configuration
49
+ @buffer_sources = {}
50
+ @disk_feature_files = nil
51
+ @environment_features = nil
49
52
  @generation = 0
50
53
  @environment = nil
51
54
  @cache_store = nil
@@ -77,6 +80,9 @@ module Rigor
77
80
  dependency_source_index: project_scan.dependency_source_index,
78
81
  synthetic_method_index: project_scan.synthetic_method_index,
79
82
  project_patched_methods: project_scan.project_patched_methods,
83
+ # Issue #1700 — the configured paths' files overlaid with the open buffers; {#note_buffers}
84
+ # rebuilds this when an edit or a save changes the set.
85
+ required_features: (@environment_features = required_features),
80
86
  **ProjectEnvironment.dependency_discovery_options(@configuration)
81
87
  )
82
88
  end
@@ -119,12 +125,37 @@ module Rigor
119
125
  diagnostics
120
126
  end
121
127
 
128
+ # Issue #1700 — records the open buffers' text (absolute path => source) and rebuilds the environment
129
+ # when it changes the {Environment::RequiredFeatures} set the current one was built with: a buffer that
130
+ # gains or drops a `require "prime"` loads or unloads the vendored `prime` signatures. The server calls
131
+ # it on every open, change, save and close; neither `didSave` nor a `.rb` edit reaches
132
+ # `workspace/didChangeWatchedFiles`, which most clients send only for the files they were asked to watch.
133
+ # The configured paths are read once per environment and only the buffers are re-scanned, so a keystroke
134
+ # costs a substring test per open buffer.
135
+ def note_buffers(sources)
136
+ @buffer_sources = sources
137
+ return if @environment.nil? || required_features == @environment_features
138
+
139
+ invalidate!
140
+ end
141
+
142
+ # The required-feature set for the configured paths' files on disk, each open buffer read in place of its
143
+ # file.
144
+ def required_features
145
+ disk = (@disk_feature_files ||= Environment::RequiredFeatures.feature_files(@configuration))
146
+ found = disk.reject { |path, _| @buffer_sources.key?(path) }.values.flatten
147
+ found += @buffer_sources.values.flat_map { |source| Environment::RequiredFeatures.features_of(source) }
148
+ found.uniq.sort.freeze
149
+ end
150
+
122
151
  # Drops every cached collaborator and bumps the generation. The next reader rebuilds from scratch.
123
152
  # Triggered by `workspace/didChangeWatchedFiles` for project source files and by
124
153
  # `workspace/didChangeConfiguration`.
125
154
  def invalidate!
126
155
  @generation += 1
127
156
  @environment = nil
157
+ @environment_features = nil
158
+ @disk_feature_files = nil
128
159
  @project_scan = nil
129
160
  # The session's per-file cache was computed against the old environment / project scan, so it cannot
130
161
  # outlive them. The next save round primes a fresh one.
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../version"
4
4
  require_relative "buffer_table"
5
+ require_relative "uri"
5
6
 
6
7
  module Rigor
7
8
  module LanguageServer
@@ -221,6 +222,7 @@ module Rigor
221
222
  bytes: doc.fetch(:text),
222
223
  version: doc.fetch(:version)
223
224
  )
225
+ note_buffers
224
226
  @publisher&.publish_for(uri)
225
227
  nil
226
228
  end
@@ -242,6 +244,7 @@ module Rigor
242
244
  changes: changes,
243
245
  version: doc.fetch(:version)
244
246
  )
247
+ note_buffers
245
248
  @publisher&.publish_for(uri)
246
249
  nil
247
250
  end
@@ -257,6 +260,7 @@ module Rigor
257
260
  def handle_did_save(params)
258
261
  uri = params.fetch(:textDocument).fetch(:uri)
259
262
  @buffer_table.save(uri: uri)
263
+ note_buffers
260
264
  @publisher&.publish_project(uri)
261
265
  nil
262
266
  end
@@ -361,10 +365,24 @@ module Rigor
361
365
  doc = params.fetch(:textDocument)
362
366
  uri = doc.fetch(:uri)
363
367
  @buffer_table.close(uri: uri)
368
+ note_buffers
364
369
  @publisher&.publish_empty(uri)
365
370
  nil
366
371
  end
367
372
 
373
+ # Issue #1700 — hands the open Ruby buffers to the project context, which rebuilds its environment when
374
+ # they change the set of required features the vendored signatures are gated on.
375
+ def note_buffers
376
+ return unless @project_context.respond_to?(:note_buffers)
377
+
378
+ sources = @buffer_table.uris.each_with_object({}) do |uri, acc|
379
+ path = Uri.to_path(uri)
380
+ entry = @buffer_table[uri]
381
+ acc[File.expand_path(path)] = entry.bytes if path&.end_with?(".rb") && entry
382
+ end
383
+ @project_context.note_buffers(sources)
384
+ end
385
+
368
386
  def method_not_found(method)
369
387
  rpc_error(ERROR_METHOD_NOT_FOUND, "method not found: #{method.inspect}")
370
388
  end