rigortype 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/11-sig-gen.md +6 -0
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/03-configuration.md +1 -1
  27. data/docs/manual/04-diagnostics.md +2 -1
  28. data/docs/manual/06-baseline.md +2 -0
  29. data/docs/manual/07-plugins.md +1 -1
  30. data/docs/manual/18-removing-dead-code.md +32 -2
  31. data/docs/manual/plugins/README.md +6 -1
  32. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  33. data/docs/manual/plugins/rigor-alba.md +60 -0
  34. data/docs/manual/plugins/rigor-minitest.md +22 -4
  35. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  36. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  37. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  38. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  39. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  40. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  41. data/lib/rigor/analysis/check_rules.rb +289 -57
  42. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  43. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  44. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  45. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  46. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  47. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  48. data/lib/rigor/analysis/incremental.rb +29 -9
  49. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  50. data/lib/rigor/analysis/incremental_run_slot.rb +3 -1
  51. data/lib/rigor/analysis/incremental_session.rb +14 -1
  52. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  53. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  54. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  55. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  56. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
  57. data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
  58. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  59. data/lib/rigor/analysis/runner.rb +34 -3
  60. data/lib/rigor/analysis/worker_session.rb +7 -4
  61. data/lib/rigor/cache/incremental_snapshot.rb +22 -0
  62. data/lib/rigor/cli/annotate_command.rb +5 -0
  63. data/lib/rigor/cli/baseline_command.rb +7 -18
  64. data/lib/rigor/cli/check_command.rb +2 -1
  65. data/lib/rigor/cli/trace_command.rb +5 -0
  66. data/lib/rigor/cli/type_of_command.rb +5 -3
  67. data/lib/rigor/cli/unused_command.rb +13 -7
  68. data/lib/rigor/cli.rb +10 -2
  69. data/lib/rigor/configuration/severity_profile.rb +6 -0
  70. data/lib/rigor/configuration.rb +46 -2
  71. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  72. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  73. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  74. data/lib/rigor/environment/rbs_loader.rb +283 -14
  75. data/lib/rigor/environment/required_features.rb +206 -0
  76. data/lib/rigor/environment.rb +21 -3
  77. data/lib/rigor/inference/acceptance.rb +28 -3
  78. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  79. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  80. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  81. data/lib/rigor/inference/expression_typer.rb +215 -16
  82. data/lib/rigor/inference/global_write_census.rb +7 -4
  83. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  84. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  85. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  86. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  87. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  88. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  89. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  90. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  91. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  92. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  93. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  94. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  95. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  96. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  97. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  98. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  99. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  100. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +259 -28
  101. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  102. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  103. data/lib/rigor/inference/module_function_state.rb +2 -1
  104. data/lib/rigor/inference/narrowing.rb +20 -6
  105. data/lib/rigor/inference/object_mixins.rb +180 -0
  106. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  107. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  108. data/lib/rigor/inference/refine_census.rb +239 -0
  109. data/lib/rigor/inference/refine_self.rb +317 -0
  110. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  111. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  112. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  113. data/lib/rigor/inference/scope_indexer.rb +687 -60
  114. data/lib/rigor/inference/statement_evaluator.rb +71 -20
  115. data/lib/rigor/inference/string_mutation.rb +9 -3
  116. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  117. data/lib/rigor/inference/version_guard.rb +19 -10
  118. data/lib/rigor/language_server/project_context.rb +31 -0
  119. data/lib/rigor/language_server/server.rb +18 -0
  120. data/lib/rigor/plugin/inflector.rb +1 -1
  121. data/lib/rigor/plugin/io_boundary.rb +42 -3
  122. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  123. data/lib/rigor/project_environment.rb +29 -6
  124. data/lib/rigor/protection/mutation_cache.rb +1 -1
  125. data/lib/rigor/protection/mutator.rb +1 -1
  126. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  127. data/lib/rigor/reflection/constant_path.rb +13 -2
  128. data/lib/rigor/reflection.rb +40 -8
  129. data/lib/rigor/scope/discovery_index.rb +40 -1
  130. data/lib/rigor/scope/resolution_chain.rb +159 -11
  131. data/lib/rigor/scope.rb +189 -10
  132. data/lib/rigor/sig_gen/classification.rb +4 -1
  133. data/lib/rigor/sig_gen/generator.rb +55 -5
  134. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  135. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  136. data/lib/rigor/source/alias_names.rb +52 -0
  137. data/lib/rigor/source/literals.rb +3 -2
  138. data/lib/rigor/source/symbol_name.rb +20 -0
  139. data/lib/rigor/type/combinator.rb +34 -2
  140. data/lib/rigor/type/float_range.rb +4 -1
  141. data/lib/rigor/type/nominal.rb +6 -1
  142. data/lib/rigor/version.rb +1 -1
  143. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  144. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  145. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  146. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  147. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  148. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  149. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  150. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  151. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  152. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  153. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  154. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  156. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  157. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  158. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  159. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  160. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  161. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  162. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  163. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  164. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  165. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  166. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  167. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  168. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  169. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  170. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  171. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  172. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  173. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  174. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  175. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  176. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  177. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  178. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  179. data/sig/rigor/scope.rbs +27 -4
  180. data/sig/rigor/source.rbs +4 -0
  181. data/sig/rigor.rbs +3 -1
  182. metadata +66 -1
@@ -31,6 +31,7 @@ require_relative "match_rebinding"
31
31
  require_relative "element_read_widening"
32
32
  require_relative "hash_lookup_mutation"
33
33
  require_relative "indexed_narrowing"
34
+ require_relative "key_presence_guard"
34
35
  require_relative "index_write_widening"
35
36
  require_relative "method_dispatcher"
36
37
  require_relative "method_parameter_binder"
@@ -352,7 +353,7 @@ module Rigor
352
353
  @on_enter&.call(node, @scope)
353
354
 
354
355
  handler = HANDLERS[node.class]
355
- 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
356
357
 
357
358
  # Default: the node is treated as a pure expression. Type it through the existing expression typer (which
358
359
  # observes the current scope's locals) and leave the scope unchanged, but for the match globals a call in it
@@ -373,6 +374,16 @@ module Rigor
373
374
  [type, after.forget_guard_narrowings]
374
375
  end
375
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
+
376
387
  # Issue #1359 — the nodes whose handler runs their parts in order, each from the scope the parts before it
377
388
  # left, so a `$_` reader among them forgets it where it runs ({LastLine}) and a read before it keeps the
378
389
  # narrowing.
@@ -2994,6 +3005,8 @@ module Rigor
2994
3005
  # outer receiver is itself a chain node (e.g. `x.last << y`) do NOT drop narrowings keyed on `x` — only direct
2995
3006
  # calls against the root variable invalidate the chain.
2996
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)
2997
3010
  # B2.2 — intervening method call ivar invalidation. An implicit-self / self-receiver call could mutate any ivar
2998
3011
  # of the enclosing class (we cannot prove purity without an effect system). Reset each ivar whose current local
2999
3012
  # binding has narrowed below the class-ivar seed back to the seed itself, so a subsequent `if @flag` predicate
@@ -3559,15 +3572,19 @@ module Rigor
3559
3572
  # does not type as a class object (a gem class with no RBS) keeps the name as written: the body is still some
3560
3573
  # class's body, and leaving it on the enclosing `self` made a `refine` at the file's top level report every
3561
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.
3562
3578
  def refined_class_context(node)
3563
3579
  target = ScopeIndexer.refine_target(node)
3564
3580
  return nil if target.nil?
3565
3581
 
3566
- refined = scope.type_of(target)
3567
- 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)
3568
3585
  return nil if name.nil?
3569
3586
 
3570
- [ClassFrame.new(name: name.delete_prefix("::"), singleton: false, refinement: true)]
3587
+ [ClassFrame.new(name: name, singleton: false, refinement: true)]
3571
3588
  end
3572
3589
 
3573
3590
  # The block calls whose body `return` leaves only the block ({ReturnBarrier.block_call?}). Like a `->` body
@@ -3601,7 +3618,9 @@ module Rigor
3601
3618
  narrowed = DefineMethodBlockSelf.narrow_self_type_for(
3602
3619
  scope: scope, call_node: call_node
3603
3620
  )
3604
- 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))
3605
3624
  end
3606
3625
 
3607
3626
  # Enters a meta-new `block` as the body of the class `class_context` names: `self_type` is that class's
@@ -3616,8 +3635,17 @@ module Rigor
3616
3635
  # record a `Outer::K` entry: that is a rung Ruby's constant lookup does not have. The `class_context`
3617
3636
  # frame is still pushed — it is what a nested `def` registers its method under — which is exactly the
3618
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.
3619
3643
  def enter_meta_class_body(block, block_entry, class_context)
3620
- 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)
3621
3649
  sub_eval(block, stamp_nesting(entry, @lexical_nesting), class_context: class_context)
3622
3650
  end
3623
3651
 
@@ -5140,27 +5168,31 @@ module Rigor
5140
5168
  # ({ExpressionTyper#block_body_self_narrowing}) already applies the same contract to block-return
5141
5169
  # typing; without it here the recorded per-node scopes — what `dump_type`/`assert_type` and the
5142
5170
  # survey read — keep the enclosing `self_type` and every DSL call inside stays `Dynamic[top]`.
5143
- narrowed = call_node && narrow_macro_block_self(call_node)
5144
- 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
5145
5177
  block_local_names(block_node).reduce(scope_with_params) do |acc, name|
5146
5178
  acc.with_local(name, Type::Combinator.constant_of(nil))
5147
5179
  end
5148
5180
  end
5149
5181
 
5150
- # 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
5151
5183
  # current `self_type` for an implicit-self DSL call (the `params do` / `namespace do` shapes, whose
5152
5184
  # receiver is the enclosing `Singleton[X]`). A miss leaves the entry scope as built — the false-
5153
5185
  # positive-safe direction.
5154
- def narrow_macro_block_self(call_node)
5186
+ def macro_block_match(call_node)
5155
5187
  receiver_type =
5156
5188
  if call_node.receiver
5157
5189
  explicit_receiver_type(call_node)
5158
5190
  else
5159
- scope.self_type
5191
+ MacroBlockSelfType.implicit_receiver_type(scope)
5160
5192
  end
5161
5193
  return nil if receiver_type.nil?
5162
5194
 
5163
- MacroBlockSelfType.narrow_self_type_for(
5195
+ MacroBlockSelfType.match_for(
5164
5196
  scope: scope, call_node: call_node, receiver_type: receiver_type
5165
5197
  )
5166
5198
  rescue StandardError
@@ -5193,7 +5225,8 @@ module Rigor
5193
5225
  method_name: call_node.name,
5194
5226
  arg_types: arg_types,
5195
5227
  environment: scope.environment,
5196
- scope: scope
5228
+ scope: scope,
5229
+ call_node: call_node
5197
5230
  )
5198
5231
  rescue StandardError
5199
5232
  []
@@ -5218,13 +5251,19 @@ module Rigor
5218
5251
  fresh = build_fresh_body_scope
5219
5252
  # Issue #1518 — `class << self` inside an unnameable class body keeps that body's opacity: it opens the
5220
5253
  # singleton of a class nothing names, and its empty frame stack is not the top level.
5221
- opaque_self ||= node.is_a?(Prism::SingletonClassNode) && @opaque_class && new_context.empty?
5222
- body_self = opaque_self ? Type::Combinator.untyped : self_type_for_class_body(new_context)
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)
5223
5262
  fresh = fresh.with_self_type(body_self) if body_self
5224
5263
  # Issue #963 — `self` in a `class << ...` body is the SINGLETON class, which shares the `Singleton[X]`
5225
5264
  # carrier a `class X` body gets. The mark is the only thing that tells the two apart downstream, and a
5226
5265
  # `def` reached from this body clears it by starting from a fresh scope.
5227
- fresh = fresh.with_singleton_class_body(node.is_a?(Prism::SingletonClassNode))
5266
+ fresh = fresh.with_singleton_class_body(singleton_body)
5228
5267
  fresh = fresh.with_match_frame(node.body)
5229
5268
  fresh = stamp_nesting(fresh, new_nesting)
5230
5269
  sub_eval(node.body, fresh, class_context: new_context, lexical_nesting: new_nesting, opaque_class: opaque_self)
@@ -5254,7 +5293,7 @@ module Rigor
5254
5293
  # then receive the parameter bindings. Slice 7 phase 2: instance defs ALSO seed their `ivars` map from the
5255
5294
  # class-level accumulator so `def get; @x; end` reads the type that a sibling `def init; @x = 1; end` wrote.
5256
5295
  fresh = build_fresh_body_scope
5257
- body_self = self_type_for_method_body(singleton: singleton)
5296
+ body_self = self_type_for_method_body(singleton: singleton, receiver: def_node.receiver)
5258
5297
  fresh = fresh.with_self_type(body_self) if body_self
5259
5298
  # A `def` opens no new `Module.nesting` entry — the body resolves constants against the chain of the
5260
5299
  # declaration that encloses it, which is what the evaluator is already carrying (#652).
@@ -5389,12 +5428,16 @@ module Rigor
5389
5428
  # chain's throwaway intermediate Scopes were a top `Scope#rebuild` source (ADR-44). Local-empty by design; the
5390
5429
  # discovery index is inherited whole by reference (ADR-53 Track A), so a table added to the index can no longer
5391
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.
5392
5434
  Scope.new(
5393
5435
  environment: scope.environment,
5394
5436
  locals: {}.freeze,
5395
5437
  source_path: scope.source_path,
5396
5438
  discovery: scope.discovery,
5397
- dynamic_origins: scope.dynamic_origins
5439
+ dynamic_origins: scope.dynamic_origins,
5440
+ declared_refinements: scope.declared_refinements
5398
5441
  )
5399
5442
  end
5400
5443
 
@@ -5444,11 +5487,15 @@ module Rigor
5444
5487
  # `Nominal[Foo]`.
5445
5488
  #
5446
5489
  # Returns nil for top-level defs that have no enclosing class.
5447
- def self_type_for_method_body(singleton:)
5490
+ def self_type_for_method_body(singleton:, receiver: nil)
5448
5491
  path = current_class_path
5449
5492
  # Issue #1518 — a `def` in a class nothing names has a receiver, just not one Rigor can type; nil would
5450
5493
  # read its body as the top level and its implicit-self calls as unresolved top-level calls.
5451
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)
5452
5499
 
5453
5500
  if singleton
5454
5501
  Type::Combinator.singleton_of(path)
@@ -5457,6 +5504,10 @@ module Rigor
5457
5504
  end
5458
5505
  end
5459
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
+
5460
5511
  def singleton_context_for(node)
5461
5512
  case node.expression
5462
5513
  when Prism::SelfNode
@@ -5464,7 +5515,7 @@ module Rigor
5464
5515
 
5465
5516
  outer = @class_context[0..-2]
5466
5517
  last = @class_context.last
5467
- outer + [ClassFrame.new(name: last.name, singleton: true)]
5518
+ outer + [ClassFrame.new(name: last.name, singleton: true, refinement: last.refinement)]
5468
5519
  when Prism::ConstantReadNode, Prism::ConstantPathNode
5469
5520
  target = singleton_constant_target(node.expression)
5470
5521
  return @class_context unless target
@@ -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
@@ -54,7 +54,7 @@ module Rigor
54
54
  # by `rigor plugins` (ADR-90) to probe {available?} at activation time — so a standalone install
55
55
  # where inflection silently degrades reports the degradation instead of an unqualified `[OK]`. When
56
56
  # a new plugin adopts the helper, add its manifest id here.
57
- CONSUMER_PLUGIN_IDS = %w[actionmailer actionpack activerecord factorybot rails-routes].freeze
57
+ CONSUMER_PLUGIN_IDS = %w[actionmailer actionpack activerecord alba factorybot rails-routes].freeze
58
58
 
59
59
  # Raised when `ActiveSupport::Inflector` is required for an inflection but cannot be loaded. Caught by
60
60
  # the per-plugin isolation boundary, so it surfaces as "this plugin produced no diagnostics" rather than
@@ -199,6 +199,44 @@ module Rigor
199
199
  entries
200
200
  end
201
201
 
202
+ # Issue #1652 — the MEMBERSHIP fingerprint of a `Dir.glob` pattern under `root`, `**` included. Returns the
203
+ # matching paths and records one `:names` {Cache::Descriptor::GlobEntry} over `root` and `pattern`: the row
204
+ # reads stale when a match appears or disappears, in a subdirectory created after the read too, and NOT
205
+ # when a match's bytes or stat tuple move. A caller whose answer depends on a match's content MUST
206
+ # {#read_file} each match it reads; that row carries the edit. A stat signature here would read stale on
207
+ # every `touch`, checkout or restored CI cache, though the content the caller read is unchanged (#979's
208
+ # reasoning). {#list_directory} keeps its `:stat` listing row.
209
+ #
210
+ # The pattern must stay under `root`, since the policy is checked against `root` alone: a `..` segment, a
211
+ # brace (`{..,x}`) or a backslash escape (`.\\./`) raises {AccessDeniedError} (`:read_outside_scope`).
212
+ # The last two are refused outright because `Dir.glob` can spell `..` with them. Out of policy, `root` is
213
+ # globbed truthfully and contributes no row, as in {#list_directory}.
214
+ #
215
+ # @param root — project directory; relative paths expand against the working directory
216
+ # @param pattern — a `Dir.glob` pattern relative to `root`, such as `"ext/**/extconf.rb"`
217
+ # @return the absolute matching paths in `Dir.glob` order
218
+ def glob(root, pattern)
219
+ absolute = File.absolute_path(root.to_s)
220
+ pattern = pattern.to_s
221
+ if pattern.match?(/[{\\]/) || pattern.split("/").include?("..")
222
+ raise AccessDeniedError.new(
223
+ "plugin #{@plugin_id.inspect} cannot glob #{pattern.inspect} under #{absolute.inspect}: " \
224
+ "a `..` segment, a brace or a backslash can leave the root the trusted-read scope was checked against",
225
+ reason: :read_outside_scope,
226
+ resource: File.join(absolute, pattern)
227
+ )
228
+ end
229
+
230
+ entries = Dir.glob(File.join(absolute, pattern))
231
+ unless @policy.allow_read?(absolute)
232
+ record_refusal(absolute)
233
+ return entries
234
+ end
235
+
236
+ record_glob_entry(absolute, pattern, mode: :names)
237
+ entries
238
+ end
239
+
202
240
  # Fetches the URL when the policy permits it. Returns the response body. Raises {AccessDeniedError} when
203
241
  # the policy is `:disabled`, the URL scheme is not `https`, the host is not on the allowlist, the
204
242
  # response is non-2xx, the body exceeds {URL_MAX_BYTES}, or the request times out
@@ -352,11 +390,12 @@ module Rigor
352
390
  self.class.attribute(entry)
353
391
  end
354
392
 
355
- # ADR-45 WD1c (#629) — the listing row for {#list_directory}, deduplicated per (root, pattern) slot.
393
+ # ADR-45 WD1c (#629) — the listing row for {#list_directory} (and the `:names` row for {#glob}),
394
+ # deduplicated per (root, pattern, mode) slot.
356
395
  # Recomputed on each call rather than `||=`d, so a directory listed again after a mid-run mutation
357
396
  # carries the signature of the state the LAST reader saw.
358
- def record_glob_entry(root, pattern)
359
- entry = Cache::Descriptor::GlobEntry.compute(root: root, pattern: pattern)
397
+ def record_glob_entry(root, pattern, mode: :stat)
398
+ entry = Cache::Descriptor::GlobEntry.compute(root: root, pattern: pattern, mode: mode)
360
399
  @mutex.synchronize { @glob_entries[entry.slot_key] = entry }
361
400
  self.class.attribute(entry)
362
401
  end