rigortype 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/docs/handbook/11-sig-gen.md +100 -18
  4. data/docs/manual/02-cli-reference.md +85 -16
  5. data/docs/manual/03-configuration.md +12 -9
  6. data/docs/manual/04-diagnostics.md +17 -6
  7. data/docs/manual/05-inspecting-types.md +2 -2
  8. data/docs/manual/06-baseline.md +1 -1
  9. data/docs/manual/07-plugins.md +2 -1
  10. data/docs/manual/08-skills.md +7 -0
  11. data/docs/manual/10-mcp-server.md +17 -2
  12. data/docs/manual/12-caching.md +8 -1
  13. data/docs/manual/18-removing-dead-code.md +18 -7
  14. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  15. data/docs/manual/plugins/rigor-actionpack.md +30 -0
  16. data/docs/manual/plugins/rigor-devise.md +15 -0
  17. data/docs/manual/plugins/rigor-rspec.md +102 -1
  18. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  19. data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
  20. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
  21. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
  22. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +115 -7
  23. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +253 -0
  24. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  25. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  26. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  27. data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
  28. data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
  29. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +64 -4
  30. data/lib/rigor/analysis/check_rules.rb +319 -21
  31. data/lib/rigor/analysis/dependency_recorder.rb +11 -0
  32. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  33. data/lib/rigor/analysis/edit_replay.rb +67 -0
  34. data/lib/rigor/analysis/incremental.rb +86 -0
  35. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
  36. data/lib/rigor/analysis/incremental_session.rb +336 -46
  37. data/lib/rigor/analysis/path_expansion.rb +44 -1
  38. data/lib/rigor/analysis/reachability/graph.rb +90 -13
  39. data/lib/rigor/analysis/reachability/scan.rb +279 -39
  40. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  41. data/lib/rigor/analysis/rule_catalog.rb +22 -5
  42. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  43. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  44. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  45. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +82 -16
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
  48. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  49. data/lib/rigor/analysis/runner.rb +222 -111
  50. data/lib/rigor/analysis/severity_stamp.rb +11 -0
  51. data/lib/rigor/analysis/worker_session.rb +24 -1
  52. data/lib/rigor/cache/descriptor.rb +40 -7
  53. data/lib/rigor/cache/engine_source.rb +3 -2
  54. data/lib/rigor/cache/incremental_snapshot.rb +49 -8
  55. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  56. data/lib/rigor/cache/store.rb +4 -3
  57. data/lib/rigor/cli/check_command.rb +93 -4
  58. data/lib/rigor/cli/command.rb +3 -1
  59. data/lib/rigor/cli/diff_command.rb +2 -1
  60. data/lib/rigor/cli/docs_command.rb +3 -2
  61. data/lib/rigor/cli/doctor_command.rb +1 -1
  62. data/lib/rigor/cli/explain_command.rb +3 -2
  63. data/lib/rigor/cli/lens_command.rb +5 -5
  64. data/lib/rigor/cli/mcp_command.rb +2 -1
  65. data/lib/rigor/cli/options.rb +20 -3
  66. data/lib/rigor/cli/plugin_command.rb +2 -1
  67. data/lib/rigor/cli/plugins_command.rb +7 -1
  68. data/lib/rigor/cli/plugins_renderer.rb +24 -2
  69. data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
  70. data/lib/rigor/cli/sig_gen_command.rb +73 -13
  71. data/lib/rigor/cli/skill_command.rb +2 -1
  72. data/lib/rigor/cli/skill_describe.rb +3 -2
  73. data/lib/rigor/cli/type_of_command.rb +2 -1
  74. data/lib/rigor/cli/type_of_project_scope.rb +27 -0
  75. data/lib/rigor/cli/unused_command.rb +10 -7
  76. data/lib/rigor/cli.rb +3 -2
  77. data/lib/rigor/config_audit.rb +72 -16
  78. data/lib/rigor/configuration/severity_profile.rb +20 -2
  79. data/lib/rigor/configuration.rb +43 -7
  80. data/lib/rigor/effects/signature_sources.rb +3 -1
  81. data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
  82. data/lib/rigor/environment/gated_signature_guard.rb +2 -1
  83. data/lib/rigor/environment/gated_signature_plan.rb +2 -1
  84. data/lib/rigor/environment/installed_gem_set.rb +2 -1
  85. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
  86. data/lib/rigor/environment/rbs_loader.rb +136 -7
  87. data/lib/rigor/environment/required_features.rb +136 -29
  88. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  89. data/lib/rigor/environment.rb +4 -2
  90. data/lib/rigor/inference/block_call_timing.rb +7 -1
  91. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  92. data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
  93. data/lib/rigor/inference/captured_locals.rb +44 -13
  94. data/lib/rigor/inference/clause_widening.rb +90 -0
  95. data/lib/rigor/inference/deep_erasure.rb +138 -0
  96. data/lib/rigor/inference/def_return_typer.rb +159 -9
  97. data/lib/rigor/inference/definer_resolution.rb +101 -20
  98. data/lib/rigor/inference/error_info.rb +17 -3
  99. data/lib/rigor/inference/expression_typer.rb +97 -18
  100. data/lib/rigor/inference/global_write_census.rb +36 -1
  101. data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
  102. data/lib/rigor/inference/ivar_writer_census.rb +195 -0
  103. data/lib/rigor/inference/loop_value_record.rb +53 -0
  104. data/lib/rigor/inference/macro_block_self_type.rb +20 -6
  105. data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
  106. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +278 -42
  107. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
  108. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
  109. data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
  110. data/lib/rigor/inference/method_dispatcher.rb +113 -19
  111. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  112. data/lib/rigor/inference/module_function_state.rb +149 -42
  113. data/lib/rigor/inference/mutation_widening.rb +5 -2
  114. data/lib/rigor/inference/narrowing.rb +116 -11
  115. data/lib/rigor/inference/object_mixins.rb +13 -1
  116. data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
  117. data/lib/rigor/inference/project_patched_methods.rb +168 -4
  118. data/lib/rigor/inference/project_patched_mixins.rb +162 -0
  119. data/lib/rigor/inference/project_patched_scanner.rb +51 -12
  120. data/lib/rigor/inference/project_patched_visibility.rb +214 -0
  121. data/lib/rigor/inference/project_seed_tables.rb +121 -0
  122. data/lib/rigor/inference/project_yielder.rb +128 -0
  123. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
  124. data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
  125. data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
  126. data/lib/rigor/inference/return_barrier.rb +16 -0
  127. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
  128. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  129. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
  130. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  131. data/lib/rigor/inference/scope_indexer.rb +1144 -197
  132. data/lib/rigor/inference/self_call_ivar_writes.rb +29 -12
  133. data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
  134. data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
  135. data/lib/rigor/inference/statement_evaluator.rb +1088 -213
  136. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  137. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  138. data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
  139. data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
  140. data/lib/rigor/language_server/buffer_table.rb +6 -0
  141. data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
  142. data/lib/rigor/language_server/project_context.rb +59 -3
  143. data/lib/rigor/language_server/server.rb +5 -3
  144. data/lib/rigor/lens/contract_reader.rb +45 -2
  145. data/lib/rigor/lens/document.rb +2 -1
  146. data/lib/rigor/lens/file_reader.rb +12 -2
  147. data/lib/rigor/lens/members.rb +16 -2
  148. data/lib/rigor/lens/run.rb +34 -7
  149. data/lib/rigor/mcp/server.rb +11 -3
  150. data/lib/rigor/plugin/base.rb +29 -13
  151. data/lib/rigor/plugin/io_boundary.rb +42 -6
  152. data/lib/rigor/plugin/isolation.rb +4 -3
  153. data/lib/rigor/plugin/manifest.rb +29 -3
  154. data/lib/rigor/plugin/registry.rb +38 -0
  155. data/lib/rigor/protection/discovery_seed.rb +64 -36
  156. data/lib/rigor/protection/mutation_cache.rb +8 -2
  157. data/lib/rigor/rooted_glob.rb +36 -0
  158. data/lib/rigor/scope/discovery_index.rb +5 -0
  159. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
  160. data/lib/rigor/scope/resolution_chain.rb +40 -5
  161. data/lib/rigor/scope.rb +59 -7
  162. data/lib/rigor/sig_gen/alias_index.rb +2 -1
  163. data/lib/rigor/sig_gen/classification.rb +4 -1
  164. data/lib/rigor/sig_gen/generator.rb +465 -49
  165. data/lib/rigor/sig_gen/layout_index.rb +5 -2
  166. data/lib/rigor/sig_gen/observation_collector.rb +141 -12
  167. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  168. data/lib/rigor/sig_gen/renderer.rb +16 -155
  169. data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
  170. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  171. data/lib/rigor/sig_gen/write_reporter.rb +203 -0
  172. data/lib/rigor/sig_gen/write_result.rb +3 -1
  173. data/lib/rigor/sig_gen/writer.rb +5 -3
  174. data/lib/rigor/signature_path_audit.rb +4 -2
  175. data/lib/rigor/type/combinator.rb +4 -3
  176. data/lib/rigor/type/constant.rb +54 -3
  177. data/lib/rigor/type/hash_shape.rb +14 -9
  178. data/lib/rigor/type/struct_instance.rb +3 -2
  179. data/lib/rigor/version.rb +1 -1
  180. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  181. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +61 -44
  182. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
  183. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
  184. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
  185. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  186. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
  187. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
  188. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
  189. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
  190. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
  191. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
  192. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
  193. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
  194. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  195. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  196. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  197. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
  198. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
  199. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
  200. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
  201. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
  202. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
  203. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
  204. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  205. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  206. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
  207. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
  208. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
  209. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
  210. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
  211. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  212. data/sig/rigor/analysis/baseline.rbs +3 -2
  213. data/sig/rigor/effects/file_collection.rbs +5 -6
  214. data/sig/rigor/effects/registry.rbs +8 -6
  215. data/sig/rigor/effects/summary.rbs +6 -5
  216. data/sig/rigor/environment.rbs +5 -3
  217. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  218. data/sig/rigor/inference.rbs +6 -0
  219. data/sig/rigor/plugin/additional_initializer.rbs +5 -5
  220. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  221. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  222. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  223. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  224. data/sig/rigor/plugin/io_boundary.rbs +5 -2
  225. data/sig/rigor/plugin/manifest.rbs +16 -18
  226. data/sig/rigor/plugin/protocol_contract.rbs +10 -9
  227. data/sig/rigor/plugin/registry.rbs +1 -1
  228. data/sig/rigor/rooted_glob.rbs +7 -0
  229. data/sig/rigor/scope.rbs +34 -52
  230. data/sig/rigor/type.rbs +0 -6
  231. data/skills/rigor-doctor/references/01-checks.md +23 -11
  232. data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
  233. metadata +32 -1
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Inference
7
+ # Issue #1154 — the instance-variable writers {GlobalWriteCensus} records for the declared-parameter credit
8
+ # ({ScopeIndexer::DeclaredParamWrites}), which must not type an ivar a writer the class-ivar census never walks may
9
+ # replace. The standard is parity with a literal write: `@x = 1` in `initialize` already reads `1` in every sibling
10
+ # method whatever an unknown object's code does, so only the writers that literal writes would also have to answer
11
+ # for are recorded, and an unknown-receiver writer records nothing (as `ReflectiveIvarWrite` leaves
12
+ # `o.instance_variable_set` alone). Three entry kinds go into the census `Set`:
13
+ #
14
+ # - `[:ivar_set, name]` — a literal `o.instance_variable_set(:@x, v)` (or the same through a `send` that spells
15
+ # it) on an explicit receiver other than `self`, and an `@x` write in the literal block of an
16
+ # `o.instance_eval { … }` / `instance_exec`: code that names the ivar it writes.
17
+ # - `[:ivar_set_class, class_name]` — an open-world writer ({#open_world?}) with an implicit or `self` receiver
18
+ # inside an instance body of the class — a `def` or a `define_method` block in its class body, in the block of a
19
+ # `K = Struct.new` / `Data.define` / `Class.new` constant, or in a `K.class_eval` block (the host walk passes
20
+ # that class as `owner`): it may write any ivar of the class's instances. A writer in a class body, a
21
+ # `def self.` or at the top level records nothing.
22
+ # - `[:own_write, class_name, name]` — an `@x` write inside an instance body of the class.
23
+ # {ScopeIndexer#merge_constant_literal_tables} appends the file's path, so a reader can tell whether a second
24
+ # file writes the same ivar of the same class.
25
+ class IvarWriterCensus
26
+ SEND_CALLS = %i[send __send__ public_send].to_set.freeze
27
+ # The methods a `send` reaching which is an open-world writer.
28
+ OPEN_WORLD_SENDS = %i[instance_variable_set instance_eval instance_exec extend
29
+ define_singleton_method].to_set.freeze
30
+ EVAL_CALLS = %i[instance_eval instance_exec].to_set.freeze
31
+ # The calls on a `singleton_class` receiver that give the object methods or code the census cannot read.
32
+ SINGLETON_CLASS_CALLS = %i[
33
+ class_eval class_exec module_eval module_exec define_method include prepend extend attr attr_reader
34
+ attr_writer attr_accessor send public_send __send__
35
+ ].to_set.freeze
36
+ NO_ARGUMENTS = [].freeze
37
+
38
+ # The call names any rule here reads, so every other call returns at one Set probe.
39
+ # (`define_method` is in {SINGLETON_CLASS_CALLS}.)
40
+ WATCHED_CALLS = (SEND_CALLS | OPEN_WORLD_SENDS | EVAL_CALLS | SINGLETON_CLASS_CALLS).freeze
41
+
42
+ def initialize(census)
43
+ @census = census
44
+ @instance_ranges = []
45
+ @owner_names = {}.compare_by_identity
46
+ @own_writes = {}
47
+ end
48
+
49
+ # Fed each node of a pre-order descent. `owner` is the class whose body the node sits in, as its qualified name
50
+ # or as the host walk's prefix Array (joined here only when an entry needs it), or nil where no class owns an
51
+ # instance (`class << self`, the top level, an opaque block).
52
+ def visit(node, owner)
53
+ case node
54
+ when Prism::DefNode then remember(node.location, owner) if owner && node.receiver.nil?
55
+ when Prism::SingletonClassNode then visit_singleton_class(node)
56
+ when Prism::CallNode then visit_call(node, owner) if WATCHED_CALLS.include?(node.name)
57
+ when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
58
+ Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
59
+ record_own_write(node) unless @instance_ranges.empty?
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ # One entry per class and ivar: a repeated write costs a Hash probe, not a location and an Array.
66
+ def record_own_write(node)
67
+ owner = instance_owner(node)
68
+ return if owner.nil?
69
+
70
+ seen = (@own_writes[owner] ||= {})
71
+ return if seen[node.name]
72
+
73
+ seen[node.name] = true
74
+ @census << [:own_write, owner_name(owner), node.name].freeze
75
+ end
76
+
77
+ def owner_name(owner) = owner.is_a?(Array) ? (@owner_names[owner] ||= owner.join("::")) : owner
78
+
79
+ def visit_call(node, owner)
80
+ remember(node.block.location, owner) if owner && define_method_block?(node)
81
+ if self_receiver?(node) || own_singleton_class_call?(node)
82
+ return if @instance_ranges.empty? || !(own_singleton_class_call?(node) || open_world?(node))
83
+
84
+ instance = instance_owner(node)
85
+ record_class(instance) if instance
86
+ else
87
+ visit_foreign(node)
88
+ end
89
+ end
90
+
91
+ # `class << self` inside an instance `def` opens the instance's singleton class.
92
+ def visit_singleton_class(node)
93
+ return unless node.expression.is_a?(Prism::SelfNode) && !@instance_ranges.empty?
94
+
95
+ instance = instance_owner(node)
96
+ record_class(instance) if instance
97
+ end
98
+
99
+ def record_class(owner) = @census << [:ivar_set_class, owner_name(owner)].freeze
100
+
101
+ # The self writers whose target or code the census cannot read: a `send` with a computed method name or one
102
+ # naming {OPEN_WORLD_SENDS}; `instance_eval` / `instance_exec` given a block argument, and `instance_eval`
103
+ # given a string; `extend`; `define_singleton_method` with a body; and an `instance_variable_set` whose name no
104
+ # literal spells. A call in {SINGLETON_CLASS_CALLS} on the object's own `singleton_class` counts as well.
105
+ def open_world?(node)
106
+ name = node.name
107
+ arguments = node.arguments&.arguments || NO_ARGUMENTS
108
+ return open_world_send?(arguments) if SEND_CALLS.include?(name)
109
+ return open_world_eval?(node, arguments) if EVAL_CALLS.include?(name)
110
+ # #1931's census already makes every ivar of the class gradual for one in the same file; this entry covers one
111
+ # in a reopening in another file, which that census does not see.
112
+ return ivar_name(arguments.first).nil? if name == :instance_variable_set
113
+
114
+ name == :extend || (name == :define_singleton_method && (!node.block.nil? || arguments.size > 1))
115
+ end
116
+
117
+ def open_world_send?(arguments)
118
+ return false if arguments.empty?
119
+
120
+ sent = literal_name(arguments.first)
121
+ sent.nil? || OPEN_WORLD_SENDS.include?(sent)
122
+ end
123
+
124
+ def open_world_eval?(node, arguments)
125
+ node.block.is_a?(Prism::BlockArgumentNode) || (node.name == :instance_eval && !arguments.empty?)
126
+ end
127
+
128
+ # A call on another receiver: a named foreign write, or `self.singleton_class.class_eval` and its kin.
129
+ def visit_foreign(node)
130
+ arguments = node.arguments&.arguments || NO_ARGUMENTS
131
+ name = node.name
132
+ if SEND_CALLS.include?(name) && arguments.first && literal_name(arguments.first) == :instance_variable_set
133
+ name = :instance_variable_set
134
+ arguments = arguments.drop(1)
135
+ end
136
+ if name == :instance_variable_set
137
+ ivar = ivar_name(arguments.first)
138
+ @census << [:ivar_set, ivar].freeze if ivar
139
+ elsif EVAL_CALLS.include?(name) && node.block.is_a?(Prism::BlockNode)
140
+ record_block_ivar_writes(node.block)
141
+ end
142
+ end
143
+
144
+ def record_block_ivar_writes(node)
145
+ return unless node.is_a?(Prism::Node)
146
+ return if node.is_a?(Prism::DefNode) || node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
147
+
148
+ case node
149
+ when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
150
+ Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
151
+ @census << [:ivar_set, node.name].freeze
152
+ end
153
+ node.rigor_each_child { |child| record_block_ivar_writes(child) }
154
+ end
155
+
156
+ def define_method_block?(node)
157
+ node.name == :define_method && self_receiver?(node) && node.block.is_a?(Prism::BlockNode)
158
+ end
159
+
160
+ # An instance body (`def` or `define_method` block) and the class it belongs to. A `define_method` block's own
161
+ # nodes may reach the walk with no owner (the walk reads its `self` as opaque), so the class rides the range.
162
+ def remember(location, owner)
163
+ @instance_ranges << [(location.start_offset...location.end_offset), owner]
164
+ end
165
+
166
+ # The class of the innermost instance body covering `node` (pre-order, so the last covering range), or nil.
167
+ def instance_owner(node)
168
+ offset = node.location.start_offset
169
+ @instance_ranges.reverse_each { |(range, owner)| return owner if range.cover?(offset) }
170
+ nil
171
+ end
172
+
173
+ def self_receiver?(node) = node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
174
+
175
+ # A call in {SINGLETON_CLASS_CALLS} on the object's own `singleton_class` (`singleton_class.class_eval { … }`).
176
+ def own_singleton_class_call?(node)
177
+ receiver = node.receiver
178
+ receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && self_receiver?(receiver) &&
179
+ SINGLETON_CLASS_CALLS.include?(node.name)
180
+ end
181
+
182
+ def literal_name(argument)
183
+ return unless argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode)
184
+
185
+ text = argument.unescaped
186
+ text.to_sym if text.valid_encoding?
187
+ end
188
+
189
+ def ivar_name(argument)
190
+ name = literal_name(argument)
191
+ name if name.to_s.start_with?("@")
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Inference
5
+ # The value the statement evaluator last gave a loop node, and the scope it evaluated the loop from, so
6
+ # `ExpressionTyper#type_of_loop` reads it instead of evaluating the loop again (issue #2031 review). A loop's value
7
+ # costs an evaluation of its body, its fixpoint and its `break` passes included, and the typer types a loop again
8
+ # wherever it types a node that holds one: the value of a write that wraps it (`id(z = (while …))`), a call it is
9
+ # the receiver of (`(while …).itself`), each narrowing and rule that types that operand. Each such typing of a
10
+ # loop nested in another loop re-ran the inner loop's passes, once more per level of nesting.
11
+ #
12
+ # The record is reused only for the very scope object the loop was evaluated from, so it answers exactly what an
13
+ # evaluation would. It is not compared with `Scope#==`, which ignores several fields (`match_frame`,
14
+ # `declared_refinements`, `struct_fold_safe_locals`, the opaque-block flags, `lexical_nesting`, the origins tables,
15
+ # `discovery`): across the cost spec's 10933 reads every match was an identity match, and `lib/` read no record at
16
+ # all (issue #2262). It lives in a per-thread map whose keys are held weakly, so a record goes when its parse tree
17
+ # does.
18
+ module LoopValueRecord
19
+ KEY = :rigor_loop_value_record
20
+ private_constant :KEY
21
+
22
+ module_function
23
+
24
+ COUNT_KEY = :rigor_loop_value_count
25
+ private_constant :COUNT_KEY
26
+
27
+ def record(node, scope, type)
28
+ Thread.current[COUNT_KEY] = loops_evaluated + 1
29
+ conditional(node, scope, type)
30
+ end
31
+
32
+ # How many loops this thread has evaluated, so a conditional can tell whether its evaluation held one.
33
+ def loops_evaluated = Thread.current[COUNT_KEY] || 0
34
+
35
+ # The same record for a conditional that evaluated a loop in a branch: `id(c ? (while …) : 0)` types the
36
+ # conditional again from the scope it was evaluated from, and each typing evaluated the loop again from a
37
+ # branch scope of its own, which multiplied the passes of every loop nested in it (issue #2262).
38
+ def conditional(node, scope, type)
39
+ (Thread.current[KEY] ||= ObjectSpace::WeakKeyMap.new)[node] = [scope, type]
40
+ type
41
+ end
42
+
43
+ # The recorded value of `node` evaluated from `scope` itself, or nil.
44
+ def fetch(node, scope)
45
+ slot = Thread.current[KEY]&.[](node)
46
+ return nil if slot.nil?
47
+
48
+ recorded, type = slot
49
+ recorded.equal?(scope) ? type : nil
50
+ end
51
+ end
52
+ end
53
+ end
@@ -47,6 +47,25 @@ module Rigor
47
47
  # same call, another plugin's included, is not consulted. A `:lexical` entry (issue #1667) keeps the caller's
48
48
  # `self`, so its `self_type` is the calling scope's.
49
49
  def match_for(scope:, call_node:, receiver_type:)
50
+ entry = first_matching_entry(scope, call_node, receiver_type)
51
+ return nil if entry.nil?
52
+
53
+ receiver_name = singleton_receiver_class_name(receiver_type) || nominal_receiver_class_name(receiver_type)
54
+ entry_match(entry, receiver_name, scope, scope.environment)
55
+ end
56
+
57
+ # Whether `call_node`'s block runs as an instance method of the receiver class (issue #1768): its first matching
58
+ # entry binds `:receiver_instance`, so the block body's `self` is the instance {#match_for} narrows to and its
59
+ # `@x` writes are that instance's ivars, as a `def` body's are. A `:lexical` or named-`self_type` entry runs the
60
+ # block on another object, and no entry means no narrowing.
61
+ def runs_as_receiver_instance_method?(scope:, call_node:, receiver_type:)
62
+ entry = first_matching_entry(scope, call_node, receiver_type)
63
+ !entry.nil? && entry.self_type == Plugin::Macro::BlockAsMethod::SELF_TYPE_RECEIVER_INSTANCE
64
+ end
65
+
66
+ # The first registered entry that matches `call_node` on `receiver_type`, in (plugin registration, declaration)
67
+ # order, or nil.
68
+ def first_matching_entry(scope, call_node, receiver_type)
50
69
  return nil if receiver_type.nil?
51
70
 
52
71
  environment = scope&.environment
@@ -61,12 +80,7 @@ module Rigor
61
80
  # (plugin registration, declaration) order; the method-name membership is guaranteed
62
81
  # by the table key.
63
82
  entries = registry.contribution_index.block_entries_for(call_node.name)
64
- entries.each do |entry|
65
- next unless entry_matches?(entry, singleton_name, nominal_name, call_node, scope, environment)
66
-
67
- return entry_match(entry, singleton_name || nominal_name, scope, environment)
68
- end
69
- nil
83
+ entries.find { |entry| entry_matches?(entry, singleton_name, nominal_name, call_node, scope, environment) }
70
84
  end
71
85
 
72
86
  # The {Match} a matching entry contributes: a `:lexical` one keeps the calling scope's `self`, unknown where the
@@ -88,6 +88,18 @@ module Rigor
88
88
  @forwarded_block == name
89
89
  end
90
90
 
91
+ # Issue #1157 — whether a call's `&expr` block argument hands on the block THIS method was called with: the
92
+ # anonymous `&` of a method that declares an anonymous `&` parameter, or {#forwarded_block?} for `&name`.
93
+ # Anything else (`&:sym`, a proc made here, a rebound name) is a block of this frame's own making.
94
+ def forwards_block?(block_argument)
95
+ expression = block_argument.expression
96
+ case expression
97
+ when nil then anonymous_block_parameter?
98
+ when Prism::LocalVariableReadNode then forwarded_block?(expression.name)
99
+ else false
100
+ end
101
+ end
102
+
91
103
  # The scan of `node` under `scope`, kept while `scope`'s local and instance-variable tables are the same
92
104
  # objects: a lookup argument's answer reads them ({MatchRebinding.may_match?}), and a rebuild that leaves
93
105
  # them alone passes the same tables on. `kind` keeps another reading of the same node apart (issue #1365:
@@ -125,6 +137,13 @@ module Rigor
125
137
  name
126
138
  end
127
139
 
140
+ # True when the method declares a bare `&` parameter, which only an anonymous `&` argument can read.
141
+ def anonymous_block_parameter?
142
+ parameters = @parameters
143
+ block = parameters.is_a?(Prism::ParametersNode) ? parameters.block : nil
144
+ !block.nil? && block.name.nil?
145
+ end
146
+
128
147
  # The method's own `&block` parameter's name, or nil.
129
148
  def block_parameter_name
130
149
  parameters = @parameters