rigortype 0.4.5 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/docs/handbook/11-sig-gen.md +11 -2
  4. data/docs/manual/02-cli-reference.md +46 -7
  5. data/docs/manual/03-configuration.md +2 -1
  6. data/docs/manual/04-diagnostics.md +1 -1
  7. data/docs/manual/10-mcp-server.md +9 -1
  8. data/docs/manual/12-caching.md +8 -1
  9. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  10. data/docs/manual/plugins/rigor-actionpack.md +30 -0
  11. data/docs/manual/plugins/rigor-devise.md +15 -0
  12. data/docs/manual/plugins/rigor-rspec.md +2 -1
  13. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  14. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
  15. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
  16. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -0
  17. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
  18. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  19. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  20. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  21. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +51 -0
  22. data/lib/rigor/analysis/check_rules.rb +93 -8
  23. data/lib/rigor/analysis/dependency_recorder.rb +11 -0
  24. data/lib/rigor/analysis/edit_replay.rb +67 -0
  25. data/lib/rigor/analysis/incremental.rb +37 -0
  26. data/lib/rigor/analysis/incremental_session.rb +175 -27
  27. data/lib/rigor/analysis/path_expansion.rb +41 -0
  28. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  29. data/lib/rigor/analysis/rule_catalog.rb +5 -1
  30. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  31. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  32. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  33. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
  34. data/lib/rigor/analysis/runner/pool_coordinator.rb +17 -4
  35. data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
  36. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  37. data/lib/rigor/analysis/runner.rb +156 -49
  38. data/lib/rigor/analysis/worker_session.rb +14 -2
  39. data/lib/rigor/cache/incremental_snapshot.rb +30 -7
  40. data/lib/rigor/cli/check_command.rb +93 -4
  41. data/lib/rigor/cli/diff_command.rb +2 -1
  42. data/lib/rigor/cli/lens_command.rb +5 -5
  43. data/lib/rigor/cli/mcp_command.rb +2 -1
  44. data/lib/rigor/cli/options.rb +20 -3
  45. data/lib/rigor/cli/plugins_command.rb +5 -0
  46. data/lib/rigor/cli/plugins_renderer.rb +24 -2
  47. data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
  48. data/lib/rigor/cli/sig_gen_command.rb +46 -7
  49. data/lib/rigor/cli/type_of_command.rb +2 -1
  50. data/lib/rigor/cli/type_of_project_scope.rb +27 -0
  51. data/lib/rigor/config_audit.rb +16 -5
  52. data/lib/rigor/configuration.rb +14 -4
  53. data/lib/rigor/environment/rbs_loader.rb +115 -6
  54. data/lib/rigor/environment/required_features.rb +136 -29
  55. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  56. data/lib/rigor/environment.rb +4 -2
  57. data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
  58. data/lib/rigor/inference/deep_erasure.rb +138 -0
  59. data/lib/rigor/inference/def_return_typer.rb +159 -9
  60. data/lib/rigor/inference/definer_resolution.rb +17 -3
  61. data/lib/rigor/inference/error_info.rb +17 -3
  62. data/lib/rigor/inference/expression_typer.rb +62 -13
  63. data/lib/rigor/inference/global_write_census.rb +36 -1
  64. data/lib/rigor/inference/ivar_writer_census.rb +195 -0
  65. data/lib/rigor/inference/loop_value_record.rb +53 -0
  66. data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
  67. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +110 -9
  68. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
  69. data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
  70. data/lib/rigor/inference/method_dispatcher.rb +46 -16
  71. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  72. data/lib/rigor/inference/module_function_state.rb +12 -0
  73. data/lib/rigor/inference/mutation_widening.rb +5 -2
  74. data/lib/rigor/inference/narrowing.rb +66 -5
  75. data/lib/rigor/inference/object_mixins.rb +13 -1
  76. data/lib/rigor/inference/project_seed_tables.rb +2 -1
  77. data/lib/rigor/inference/project_yielder.rb +128 -0
  78. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
  79. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  80. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
  81. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  82. data/lib/rigor/inference/scope_indexer.rb +480 -103
  83. data/lib/rigor/inference/self_call_ivar_writes.rb +18 -6
  84. data/lib/rigor/inference/statement_evaluator.rb +658 -90
  85. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  86. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  87. data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
  88. data/lib/rigor/language_server/buffer_table.rb +6 -0
  89. data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
  90. data/lib/rigor/language_server/project_context.rb +59 -3
  91. data/lib/rigor/language_server/server.rb +5 -3
  92. data/lib/rigor/mcp/server.rb +9 -1
  93. data/lib/rigor/plugin/base.rb +26 -11
  94. data/lib/rigor/plugin/manifest.rb +29 -3
  95. data/lib/rigor/plugin/registry.rb +38 -0
  96. data/lib/rigor/protection/discovery_seed.rb +57 -2
  97. data/lib/rigor/rooted_glob.rb +10 -1
  98. data/lib/rigor/scope.rb +14 -5
  99. data/lib/rigor/sig_gen/generator.rb +308 -37
  100. data/lib/rigor/sig_gen/observation_collector.rb +4 -1
  101. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  102. data/lib/rigor/sig_gen/renderer.rb +6 -3
  103. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  104. data/lib/rigor/type/combinator.rb +4 -3
  105. data/lib/rigor/type/constant.rb +45 -3
  106. data/lib/rigor/type/hash_shape.rb +14 -9
  107. data/lib/rigor/type/struct_instance.rb +3 -2
  108. data/lib/rigor/version.rb +1 -1
  109. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +60 -43
  110. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
  111. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
  112. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
  113. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  114. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +6 -5
  115. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  116. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
  117. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  118. data/sig/rigor/analysis/baseline.rbs +3 -2
  119. data/sig/rigor/effects/file_collection.rbs +5 -6
  120. data/sig/rigor/effects/registry.rbs +8 -6
  121. data/sig/rigor/effects/summary.rbs +6 -5
  122. data/sig/rigor/environment.rbs +5 -3
  123. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  124. data/sig/rigor/inference.rbs +6 -0
  125. data/sig/rigor/plugin/additional_initializer.rbs +5 -5
  126. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  127. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  128. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  129. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  130. data/sig/rigor/plugin/io_boundary.rbs +4 -4
  131. data/sig/rigor/plugin/manifest.rbs +16 -18
  132. data/sig/rigor/plugin/protocol_contract.rbs +10 -9
  133. data/sig/rigor/plugin/registry.rbs +1 -1
  134. data/sig/rigor/scope.rbs +31 -50
  135. data/sig/rigor/type.rbs +0 -6
  136. metadata +20 -1
@@ -535,10 +535,9 @@ module Rigor
535
535
  # The question is asked HERE, of the same two tiers `resolve` consults and in the same order, rather
536
536
  # than reconstructed in the veto: a name this reports as answered is a name the dispatch really would
537
537
  # resolve, and no plugin knowledge moves into the engine's veto. The tiers are the gated
538
- # `dynamic_return` walk and the ADR-16 synthetic-method index (Tier C `heredoc_templates` emissions
539
- # today; the Tier B `Plugin::Macro::TraitRegistry` explosions share that table but cannot populate it
540
- # until #476 threads a real environment into the pre-pass that builds it, so no trait member reaches
541
- # this predicate in production yet — it will when #476 lands, with no change here).
538
+ # `dynamic_return` walk and the ADR-16 synthetic-method index (Tier C `heredoc_templates` emissions and
539
+ # the Tier B `Plugin::Macro::TraitRegistry` explosions, which share that table; since #476 the pre-pass
540
+ # explodes a trait module whenever the project's RBS declares it).
542
541
  #
543
542
  # It is a predicate, not a type: the veto's job is to stop the top-level bind, after which the call
544
543
  # falls through to the ordinary dispatch chain and the tier that answered here answers there too. The
@@ -1031,6 +1030,7 @@ module Rigor
1031
1030
 
1032
1031
  fallback_receiver = user_class_fallback_receiver(receiver_type, environment)
1033
1032
  return nil if fallback_receiver.nil?
1033
+ return nil if declared_by_external_ancestor?(receiver_type, environment, context)
1034
1034
 
1035
1035
  # Preserve the ORIGINAL receiver type as the `self` substitution so `Kernel#dup: () ->
1036
1036
  # self` and other `self`-returning methods route through Object's RBS while still
@@ -1055,6 +1055,45 @@ module Rigor
1055
1055
  )
1056
1056
  end
1057
1057
 
1058
+ # Issue #2196 — a project class whose ancestry reaches a core or stdlib class before `Object` (`class FL <
1059
+ # Array`) answers a name that class declares from that class, not from `Object` / `Kernel`: Ruby dispatches
1060
+ # `select { … }` on an `FL` to `Array#select`, never to the private `Kernel#select` (`-> Array[String]`). The
1061
+ # ancestor tiers decline such a name where they cannot type it (`RbsDispatch#returns_the_walked_ancestry?`),
1062
+ # and the `Object` fallback then fabricated `Kernel#select`'s answer, so `fields.detect { |f| f.errors }` read
1063
+ # `f` as `String`. The fallback declines the name instead, which leaves the call `Dynamic[top]`.
1064
+ # {ExternalAncestorResolution.resolve} answers nil for a name only `Object`, `Kernel` or `BasicObject` declares,
1065
+ # so `puts` and `raise` keep resolving.
1066
+ def declared_by_external_ancestor?(receiver_type, environment, context)
1067
+ return false unless receiver_type.is_a?(Type::Nominal)
1068
+
1069
+ scope = context.scope
1070
+ return false if scope.nil?
1071
+
1072
+ memo = external_ancestor_memo(scope, environment)
1073
+ key = [receiver_type.class_name, context.method_name]
1074
+ return memo[key] if memo.key?(key)
1075
+
1076
+ memo[key] = !ExternalAncestorResolution.resolve(receiver_type.class_name, context.method_name, :instance,
1077
+ scope: scope, environment: environment,
1078
+ record_dependencies: false).nil?
1079
+ end
1080
+
1081
+ EXTERNAL_ANCESTOR_MEMO_KEY = :__rigor_user_class_fallback_external_ancestor__
1082
+ private_constant :EXTERNAL_ANCESTOR_MEMO_KEY
1083
+
1084
+ # One slot per discovery index and environment, as `RbsDispatch#ancestor_memo_slot` keeps its own: the answer
1085
+ # reads only the frozen discovery tables and the RBS environment, and its walk is withheld from dependency
1086
+ # recording, so it may be served again for the same pair.
1087
+ def external_ancestor_memo(scope, environment)
1088
+ discovery = scope.discovery
1089
+ slot = Thread.current[EXTERNAL_ANCESTOR_MEMO_KEY]
1090
+ unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment)
1091
+ slot = [discovery, environment, {}]
1092
+ Thread.current[EXTERNAL_ANCESTOR_MEMO_KEY] = slot
1093
+ end
1094
+ slot[2]
1095
+ end
1096
+
1058
1097
  # True when the call node carries an explicit receiver that is not the literal `self`.
1059
1098
  # Such a call cannot legally dispatch to a private method, so the user-class fallback must
1060
1099
  # skip private signatures rather than return a confidently-wrong type. Returns false for
@@ -1257,7 +1296,7 @@ module Rigor
1257
1296
  range_lift = range_new_lift(receiver_type.class_name, arg_types)
1258
1297
  return range_lift if range_lift
1259
1298
 
1260
- set_lift = set_new_lift(receiver_type.class_name, arg_types)
1299
+ set_lift = set_new_lift(receiver_type.class_name, arg_types, context)
1261
1300
  return set_lift if set_lift
1262
1301
 
1263
1302
  hash_lift = hash_new_lift(receiver_type.class_name, arg_types, context&.call_node)
@@ -1613,19 +1652,10 @@ module Rigor
1613
1652
  # are given or the single argument is a `Tuple` whose every element is a `Constant[T]`.
1614
1653
  # Mirrors `SetFolding#fold_new` but lives here so the `:new` path in
1615
1654
  # `try_meta_introspection` can reach it before the RBS tier answers `Nominal[Set]`.
1616
- def set_new_lift(class_name, arg_types)
1655
+ def set_new_lift(class_name, arg_types, context = nil)
1617
1656
  return nil unless class_name == "Set"
1618
- return Type::Combinator.constant_of(::Set.new) if arg_types.empty?
1619
- return nil if arg_types.size > 1
1620
1657
 
1621
- arg = arg_types.first
1622
- return nil unless arg.is_a?(Type::Tuple)
1623
- return nil unless arg.elements.all?(Type::Constant)
1624
-
1625
- values = arg.elements.map(&:value)
1626
- Type::Combinator.constant_of(::Set.new(values))
1627
- rescue StandardError
1628
- nil
1658
+ SetFolding.fold_new(arg_types, context&.call_node, context&.block_type)
1629
1659
  end
1630
1660
 
1631
1661
  # `Regexp.new(pattern)` / `Regexp.new(pattern, opts)` — folds to `Constant[Regexp]` when
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require_relative "../../source/literals"
5
+
6
+ module Rigor
7
+ module Inference
8
+ module ModuleFunctionState
9
+ # Issue #2209 — the `module_function` spellings other than the receiverless call that the deferred-ranges
10
+ # prescan ({ModuleFunctionState.prescan_deferred}) reads: `self.module_function(...)`, and
11
+ # `send(:module_function, ...)` or `__send__(:module_function, ...)` on no receiver or `self`, which Ruby runs
12
+ # as the receiverless call. `public_send` raises on a private method such as `module_function`, so it is not
13
+ # one.
14
+ #
15
+ # A literal name gives the `:singleton` row and a `def` argument the `:both` or `:singleton` row a receiverless
16
+ # call gives. The bare form, and an argument that is neither (`*names`, a local), gives an {ANY_ROW_NAME} row
17
+ # over the call: the prescan reads neither the toggle these spellings set nor such a name, so every singleton
18
+ # copy of the owner is uncertain (`DefinerResolution.module_function_row?`).
19
+ module Spellings
20
+ SEND_CALLS = %i[send __send__].freeze
21
+ private_constant :SEND_CALLS
22
+
23
+ module_function
24
+
25
+ # The arguments the spelling `node` passes `module_function`, or nil for any other call.
26
+ def arguments(node)
27
+ arguments = node.arguments&.arguments || []
28
+ return arguments if node.name == :module_function && node.receiver.is_a?(Prism::SelfNode)
29
+ return nil unless SEND_CALLS.include?(node.name)
30
+ return nil unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
31
+ return nil unless Source::Literals.symbol_or_string(arguments.first) == :module_function
32
+
33
+ arguments.drop(1)
34
+ end
35
+
36
+ # Appends the rows of the spelling `node` with `arguments` ({.arguments}) to `ranges`, and prescans the rest
37
+ # as {ModuleFunctionState.prescan_deferred} prescans a receiverless call's.
38
+ def prescan(node, arguments, qualified_prefix, in_singleton_class, offsets, ranges)
39
+ owner = qualified_prefix.empty? ? nil : qualified_prefix.join("::")
40
+ call_span = [node.location.start_offset, node.location.end_offset]
41
+ any = arguments.empty?
42
+ arguments.each do |arg|
43
+ if arg.is_a?(Prism::DefNode)
44
+ kind = ScopeIndexer.def_singleton?(arg, qualified_prefix, in_singleton_class) ? :singleton : :both
45
+ ranges << [arg.location.start_offset, arg.location.end_offset, arg.name, kind, owner]
46
+ elsif (name = Source::Literals.symbol_or_string(arg))
47
+ ranges << [*call_span, name, :singleton, owner]
48
+ else
49
+ any = true
50
+ nested_prescan(arg, qualified_prefix, in_singleton_class, offsets, ranges)
51
+ end
52
+ end
53
+ ranges << [*call_span, ANY_ROW_NAME, :singleton, owner] if any
54
+ nested_prescan(node.block, qualified_prefix, in_singleton_class, offsets, ranges) if node.block
55
+ end
56
+
57
+ def nested_prescan(node, qualified_prefix, in_singleton_class, offsets, ranges)
58
+ ModuleFunctionState.prescan_deferred(node, qualified_prefix, in_singleton_class, offsets, ranges,
59
+ nested: true)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -318,10 +318,20 @@ module Rigor
318
318
  end
319
319
  end
320
320
 
321
+ # Issue #2209 — the row name of a deferred-ranges row that stands for every name of its owner
322
+ # ({Spellings}).
323
+ ANY_ROW_NAME = :*
324
+
325
+ # Issue #2209 — the deferred-ranges row names that make `method_name` a module function on their owner: its
326
+ # own and {ANY_ROW_NAME}.
327
+ def row_names(method_name) = [method_name.to_sym, ANY_ROW_NAME]
328
+
321
329
  # The prescan's arms for a call; true when the arm took the call and its subtree.
322
330
  def prescanned_call?(node, qualified_prefix, in_singleton_class, offsets, ranges, nested)
323
331
  if call?(node)
324
332
  prescan_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
333
+ elsif (arguments = Spellings.arguments(node))
334
+ Spellings.prescan(node, arguments, qualified_prefix, in_singleton_class, offsets, ranges)
325
335
  elsif visibility_reset?(node)
326
336
  offsets << [node.location.start_offset, nested ? :maybe_off : :off]
327
337
  elsif ScopeIndexer.receiver_eval_call?(node) || (node.block && ScopeIndexer.meta_new_constant_rvalue?(node))
@@ -374,3 +384,5 @@ module Rigor
374
384
  end
375
385
  end
376
386
  end
387
+
388
+ require_relative "module_function_state/spellings"
@@ -7,6 +7,7 @@ require_relative "../source/node_children"
7
7
  require_relative "content_join"
8
8
  require_relative "hash_lookup_mutation"
9
9
  require_relative "mutation_rejoin"
10
+ require_relative "deep_erasure"
10
11
  require_relative "receiver_alias"
11
12
  require_relative "refinement_mutation"
12
13
  require_relative "rewrite_mutation"
@@ -200,12 +201,15 @@ module Rigor
200
201
  # `Prism::BlockNode`s — each block is processed by its own `eval_call`.
201
202
  def widen_after_block(call_node:, outer_scope:)
202
203
  block = call_node.block
204
+ return DeepErasure.widen_symbol_proc(call_node, outer_scope) if DeepErasure.symbol_proc_mutation?(block)
203
205
  return outer_scope unless block.is_a?(Prism::BlockNode)
204
206
 
205
207
  body = block.body
206
208
  return outer_scope if body.nil?
207
209
 
208
- walk_for_outer_mutations(body, outer_scope)
210
+ scope = walk_for_outer_mutations(body, outer_scope)
211
+ names = DeepErasure.parameter_names(block.parameters)
212
+ DeepErasure.widen_element_aliases(call_node.receiver, block.body, names, scope)
209
213
  end
210
214
 
211
215
  def walk_for_outer_mutations(node, scope)
@@ -257,7 +261,6 @@ module Rigor
257
261
  Prism::ClassVariableReadNode => %i[cvar with_cvar],
258
262
  Prism::GlobalVariableReadNode => %i[global with_global]
259
263
  })
260
- private_constant :ALIAS_ACCESSORS
261
264
 
262
265
  def widen_alias_read(method_name, read, scope, values: :widen, arg_types: NO_ARG_TYPES)
263
266
  getter, builder = ALIAS_ACCESSORS[read.class]
@@ -524,7 +524,7 @@ module Rigor
524
524
 
525
525
  # Issue #1429 — a global or constant subject (`case $stdout when StringIO`) narrows as a local does, and so
526
526
  # does an instance variable (#1446).
527
- slot = receiver_slot(subject, scope, kinds: CASE_SUBJECT_KINDS)
527
+ slot = case_subject_slot(subject, scope)
528
528
  return [body_scope, scope] if slot.nil?
529
529
 
530
530
  truthy_type, falsey_type = case_when_types(scope, slot.current, conditions)
@@ -538,6 +538,32 @@ module Rigor
538
538
  CASE_SUBJECT_KINDS = %i[local ivar global constant].freeze
539
539
  private_constant :CASE_SUBJECT_KINDS
540
540
 
541
+ # The slot a `case` subject narrows. Issue #2211 — a subject that writes a local or instance variable
542
+ # (`case (x = v)`, `case x = v`) tests the value it stores, which the variable holds in every clause, so the
543
+ # clauses narrow the variable as they narrow a bare read of it: `case (x = s ? String(s) : nil) when String then
544
+ # x.upcase end` reads `x` as `String`. A single-statement `(…)` is unwrapped.
545
+ def case_subject_slot(subject, scope)
546
+ subject = unwrap_case_subject(subject)
547
+ case subject
548
+ when Prism::LocalVariableWriteNode
549
+ variable_slot(:local, subject.name, scope.local(subject.name), CASE_SUBJECT_KINDS)
550
+ when Prism::InstanceVariableWriteNode
551
+ variable_slot(:ivar, subject.name, scope.ivar(subject.name), CASE_SUBJECT_KINDS)
552
+ else receiver_slot(subject, scope, kinds: CASE_SUBJECT_KINDS)
553
+ end
554
+ end
555
+ private_class_method :case_subject_slot
556
+
557
+ # `subject` with each single-statement `(…)` around it removed: the node {.case_subject_slot} narrows through,
558
+ # which `StatementEvaluator#when_test_rebinds_subject?` reads the same way.
559
+ def unwrap_case_subject(subject)
560
+ while subject.is_a?(Prism::ParenthesesNode) && subject.body.is_a?(Prism::StatementsNode) &&
561
+ subject.body.body.size == 1
562
+ subject = subject.body.body.first
563
+ end
564
+ subject
565
+ end
566
+
541
567
  # Returns `[body_scope, falsey_scope]` for one clause of a `case` with a subject: a `when` clause narrows
542
568
  # through {.case_when_scopes}, and an `in` clause only for a bare class pattern ({.bare_class_pattern_node}),
543
569
  # which matches exactly when `subject.is_a?(C)`. Every other pattern can fail to match where a class test
@@ -2762,10 +2788,14 @@ module Rigor
2762
2788
 
2763
2789
  # Issue #1955 — the subject on the match edge of `/re/ === x` (and of `case x when /re/`). `Regexp#===`
2764
2790
  # matches a String, a Symbol by its name, and an object that converts with `to_str`, and answers false for
2765
- # anything else. Each member narrows to its String and Symbol parts: a `String | nil` subject reads `String`, a
2766
- # Symbol one stays `Symbol`. The `to_str` conversion is not modelled, so a member that defines `to_str` and is
2767
- # neither drops out of the edge. A `Dynamic` member is left as it is, since it may be a Symbol at run time. The
2768
- # no-match edge keeps the entry type: a String that does not match reaches it too.
2791
+ # anything else. A member is kept whole when its class defines `to_str` (#2002) — a String and its
2792
+ # subclasses, a class whose signature, ancestry or project source declares it — or when no signature
2793
+ # declares the class, and a `Dynamic` member is kept, since it may also be a Symbol at run time. Any other
2794
+ # member narrows to its String and Symbol parts, which are only what can match: a `String | nil` subject
2795
+ # reads `String`, a Symbol one stays `Symbol`, an Integer one drops out. An `Object`, `BasicObject` or
2796
+ # module member narrows too (to `String | Symbol`): its instances may be anything, and reading the edge as
2797
+ # the String it most often is keeps `s.length` on it from reporting. The no-match edge keeps the entry type:
2798
+ # a String that does not match reaches it too.
2769
2799
  def regexp_case_equality_type(current, scope)
2770
2800
  members = current.is_a?(Type::Union) ? current.members : [current]
2771
2801
  Type::Combinator.union(*members.map { |member| regexp_case_equality_member(member, scope) })
@@ -2773,6 +2803,7 @@ module Rigor
2773
2803
 
2774
2804
  def regexp_case_equality_member(member, scope)
2775
2805
  return member if member.is_a?(Type::Dynamic)
2806
+ return member if converts_with_to_str?(member, scope)
2776
2807
 
2777
2808
  parts = %w[String Symbol].map do |class_name|
2778
2809
  narrow_class(member, class_name, exact: false, environment: scope.environment, scope: scope)
@@ -2780,6 +2811,36 @@ module Rigor
2780
2811
  Type::Combinator.union(*parts)
2781
2812
  end
2782
2813
 
2814
+ # True when a class `member` dispatches through declares `to_str`, or is declared by no signature
2815
+ # ({#class_lacks_method?} answers false for it). `Object`, `BasicObject` and a module String or Symbol
2816
+ # includes (`Kernel`, `Comparable`) are read as their String and Symbol parts instead: their instances may be
2817
+ # anything, and the String they most often are keeps a String method on the arm from reporting. Any other
2818
+ # module is kept whole, since a matching value may include it; so is one of those that declares `to_str`.
2819
+ def converts_with_to_str?(member, scope)
2820
+ targets = ProjectMethodOwnership.targets(member)
2821
+ return false if targets.nil? || targets.empty?
2822
+
2823
+ targets.any? { |class_name, kind| class_converts_with_to_str?(class_name, kind, scope) }
2824
+ end
2825
+
2826
+ # The modules String's or Symbol's own ancestry includes, read from the running Ruby.
2827
+ STRING_SYMBOL_MODULES = (String.ancestors + Symbol.ancestors).grep_v(Class).filter_map(&:name).uniq.freeze
2828
+ private_constant :STRING_SYMBOL_MODULES
2829
+
2830
+ def class_converts_with_to_str?(class_name, kind, scope)
2831
+ return false if kind == :instance && OPEN_SURFACE_CLASSES.include?(class_name)
2832
+ if kind == :instance && scope.environment.rbs_module?(class_name)
2833
+ return module_declares_to_str?(class_name, scope) || !STRING_SYMBOL_MODULES.include?(class_name)
2834
+ end
2835
+
2836
+ !class_lacks_method?(class_name, :to_str, kind, scope)
2837
+ end
2838
+
2839
+ def module_declares_to_str?(module_name, scope)
2840
+ ProjectMethodOwnership.defines?(module_name, :to_str, :instance, scope) ||
2841
+ !ExternalAncestorResolution.method_definition(module_name, :to_str, :instance, scope: scope).nil?
2842
+ end
2843
+
2783
2844
  def unwrap_parens(node)
2784
2845
  while node.is_a?(Prism::ParenthesesNode) && node.body.is_a?(Prism::StatementsNode) &&
2785
2846
  node.body.body.size == 1
@@ -123,9 +123,14 @@ module Rigor
123
123
  # all.
124
124
  def answer_for_entry(scope, entry, method_name)
125
125
  unless entry.external?
126
- return source_defines?(scope, entry.name, method_name) ? [:source, entry.name] : nil
126
+ defines = source_defines?(scope, entry.name, method_name) || macro_named?(scope, entry.name, method_name)
127
+ return defines ? [:source, entry.name] : nil
127
128
  end
128
129
 
130
+ # Issue #2094 — a project module with no `def` is held as an external; a macro of its body may name it.
131
+ named = entry.candidates.find { |candidate| macro_named?(scope, candidate, method_name) }
132
+ return [:source, named] if named
133
+
129
134
  entry.candidates.each do |candidate|
130
135
  definition = Rigor::Reflection.instance_method_definition(candidate, method_name, scope: scope)
131
136
  return [private_definition?(definition) ? :private_rbs : :rbs, candidate, definition] if definition
@@ -137,6 +142,13 @@ module Rigor
137
142
  end
138
143
  private_class_method :answer_for_entry
139
144
 
145
+ # Issue #2094 — whether a class-body macro of `owner` was handed `method_name` as a literal
146
+ # (`dsl_method :xdyn`, `def_delegators :@target, :xdyn`). The indexer files every such name as an opaque
147
+ # envelope, since what the macro defines under it is unknown, so the module may define the name.
148
+ def macro_named?(scope, owner, method_name)
149
+ scope.parameter_envelopes_of(owner).key?([:instance, method_name.to_sym])
150
+ end
151
+
140
152
  def private_definition?(definition)
141
153
  definition.respond_to?(:accessibility) && definition.accessibility == :private
142
154
  end
@@ -39,7 +39,8 @@ module Rigor
39
39
  # `dependencies.source_inference:`), and no `discovery_seed:` base is merged in (the runner can start from one).
40
40
  NOT_SEEDED = {
41
41
  run_generation: "the identity token of a check run's return memo; these paths run no check",
42
- constant_sources: "read only by dependency recording, which only an incremental check run turns on",
42
+ constant_sources: "read by dependency recording and the #663 pre_eval retraction; a seed carries it only " \
43
+ "when asked (DiscoverySeed.tables_for_buffer(constant_sources: true), the editor publish)",
43
44
  param_inferred_types: "the parameter-inference output itself; each caller adds its own " \
44
45
  "(DiscoverySeed.build, coverage under parameter_inference:, the collector's last round)"
45
46
  }.freeze
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../type"
6
+
7
+ module Rigor
8
+ module Inference
9
+ # Issue #2127 — whether a call to a PROJECT method may run the block it passes more than once, read off the
10
+ # callee's body. {BlockRepetition.may_repeat?} answers only for the core iterators {ClosureEscapeAnalyzer}
11
+ # catalogues, so `def yielder = (yield; yield)` or the idiomatic wrapper `def each = @items.each { |i| yield i }`
12
+ # counted as running the block once, and a `return` in the block that only a later run reaches (`each { return
13
+ # false if (seen += 1) > 3 }`) never joined the method's return type.
14
+ #
15
+ # Only the later-run `return` pass asks this (`StatementEvaluator#collect_repeated_block_returns`); the block's
16
+ # entry scopes and the ADR-56 write-back keep {BlockRepetition}'s rule. That pass only widens the method's
17
+ # return type, so the rule here errs toward "repeats". The callee repeats when its body, outside a nested `def`
18
+ # (a `yield` in a class body is a syntax error inside a method):
19
+ #
20
+ # - holds two or more sites that run the block (`yield`, or `call` / `yield` / `===` / `[]` on the `&blk`
21
+ # parameter), or one inside a loop, a block or a lambda, which may run it many times; or
22
+ # - hands the block on (`each(&blk)`, `each(&)`, `...` forwarding) or reads the `&blk` parameter any other way
23
+ # (stores it, passes it as an argument), so code the walk does not see may run it any number of times.
24
+ #
25
+ # A callee the analysis cannot resolve to a project `def` (an unknown receiver, a method defined by
26
+ # metaprogramming) answers false, as before.
27
+ module ProjectYielder
28
+ module_function
29
+
30
+ BLOCK_RUN_CALLS = Set[:call, :yield, :===, :[]].freeze
31
+ REPEATING_NODES = Set[Prism::WhileNode, Prism::UntilNode, Prism::ForNode, Prism::BlockNode,
32
+ Prism::LambdaNode].freeze
33
+ private_constant :BLOCK_RUN_CALLS, :REPEATING_NODES
34
+
35
+ def may_run_block_repeatedly?(call_node, receiver_type, scope)
36
+ def_node = callee_def(call_node, receiver_type, scope)
37
+ return false unless def_node.is_a?(Prism::DefNode) && def_node.body
38
+
39
+ repeats?(def_node)
40
+ rescue StandardError
41
+ false
42
+ end
43
+
44
+ # Issue #2301 — whether the callee hands the block it is called with on to another call (`g(x, &)`,
45
+ # `g(x, &blk)`), outside a nested `def`. The block-return pass types such a forward's target `yield` as the
46
+ # caller's block ({ExpressionTyper#forwarded_block_return_type}), and the target may run it any number of
47
+ # times, so the caller's block is typed as one that may repeat. Answers false where the callee does not resolve.
48
+ def hands_block_on?(call_node, receiver_type, scope)
49
+ def_node = callee_def(call_node, receiver_type, scope)
50
+ return false unless def_node.is_a?(Prism::DefNode) && def_node.body
51
+
52
+ hand_off_in?(def_node.body, def_node.parameters&.block&.name)
53
+ rescue StandardError
54
+ false
55
+ end
56
+
57
+ def hand_off_in?(node, block_name)
58
+ return false if node.is_a?(Prism::DefNode)
59
+ return true if node.is_a?(Prism::BlockArgumentNode) && forwards_block?(node, block_name)
60
+
61
+ node.compact_child_nodes.any? { |child| hand_off_in?(child, block_name) }
62
+ end
63
+
64
+ # The `def` an instance call on a project class, a singleton call on one, or an implicit-self call to a
65
+ # top-level method resolves to.
66
+ def callee_def(call_node, receiver_type, scope)
67
+ name = call_node.name
68
+ found =
69
+ case receiver_type
70
+ when Type::Nominal then scope.user_def_through_ancestors(receiver_type.class_name, name).first
71
+ when Type::Singleton then scope.singleton_def_for(receiver_type.class_name, name)
72
+ end
73
+ found || (call_node.receiver.nil? ? scope.top_level_def_for(name) : nil)
74
+ end
75
+
76
+ def repeats?(def_node)
77
+ block_param = def_node.parameters&.block
78
+ counter = { sites: 0, repeats: false }
79
+ walk(def_node.body, block_param&.name, false, counter)
80
+ counter[:repeats] || counter[:sites] >= 2
81
+ end
82
+
83
+ def walk(node, block_name, repeating, counter)
84
+ return if counter[:repeats] || node.is_a?(Prism::DefNode)
85
+
86
+ skipped = visit(node, block_name, repeating, counter)
87
+ inner = repeating || REPEATING_NODES.include?(node.class)
88
+ node.compact_child_nodes.each do |child|
89
+ walk(child, block_name, inner, counter) unless child.equal?(skipped)
90
+ end
91
+ end
92
+
93
+ # Counts `node` and answers the child the walk must not visit: the `blk` receiver of a `blk.call`, which is a
94
+ # run, not a hand-off.
95
+ def visit(node, block_name, repeating, counter)
96
+ case node
97
+ when Prism::YieldNode then run_site(repeating, counter)
98
+ when Prism::ForwardingArgumentsNode then counter[:repeats] = true
99
+ when Prism::BlockArgumentNode then counter[:repeats] = true if forwards_block?(node, block_name)
100
+ when Prism::CallNode
101
+ return unless block_run_call?(node, block_name)
102
+
103
+ run_site(repeating, counter)
104
+ node.receiver
105
+ when Prism::LocalVariableReadNode then counter[:repeats] = true if block_name && node.name == block_name
106
+ end
107
+ end
108
+
109
+ def run_site(repeating, counter)
110
+ counter[:sites] += 1
111
+ counter[:repeats] = true if repeating
112
+ end
113
+
114
+ # `blk.call` and its spellings.
115
+ def block_run_call?(node, block_name)
116
+ receiver = node.receiver
117
+ !block_name.nil? && receiver.is_a?(Prism::LocalVariableReadNode) && receiver.name == block_name &&
118
+ BLOCK_RUN_CALLS.include?(node.name)
119
+ end
120
+
121
+ # `&blk` or an anonymous `&` handed to another call.
122
+ def forwards_block?(node, block_name)
123
+ expression = node.expression
124
+ expression.nil? || (expression.is_a?(Prism::LocalVariableReadNode) && expression.name == block_name)
125
+ end
126
+ end
127
+ end
128
+ end
@@ -66,6 +66,15 @@ module Rigor
66
66
  classifier.result
67
67
  end
68
68
 
69
+ # Issue #2021 — every contribution of the tree under `root`, for a file discovery reached only through a
70
+ # `require_relative` walk: another file the walk does not see may redefine what it defines, so none of its
71
+ # contributions is certain.
72
+ def every_contribution(root)
73
+ classifier = Classifier.new
74
+ classifier.possible(root)
75
+ classifier.result
76
+ end
77
+
69
78
  # Whether `node` was classified possible in `possible` (a {possible_nodes} answer).
70
79
  def possible?(possible, node)
71
80
  !possible.empty? && possible.include?(node)
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../reflection"
4
+ require_relative "../rbs_type_translator"
5
+
6
+ module Rigor
7
+ module Inference
8
+ module ScopeIndexer
9
+ # Issue #1406 — an ivar the class's RBS declares, as `@x: T` or through an `attr_reader x: T` /
10
+ # `attr_writer` / `attr_accessor` member (in `sig/`, or inline `attr_reader :x #: T`), reads the declared type
11
+ # in place of the census's join of the writes. The declaration is the author's contract for the field, so it
12
+ # wins over every write the census saw, #541's untyped `attr_writer` write included; whether the writes
13
+ # honour it is ADR-112 WD5's question (#1075), not this one's. It serves
14
+ # checking only: `rigor sig-gen` indexes with `declared_ivar_types: false` and keeps the census's answer.
15
+ #
16
+ # Only an ivar the census already holds is replaced, so a declaration adds no entry the file's writes did not.
17
+ # A declaration is not taken when it is `untyped` (what rbs-inline supplies for a bare `attr_reader :x`, which
18
+ # the type specification reads as a placeholder) or mentions a type variable (`@x: T` in `class Box[T]` is
19
+ # unsolved at a read), and the census entry stands.
20
+ module DeclaredIvarTypes
21
+ module_function
22
+
23
+ def apply!(accumulator, scope)
24
+ environment = scope&.environment
25
+ return if environment.nil?
26
+
27
+ accumulator.each do |class_name, entries|
28
+ next unless class_name.is_a?(String) # a guard: the side-table keys are deleted before this runs
29
+
30
+ declared = declared_ivars(class_name, environment)
31
+ next if declared.empty?
32
+
33
+ accumulator[class_name] = entries.to_h do |ivar, type|
34
+ [ivar, declared.fetch(ivar, type)]
35
+ end
36
+ end
37
+ end
38
+
39
+ # `{ivar name => Rigor::Type}` for the ivars `class_name`'s RBS instance definition declares with a usable
40
+ # type, or `{}`.
41
+ def declared_ivars(class_name, environment)
42
+ definition = Reflection.instance_definition(class_name, environment: environment)
43
+ return {} if definition.nil?
44
+
45
+ instance = Type::Combinator.nominal_of(class_name)
46
+ definition.instance_variables.each_with_object({}) do |(name, variable), acc|
47
+ type = translate(variable.type, instance, environment)
48
+ acc[name] = type if type
49
+ end
50
+ rescue StandardError # a guard: a definition that fails to build declares nothing
51
+ {}
52
+ end
53
+
54
+ def translate(rbs_type, instance, environment)
55
+ return nil unless rbs_type.free_variables.empty?
56
+
57
+ type = RbsTypeTranslator.translate(rbs_type, self_type: instance, instance_type: instance,
58
+ alias_expander: environment.rbs_loader)
59
+ type.is_a?(Type::Dynamic) ? nil : type
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end