rigortype 0.4.7 → 0.4.8

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 (268) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/docs/handbook/01-getting-started.md +1 -1
  8. data/docs/handbook/11-sig-gen.md +35 -0
  9. data/docs/install.md +20 -0
  10. data/docs/manual/01-installation.md +67 -26
  11. data/docs/manual/02-cli-reference.md +307 -91
  12. data/docs/manual/03-configuration.md +41 -3
  13. data/docs/manual/04-diagnostics.md +5 -4
  14. data/docs/manual/08-skills.md +11 -0
  15. data/docs/manual/09-editor-integration.md +8 -0
  16. data/docs/manual/14-rails-quickstart.md +1 -1
  17. data/docs/manual/19-effect-labels.md +3 -4
  18. data/docs/manual/plugins/rigor-actionpack.md +15 -6
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
  20. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  21. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  22. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  23. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
  24. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
  25. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
  26. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  27. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
  28. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  29. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  30. data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
  31. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  32. data/lib/rigor/analysis/check_rules.rb +598 -99
  33. data/lib/rigor/analysis/crash_signature.rb +43 -1
  34. data/lib/rigor/analysis/dependency_recorder.rb +233 -76
  35. data/lib/rigor/analysis/incremental.rb +11 -1
  36. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
  37. data/lib/rigor/analysis/incremental_session.rb +279 -37
  38. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  39. data/lib/rigor/analysis/rule_catalog.rb +37 -1
  40. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  41. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  42. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
  43. data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
  44. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
  45. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  48. data/lib/rigor/analysis/runner.rb +222 -39
  49. data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
  50. data/lib/rigor/analysis/worker_session.rb +42 -41
  51. data/lib/rigor/bleeding_edge.rb +19 -4
  52. data/lib/rigor/cache/file_digest.rb +39 -31
  53. data/lib/rigor/cache/incremental_snapshot.rb +66 -7
  54. data/lib/rigor/cache/rbs_descriptor.rb +37 -2
  55. data/lib/rigor/cache/rbs_environment.rb +5 -1
  56. data/lib/rigor/cli/baseline_command.rb +15 -2
  57. data/lib/rigor/cli/check_command.rb +4 -2
  58. data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
  59. data/lib/rigor/cli/lens_command.rb +17 -2
  60. data/lib/rigor/cli/sig_gen_command.rb +8 -0
  61. data/lib/rigor/cli/skill_describe.rb +1 -0
  62. data/lib/rigor/cli.rb +5 -2
  63. data/lib/rigor/config_audit.rb +32 -18
  64. data/lib/rigor/configuration/severity_profile.rb +9 -0
  65. data/lib/rigor/configuration.rb +32 -5
  66. data/lib/rigor/effects/collector.rb +53 -21
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
  68. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  69. data/lib/rigor/environment/default_libraries.rb +17 -0
  70. data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
  71. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  72. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  73. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  74. data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
  75. data/lib/rigor/environment/rbs_loader.rb +312 -63
  76. data/lib/rigor/environment.rb +131 -35
  77. data/lib/rigor/inference/acceptance.rb +50 -15
  78. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  79. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  80. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  81. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  82. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  83. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  84. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  85. data/lib/rigor/inference/block_run_count.rb +9 -2
  86. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  87. data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
  88. data/lib/rigor/inference/captured_locals.rb +2 -0
  89. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  90. data/lib/rigor/inference/clause_widening.rb +1 -1
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  92. data/lib/rigor/inference/construction_escape.rb +56 -15
  93. data/lib/rigor/inference/declaration_walk.rb +4 -0
  94. data/lib/rigor/inference/def_node_resolver.rb +32 -14
  95. data/lib/rigor/inference/def_return_typer.rb +1 -1
  96. data/lib/rigor/inference/definer_resolution.rb +15 -7
  97. data/lib/rigor/inference/definers_index.rb +72 -16
  98. data/lib/rigor/inference/error_info.rb +16 -0
  99. data/lib/rigor/inference/expression_typer.rb +1459 -287
  100. data/lib/rigor/inference/flow_tracer.rb +31 -14
  101. data/lib/rigor/inference/fork_map.rb +3 -0
  102. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  103. data/lib/rigor/inference/global_write_census.rb +8 -6
  104. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  105. data/lib/rigor/inference/indexed_narrowing.rb +57 -8
  106. data/lib/rigor/inference/ivar_writer_census.rb +106 -11
  107. data/lib/rigor/inference/key_presence_guard.rb +51 -25
  108. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  109. data/lib/rigor/inference/last_line.rb +5 -2
  110. data/lib/rigor/inference/loop_value_record.rb +63 -15
  111. data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
  112. data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
  113. data/lib/rigor/inference/match_rebinding.rb +21 -8
  114. data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
  115. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  116. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  117. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  118. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  119. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  120. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  121. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
  122. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
  123. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
  124. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
  125. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  126. data/lib/rigor/inference/method_dispatcher.rb +66 -18
  127. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  128. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  129. data/lib/rigor/inference/module_function_state.rb +13 -2
  130. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  131. data/lib/rigor/inference/mutation_widening.rb +44 -4
  132. data/lib/rigor/inference/narrowing.rb +201 -27
  133. data/lib/rigor/inference/operand_effects.rb +43 -2
  134. data/lib/rigor/inference/optimistic_origin.rb +133 -8
  135. data/lib/rigor/inference/origin_lookup.rb +1 -1
  136. data/lib/rigor/inference/precision_scanner.rb +1 -1
  137. data/lib/rigor/inference/project_method_ownership.rb +28 -10
  138. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  139. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  140. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  141. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  142. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  143. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  144. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
  145. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
  146. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  147. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  148. data/lib/rigor/inference/remover_holder_scan.rb +76 -22
  149. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  150. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
  151. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
  152. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  153. data/lib/rigor/inference/scope_indexer.rb +2079 -183
  154. data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
  155. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  156. data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
  157. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  158. data/lib/rigor/inference/statement_evaluator.rb +970 -250
  159. data/lib/rigor/inference/string_mutation.rb +14 -0
  160. data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
  161. data/lib/rigor/inference/toplevel_def_census.rb +14 -6
  162. data/lib/rigor/inference/unknown_store_widening.rb +1 -1
  163. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  164. data/lib/rigor/inference/void_tail_summary.rb +29 -10
  165. data/lib/rigor/lens/claims/comparison.rb +78 -0
  166. data/lib/rigor/lens/claims/request.rb +131 -0
  167. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  168. data/lib/rigor/lens/claims.rb +208 -0
  169. data/lib/rigor/lens/contract_reader.rb +5 -0
  170. data/lib/rigor/lens/document.rb +1 -1
  171. data/lib/rigor/lens/file_reader.rb +21 -8
  172. data/lib/rigor/lens/run.rb +8 -0
  173. data/lib/rigor/lens.rb +1 -1
  174. data/lib/rigor/library_probe.rb +68 -0
  175. data/lib/rigor/memo_audit.rb +282 -0
  176. data/lib/rigor/plugin/base.rb +15 -2
  177. data/lib/rigor/plugin/io_boundary.rb +25 -3
  178. data/lib/rigor/plugin/manifest.rb +7 -3
  179. data/lib/rigor/plugin/registry.rb +13 -2
  180. data/lib/rigor/plugin/trust_policy.rb +46 -0
  181. data/lib/rigor/protection/mutation_cache.rb +4 -1
  182. data/lib/rigor/reflection/constant_ancestors.rb +79 -22
  183. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  184. data/lib/rigor/reflection/declarations.rb +58 -0
  185. data/lib/rigor/reflection/project_method.rb +177 -0
  186. data/lib/rigor/reflection.rb +2 -0
  187. data/lib/rigor/rooted_glob.rb +18 -1
  188. data/lib/rigor/run_scoped_slots.rb +20 -20
  189. data/lib/rigor/scope/discovery_index.rb +149 -2
  190. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  191. data/lib/rigor/scope/resolution_chain.rb +900 -55
  192. data/lib/rigor/scope.rb +307 -34
  193. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  194. data/lib/rigor/sig_gen/classification.rb +14 -1
  195. data/lib/rigor/sig_gen/generator.rb +222 -13
  196. data/lib/rigor/sig_gen/method_candidate.rb +14 -0
  197. data/lib/rigor/sig_gen/observation_collector.rb +11 -2
  198. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  199. data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
  200. data/lib/rigor/sig_gen/writer.rb +21 -0
  201. data/lib/rigor/sig_gen.rb +1 -0
  202. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  203. data/lib/rigor/source/node_children.rb +13 -0
  204. data/lib/rigor/type/combinator.rb +164 -8
  205. data/lib/rigor/type/interface.rb +60 -0
  206. data/lib/rigor/type.rb +1 -0
  207. data/lib/rigor/type_of_memo/serving.rb +99 -0
  208. data/lib/rigor/type_of_memo/stats.rb +43 -0
  209. data/lib/rigor/type_of_memo.rb +337 -0
  210. data/lib/rigor/typing_state.rb +1094 -0
  211. data/lib/rigor/version.rb +1 -1
  212. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
  213. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
  214. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  215. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  216. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  217. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  218. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  219. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
  220. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
  221. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  222. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
  223. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  224. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
  225. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
  226. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
  227. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  228. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  229. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
  230. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  231. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
  232. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
  233. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
  234. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
  235. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
  236. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  237. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  238. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
  239. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  240. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
  241. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  242. data/sig/rigor/analysis/fact_store.rbs +2 -3
  243. data/sig/rigor/cache/descriptor.rbs +14 -0
  244. data/sig/rigor/environment.rbs +4 -1
  245. data/sig/rigor/plugin/base.rbs +53 -19
  246. data/sig/rigor/plugin/interfaces.rbs +51 -0
  247. data/sig/rigor/plugin/io_boundary.rbs +2 -3
  248. data/sig/rigor/plugin/node_context.rbs +21 -0
  249. data/sig/rigor/reflection.rbs +37 -0
  250. data/sig/rigor/scope.rbs +22 -3
  251. data/sig/rigor/trinary.rbs +1 -0
  252. data/sig/rigor/type.rbs +9 -3
  253. data/sig/rigor.rbs +3 -2
  254. data/skills/rigor-ask/SKILL.md +1 -0
  255. data/skills/rigor-project-init/references/01-detect.md +1 -0
  256. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
  257. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  258. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  259. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  260. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  261. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  262. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  263. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  264. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  265. data/skills/rigor-type-oracle/SKILL.md +26 -1
  266. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  267. metadata +48 -2
  268. data/lib/rigor/inference/memo_audit.rb +0 -55
@@ -65,6 +65,14 @@ module Rigor
65
65
  manifest(
66
66
  id: "activerecord",
67
67
  target_gems: ["activerecord"],
68
+ # 0.16.0, 2026-11-13 (#2956) — the `id` column takes the primary key's type (`id: :uuid` reads `String`)
69
+ # instead of always `Integer`, and `serial` / `bigserial` columns read `Integer`. The schema table
70
+ # payload changes, so the bump follows the 0.15.0 entry.
71
+ #
72
+ # 0.15.0, 2026-11-07 (#2945) — `create_table`'s `primary_key: "name"` registers that column, so a query
73
+ # on it, or on an `alias_attribute` targeting it, no longer reports `unknown-column`. The schema table
74
+ # payload changes, and the bump (part of the producer cache KEY) stops a warm run serving the old one.
75
+ #
68
76
  # 0.14.0, 2026-10-10 (#1721) — the plugin publishes `:reachability_roots`: the models ActiveRecord loads
69
77
  # by name from an association. The association rows gain `macro`, `class_name_option`, `through` and
70
78
  # `source_type_option`, so a cached 0.13.0 payload lacks the fields the roots are derived from and
@@ -124,7 +132,7 @@ module Rigor
124
132
  # a scope lambda body / class-method body now contributes `Relation[Model]` via `scope.self_type`
125
133
  # instead of falling through to `Kernel#select` (the IO multiplexer, `Array[String]` return). Plus
126
134
  # `:select` added to the relation-entry-point list.
127
- version: "0.14.0",
135
+ version: "0.16.0",
128
136
  description: "Types ActiveRecord finders against the project's db/schema.rb and AR models.",
129
137
  config_schema: {
130
138
  "schema_file" => { kind: :string, default: "db/schema.rb" },
@@ -491,8 +499,8 @@ module Rigor
491
499
  # The project's OWN definition wins, and is not merely a precision preference: `def name` on the
492
500
  # model — or on an ancestor it declares — is an override Ruby dispatches to, and the plugin tier sits
493
501
  # ABOVE the engine's body-inference tiers, so answering here would displace the override's real return
494
- # type with `Dynamic`. `discovered_method_through_ancestors?` is the same table the veto's own
495
- # discovery arm reads, and it fails toward declining (a budget-exhausted walk answers true).
502
+ # type with `Dynamic`. `Reflection.project_method_defined(source: :discovered)` is the same table the veto's own
503
+ # discovery arm reads, and it fails toward declining (a budget-exhausted walk answers `maybe`).
496
504
  def implicit_self_instance_member_type(call_node, scope, index)
497
505
  return nil if scope.nil?
498
506
  return nil unless call_node.arguments.nil?
@@ -504,7 +512,9 @@ module Rigor
504
512
  entry = index.find(self_type.class_name)
505
513
  return nil if entry.nil?
506
514
  return nil unless instance_member_name?(entry, call_node.name)
507
- return nil if scope.discovered_method_through_ancestors?(self_type.class_name, call_node.name, :instance)
515
+ return nil unless Rigor::Reflection.project_method_defined(
516
+ self_type.class_name, call_node.name, kind: :instance, source: :discovered, scope: scope
517
+ ).no?
508
518
  return nil if rbs_answers_before_object?(self_type.class_name, call_node.name, scope)
509
519
 
510
520
  Rigor::Type::Combinator.untyped
@@ -552,6 +562,18 @@ module Rigor
552
562
  !entry.column(column_name).nil?
553
563
  end
554
564
 
565
+ # A project `self.find` on the model or an ancestor, or a walk that ran out of budget before ruling one out
566
+ # (`maybe`): the veto fails toward declining, as the discovery walk always has.
567
+ def project_find_defined?(entry, scope)
568
+ project_singleton_defined?(entry, :find, scope)
569
+ end
570
+
571
+ # The same veto for any class-side selector the plugin answers in Rails' name (`find`, `select`).
572
+ def project_singleton_defined?(entry, name, scope)
573
+ !Rigor::Reflection.project_method_defined(entry.class_name, name, kind: :singleton, source: :discovered,
574
+ scope: scope).no?
575
+ end
576
+
555
577
  # Class-side finders + the class-side relation entry points. `find` returns the model, or an Array of
556
578
  # them for two or more ids, or with a block the model or `nil`; `find_by!` returns the model;
557
579
  # `find_by` adds the `nil` arm; `where` / `all` / `order` / `limit` / `none` open a relation. The
@@ -572,7 +594,7 @@ module Rigor
572
594
 
573
595
  # A model that defines its own `self.find` (`find(owner, id)`) takes two arguments for its own
574
596
  # reasons, so the Array reading of Rails' `find` is withheld and the project's method answers.
575
- return nil if scope.discovered_method_through_ancestors?(entry.class_name, :find, :singleton)
597
+ return nil if project_find_defined?(entry, scope)
576
598
 
577
599
  Rigor::Type::Combinator.nominal_of("Array", type_args: [model])
578
600
  when :find_by!
@@ -584,13 +606,10 @@ module Rigor
584
606
  Rigor::Type::Combinator.nominal_of(entry.class_name),
585
607
  Rigor::Type::Combinator.constant_of(nil)
586
608
  )
587
- when :where, :all, :order, :limit, :none, :select
588
- # `:select` was added to close Mastodon's
589
- # `scope :duplicate_uris, -> { select(:uri).group(:uri).having(...) }` shape: the implicit-self
590
- # `select(:uri)` inside the scope lambda body had been resolving to `Kernel#select` (IO
591
- # multiplexer, return `Array[String]`), masking the AR class-side relation entry point. The rest
592
- # of the query DSL chains through the bundled `ActiveRecord::Relation` RBS once a relation is
593
- # open.
609
+ when :select then class_select_return_type(call_node, entry, scope)
610
+ when :where, :all, :order, :limit, :none
611
+ # The rest of the query DSL chains through the bundled `ActiveRecord::Relation` RBS once a
612
+ # relation is open.
594
613
  relation_of(entry.class_name)
595
614
  when :table_name
596
615
  table_name_return_type(entry)
@@ -601,6 +620,23 @@ module Rigor
601
620
  end
602
621
  end
603
622
 
623
+ # `:select` was added to close Mastodon's `scope :duplicate_uris, -> { select(:uri).group(:uri).having(...) }`
624
+ # shape: the implicit-self `select(:uri)` inside the scope lambda body had been resolving to `Kernel#select`
625
+ # (IO multiplexer, return `Array[String]`), masking the AR class-side relation entry point.
626
+ #
627
+ # `Model.select` is `all.select`, so a block makes it `Enumerable#select` over the loaded records, as the
628
+ # bundled `Relation#select` overload says (#1408), and the answer is an Array of the model. Fields next to a
629
+ # block answer the Array too, for the reason given on that overload. A model that defines its own
630
+ # `self.select` keeps its own answer for the block forms, as it does for `find`'s block and multi-id
631
+ # forms (`project_find_defined?`); the builder form stays the relation, as `find`'s one-id form stays the
632
+ # model.
633
+ def class_select_return_type(call_node, entry, scope)
634
+ return relation_of(entry.class_name) if call_node.block.nil?
635
+ return nil if project_singleton_defined?(entry, :select, scope)
636
+
637
+ Rigor::Type::Combinator.nominal_of("Array", type_args: [Rigor::Type::Combinator.nominal_of(entry.class_name)])
638
+ end
639
+
604
640
  # `Model.find { |m| … }` / `Model.find(&:pred?)` never reaches the id lookup: `Core.find` passes a
605
641
  # block to `super` (`all.find`), whose `Relation#find` does `return super if block_given?`, landing on
606
642
  # `Enumerable#find` over the loaded records. So the answer is the element or `nil`, exactly what the
@@ -615,7 +651,7 @@ module Rigor
615
651
  # `Relation#find` overload does. A project `self.find` keeps its own answer, as it does for the
616
652
  # multi-id form.
617
653
  def block_find_return_type(call_node, entry, scope)
618
- return nil if scope.discovered_method_through_ancestors?(entry.class_name, :find, :singleton)
654
+ return nil if project_find_defined?(entry, scope)
619
655
  return Rigor::Type::Combinator.untyped unless call_argument_count(call_node).zero?
620
656
 
621
657
  Rigor::Type::Combinator.union(
@@ -231,12 +231,26 @@ module ActiveRecord
231
231
  def left_outer_joins: (*untyped) -> self
232
232
  %a{pure}
233
233
  def references: (*untyped) -> self
234
- # With a block, `select` is Enumerable's and returns an Array. It
235
- # stays `self`: on this open receiver a method the file does not
236
- # declare is not reported, while on `Array[Elem]` any ActiveSupport
237
- # method the bundled overlay leaves out would be.
234
+ # With a block, `select` is `Enumerable#select` over the loaded
235
+ # records and returns an Array (`return super()` when a block is
236
+ # given, `relation/query_methods.rb:413` in activerecord 8.1.3.1).
237
+ # Fields and a block together raise `ArgumentError` there; Rails 4
238
+ # ignored the fields and returned the Array, so that form answers
239
+ # the Array too, which holds on either version. Without a block it
240
+ # is the query builder.
241
+ #
242
+ # `Array[Elem]` is a closed receiver, unlike this one: an
243
+ # ActiveSupport Array method on the result (`pluck`, `second`,
244
+ # `to_sentence`, `blank?`) resolves only with the
245
+ # `rigor-activesupport-core-ext` plugin enabled, and reports
246
+ # `call.undefined-method` without it, as it does after `to_a`.
247
+ #
248
+ # The `%a{pure}` is the builder form's. The block form's
249
+ # `io.db.read` comes from the plugin's `RELATION_MATERIALIZERS` row,
250
+ # which labels both forms today (#1318).
238
251
  %a{pure}
239
- def select: (*untyped) -> self
252
+ def select: (*untyped) { (Elem) -> boolish } -> Array[Elem]
253
+ | (*untyped) -> self
240
254
  %a{pure}
241
255
  def reselect: (*untyped) -> self
242
256
  %a{pure}
@@ -89,13 +89,15 @@ module Rigor
89
89
  # decline — they take side-effecting argument types that the RBS surface already covers.
90
90
  dynamic_return receivers: -> { attachment_index&.class_names || [] } do |call_node, scope|
91
91
  next nil unless call_node.is_a?(Prism::CallNode)
92
- next nil if call_node.receiver.nil?
92
+
93
+ receiver = call_node.receiver
94
+ next nil if receiver.nil?
93
95
  next nil unless call_node.arguments.nil?
94
96
 
95
97
  index = attachment_index
96
98
  next nil if index.nil? || index.empty?
97
99
 
98
- receiver_type = scope.type_of(call_node.receiver)
100
+ receiver_type = scope.type_of(receiver)
99
101
  next nil unless receiver_type.is_a?(Rigor::Type::Nominal)
100
102
 
101
103
  # `AttachmentIndex#attachments_for` de-roots the query itself (#621) — no retry arm here.
@@ -132,10 +132,12 @@ module Rigor
132
132
 
133
133
  dynamic_return methods: DURATION_MULTIPLIERS do |call_node, scope|
134
134
  next nil unless call_node.is_a?(Prism::CallNode)
135
- next nil unless call_node.receiver # `day` with no receiver is somebody's own method
135
+
136
+ receiver = call_node.receiver
137
+ next nil unless receiver # `day` with no receiver is somebody's own method
136
138
  next nil unless call_node.arguments.nil?
137
139
  next nil unless call_node.block.nil?
138
- next nil unless numeric_receiver?(scope&.type_of(call_node.receiver))
140
+ next nil unless numeric_receiver?(scope&.type_of(receiver))
139
141
 
140
142
  Rigor::Type::Combinator.nominal_of(DURATION_NOMINAL)
141
143
  end
@@ -200,7 +202,11 @@ module Rigor
200
202
  argument_kind = duration_operand_kind(scope&.type_of(argument))
201
203
  next nil if argument_kind.nil?
202
204
 
203
- receiver_kind = duration_operand_kind(scope&.type_of(call_node.receiver))
205
+ receiver = call_node.receiver
206
+ # Prism gives every parsed `+` / `-` / `*` call a receiver; the guard is for the RBS `Node?` type.
207
+ next nil if receiver.nil?
208
+
209
+ receiver_kind = duration_operand_kind(scope&.type_of(receiver))
204
210
  next nil if receiver_kind.nil?
205
211
 
206
212
  duration_arithmetic_result(call_node.name, receiver_kind, argument_kind)
@@ -210,11 +216,13 @@ module Rigor
210
216
  # member; see {SymbolizeKeys}. The RBS rows answer `Hash[untyped, V]` wherever this declines.
211
217
  dynamic_return methods: %i[symbolize_keys to_options] do |call_node, scope|
212
218
  next nil unless call_node.is_a?(Prism::CallNode)
213
- next nil unless call_node.receiver
219
+
220
+ receiver = call_node.receiver
221
+ next nil unless receiver
214
222
  next nil unless call_node.arguments.nil?
215
223
 
216
224
  # A block is ignored by `symbolize_keys`, so it does not decline.
217
- SymbolizeKeys.result(scope&.type_of(call_node.receiver), scope)
225
+ SymbolizeKeys.result(scope&.type_of(receiver), scope)
218
226
  end
219
227
 
220
228
  # ADR-88 WD1 — both tables are static and the RBS bundle ships with the gem; nothing here reads a
@@ -604,6 +604,35 @@ class Float
604
604
  def gigabytes: () -> Float
605
605
  end
606
606
 
607
+ # ---------------------------------------------------------------
608
+ # NumericWithFormat — ActiveSupport's `core_ext/numeric/conversions`, included into Integer, Float and
609
+ # BigDecimal. Declared on the module and included, not as a `def` on each class, so a project `sig/` that
610
+ # declares `Integer#to_fs` itself overrides it instead of raising `DuplicatedMethodDefinitionError` (the
611
+ # `process.rbs` shape). Not pure: the Symbol formats read `I18n` for their locale. An Integer or String
612
+ # format is `to_s(format)`, and no format is `to_s`, so every form answers a String. `options` is any
613
+ # Hash-like value: ActiveSupport calls `symbolize_keys` on it, so String keys and a HashWithIndifferentAccess
614
+ # are both valid.
615
+ # ---------------------------------------------------------------
616
+
617
+ module ActiveSupport
618
+ module NumericWithFormat
619
+ def to_fs: (?(Symbol | Integer | String | nil) format, ?Hash[untyped, untyped]? options) -> String
620
+ def to_formatted_s: (?(Symbol | Integer | String | nil) format, ?Hash[untyped, untyped]? options) -> String
621
+ end
622
+ end
623
+
624
+ class Integer
625
+ include ActiveSupport::NumericWithFormat
626
+ end
627
+
628
+ class Float
629
+ include ActiveSupport::NumericWithFormat
630
+ end
631
+
632
+ class BigDecimal
633
+ include ActiveSupport::NumericWithFormat
634
+ end
635
+
607
636
  # ---------------------------------------------------------------
608
637
  # ActiveSupport::Duration — issue #632
609
638
  # ---------------------------------------------------------------
@@ -2,24 +2,22 @@
2
2
 
3
3
  require "prism"
4
4
 
5
- require "rigor/inference/declaration_walk"
5
+ require "rigor/reflection"
6
6
 
7
7
  require_relative "resource_index"
8
8
 
9
9
  module Rigor
10
10
  module Plugin
11
11
  class Alba < Rigor::Plugin::Base
12
- # A {Rigor::Inference::DeclarationWalk} collector over one file: the classes it declares, whether each
13
- # `include Alba::Resource`s, and the association declarations whose resource
14
- # class alba infers from the name.
12
+ # A collector over one file, built on {Rigor::Reflection.class_declarations_in}: the classes it declares, whether
13
+ # each `include Alba::Resource`s, and the association declarations whose resource class alba infers from
14
+ # the name.
15
15
  #
16
16
  # An association is recorded only when alba would take the inference path (`Alba::Association#
17
17
  # resource_from`): a literal Symbol/String name, exactly one positional argument, no block, and no
18
18
  # `resource:` / `serializer:` keyword. A keyword splat (`**opts`) might carry either, so it is not
19
19
  # recorded either — a skipped association costs a root, never invents one.
20
20
  class ResourceCollector
21
- include Rigor::Inference::DeclarationWalk::Collector
22
-
23
21
  ASSOCIATION_METHODS = %i[association one many has_one has_many].freeze
24
22
  RESOURCE_MODULE = "Alba::Resource"
25
23
  RESOURCE_KEYWORDS = %i[resource serializer].freeze
@@ -40,22 +38,13 @@ module Rigor
40
38
  @associations = []
41
39
  end
42
40
 
43
- def on_declaration(node, context, body_context)
44
- return Rigor::Inference::DeclarationWalk::DESCEND unless node.is_a?(Prism::ClassNode)
45
-
46
- name = body_context.prefix.join("::")
47
- unless name.empty?
48
- @entries[name] = superclass_name(node.superclass)
49
- @nestings[name] = context.nesting || []
41
+ # Walks `root` (a parsed file's `Prism::Node`) and records what it finds; answers the collector itself.
42
+ def collect(root)
43
+ Rigor::Reflection.class_declarations_in(root) { |call| on_call(call.node, call.owner) }.each do |declaration|
44
+ @entries[declaration.name] = superclass_name(declaration.node.superclass)
45
+ @nestings[declaration.name] = declaration.nesting
50
46
  end
51
- Rigor::Inference::DeclarationWalk::DESCEND
52
- end
53
-
54
- def on_call(node, context)
55
- owner = context.prefix.join("::")
56
- record_call(node, owner) unless owner.empty? || !node.receiver.nil?
57
- remember_block(node)
58
- Rigor::Inference::DeclarationWalk::DESCEND
47
+ self
59
48
  end
60
49
 
61
50
  # The classes this file declares, as {ResourceIndex::ClassEntry} values.
@@ -70,6 +59,11 @@ module Rigor
70
59
 
71
60
  private
72
61
 
62
+ def on_call(node, owner)
63
+ record_call(node, owner) unless owner.nil? || !node.receiver.nil?
64
+ remember_block(node)
65
+ end
66
+
73
67
  def record_call(node, owner)
74
68
  if node.name == :include
75
69
  @includes[owner] = true if includes_resource?(node)
@@ -2,8 +2,6 @@
2
2
 
3
3
  require "prism"
4
4
 
5
- require "rigor/inference/declaration_walk"
6
-
7
5
  require_relative "resource_collector"
8
6
  require_relative "resource_index"
9
7
 
@@ -28,10 +26,7 @@ module Rigor
28
26
  parsed = Prism.parse(contents)
29
27
  next unless parsed.success?
30
28
 
31
- collector = ResourceCollector.new
32
- Rigor::Inference::DeclarationWalk.run(
33
- parsed.value, [collector], Rigor::Inference::DeclarationWalk::Context.root(nesting: [])
34
- )
29
+ collector = ResourceCollector.new.collect(parsed.value)
35
30
  entries.concat(collector.class_entries)
36
31
  associations.concat(collector.associations)
37
32
  end
@@ -47,7 +47,8 @@ module Rigor
47
47
  catalog = producer_value(:ffi_catalog)
48
48
  next nil if catalog.nil?
49
49
 
50
- receiver_type = call_node.receiver ? scope&.type_of(call_node.receiver) : scope&.self_type
50
+ receiver = call_node.receiver
51
+ receiver_type = receiver ? scope&.type_of(receiver) : scope&.self_type
51
52
  receiver_class = case receiver_type
52
53
  when Rigor::Type::Nominal, Rigor::Type::Singleton then receiver_type.class_name
53
54
  end
@@ -77,7 +78,8 @@ module Rigor
77
78
  catalog = producer_value(:ffi_catalog)
78
79
  next nil if catalog.nil?
79
80
 
80
- receiver_type = call_node.receiver ? scope&.type_of(call_node.receiver) : scope&.self_type
81
+ receiver = call_node.receiver
82
+ receiver_type = receiver ? scope&.type_of(receiver) : scope&.self_type
81
83
  receiver_class = case receiver_type
82
84
  when Rigor::Type::Nominal, Rigor::Type::Singleton then receiver_type.class_name
83
85
  end
@@ -77,8 +77,18 @@ module Rigor
77
77
  signature_paths: ["sig"],
78
78
  # ADR-43 WD4 — the classes the bundled sig covers completely enough that a Ruby-source subclass
79
79
  # (`class PostType < GraphQL::Schema::Object`) may bridge inherited calls to the RBS ancestor.
80
- # Listing one turns on `call.undefined-method`/arity checks for inherited DSL calls on every
81
- # subclass — justified here because the sig IS the authority (graphql-ruby ships none).
80
+ # The bridge is a signature lookup only: an inherited DSL call resolves to the declaration's
81
+ # return type and block parameters, and a subclass `def` overriding a declared method other
82
+ # than `initialize` binds its parameters from it (#2681). The subclass itself stays outside
83
+ # RBS, so no signature-reading call rule fires on a call it makes, declared or not:
84
+ # `call.undefined-method`, `call.wrong-arity` and `call.argument-type-mismatch` all stay
85
+ # silent, while the same calls on a listed class AS THE DIRECT RECEIVER still report.
86
+ # `open_receivers:` is what covers those direct receivers; it is not what silences the
87
+ # subclass — the receiver gate is (ADR-43 Context § "Amendment 2026-11-15", which withdrew
88
+ # the claim that the rules follow the resolution). So listing a class here cannot cost a
89
+ # false positive on a subclass. The risk runs the other way: a declared member that does not exist at runtime makes a genuinely
90
+ # failing call resolve silently, so the sig must be accurate — it IS the authority
91
+ # (graphql-ruby ships none).
82
92
  rbs_complete_ancestors: %w[
83
93
  GraphQL::Schema
84
94
  GraphQL::Schema::Member
@@ -113,7 +113,13 @@ module Rigor
113
113
  # ADR-37 slice 2 — a pure return-type contributor. The engine gates on the receiver being a known
114
114
  # carrier class (so the block only runs on Result/Option receivers); the block then confirms the call
115
115
  # is an unwrap method on an instantiated carrier and yields the carried `type_args[0]`.
116
+ # #2717 — the rule has no `methods:` gate, so the name gate does not screen it: the engine also offers it
117
+ # the compound-write nodes (`x.attr op= v`, `x[i] ||= v`, ...) and the `&:symbol` block's
118
+ # `Prism::BlockArgumentNode`. None of those has `name`, which the rule reads after `receiver`, so decline
119
+ # anything that is not a `CallNode` rather than raise into the rescue.
116
120
  dynamic_return receivers: (RESULT_CARRIERS + OPTION_CARRIERS) do |call_node, scope|
121
+ next nil unless call_node.is_a?(Prism::CallNode)
122
+
117
123
  receiver = call_node.receiver
118
124
  next nil if receiver.nil?
119
125
 
@@ -65,7 +65,7 @@ module Rigor
65
65
  # implicit `*_path` / `*_url` helper call, is shadowed by a same-file binding, or is in a
66
66
  # suppressed directory. ADR-37: the engine owns the walk; the same-file `shadowing` set is built
67
67
  # once as the node-rule file context.
68
- def violations_for(call_node:, helper_table:, shadowing:, path:)
68
+ def violations_for(call_node:, helper_table:, shadowing:, path:, scope: nil, context: nil)
69
69
  return [] unless call_node.is_a?(Prism::CallNode) && implicit_helper_call?(call_node)
70
70
 
71
71
  name = call_node.name.to_s
@@ -88,11 +88,209 @@ module Rigor
88
88
  # Recognised custom / dynamic helper (no arity to check), or a directory where helpers aren't
89
89
  # customarily resolved.
90
90
  []
91
+ elsif self_resolves_method?(name, scope, context)
92
+ []
91
93
  else
92
94
  [unknown_helper_violation(name, helper_table)]
93
95
  end
94
96
  end
95
97
 
98
+ # True when `name` is already a method on the call's `self`, asked of the engine rather than a
99
+ # plugin-side name list: the project's discovered methods (`def`, `attr_*`, the singleton side) through
100
+ # the project's own ancestry, then the RBS ancestry. A truncated walk (`maybe`) declines too — a definer
101
+ # past the budget cannot be ruled out, and a false positive outranks a missed typo.
102
+ #
103
+ # `self` is read from the node's lexical ancestors (`NodeContext#ancestors`), because the scope a
104
+ # `node_rule` receives is the file's, not the call site's. {#call_site} names the enclosing class or
105
+ # module and the kinds `self` may be; where the kind is not decidable (a block in a class body, a
106
+ # `def obj.m`) both are asked, so a definer on either side declines and a typo on neither still reports.
107
+ def self_resolves_method?(name, scope, context)
108
+ return false if scope.nil? || context.nil?
109
+
110
+ site = call_site(context.ancestors)
111
+ return false if site.nil?
112
+ # An owner no constant spelling names (`class foo::Bar`) cannot be asked about.
113
+ return true if site.names.empty?
114
+ # A module is mixed into classes that cannot be seen from where it is defined (a controller concern).
115
+ return true if site.module_owner && FRAMEWORK_METHODS.key?(name)
116
+
117
+ site.names.product(site.kinds).any? { |class_name, kind| resolves_on?(class_name, name, kind, scope) }
118
+ end
119
+
120
+ def resolves_on?(class_name, name, kind, scope)
121
+ return true unless Rigor::Reflection.project_method_defined(class_name, name, kind: kind, source: :either,
122
+ scope: scope).no?
123
+ return true unless Rigor::Reflection.rbs_method_defined(class_name, name, kind: kind, scope: scope).no?
124
+
125
+ bases = FRAMEWORK_METHODS[name]
126
+ !bases.nil? && !Rigor::Reflection.ancestry_reaches(class_name, bases, kind: kind, scope: scope).no?
127
+ end
128
+
129
+ # Rails' own `*_path` / `*_url` methods on a controller or mailer, read from the Rails source (rails/rails
130
+ # main, 1105756f42), each with the framework classes a project class names directly as its superclass. A
131
+ # project without that framework's RBS cannot see these through the ancestry, so a class whose ancestry
132
+ # reaches one of the listed bases declines for that name; a module declines for every name, being mixed
133
+ # into classes that cannot be seen.
134
+ #
135
+ # - `controller_path`: AbstractController::Base, actionpack/lib/abstract_controller/base.rb:157 (instance),
136
+ # :117 (class); every framework base below inherits it (metal.rb:185, base.rb:206, api.rb:91,
137
+ # actionmailer/lib/action_mailer/base.rb:479).
138
+ # - `append_view_path` / `prepend_view_path`: `ActionView::ViewPaths` (actionview/lib/action_view/
139
+ # view_paths.rb:113, :124 instance, :46, :57 class), which `AbstractController::Rendering` includes
140
+ # (abstract_controller/rendering.rb:18); `ActionController::Base` (base.rb:270), `::API` (api.rb:115) and
141
+ # `ActionMailer::Base` (action_mailer/base.rb:489) include that.
142
+ # - `helpers_path`: a class attribute of `ActionController::Helpers` (metal/helpers.rb:68), included by
143
+ # `ActionController::Base` (base.rb:273).
144
+ # - `edit_` / `new_` `polymorphic_path` / `polymorphic_url`: actionpack/lib/action_dispatch/routing/
145
+ # polymorphic_routes.rb:163-166, through `UrlFor` in `ActionController::Base` (base.rb:274) and `::API`
146
+ # (api.rb:117), and in `ActionMailer::Base`, which actionmailer/lib/action_mailer/railtie.rb:40
147
+ # includes `AbstractController::UrlFor` into, whose `ActionDispatch::Routing::UrlFor` includes
148
+ # `PolymorphicRoutes` (action_dispatch/routing/url_for.rb:92). (`polymorphic_path` / `polymorphic_url` are in {BUILTIN_PASSTHROUGH}.)
149
+ FRAMEWORK_CONTROLLERS = %w[
150
+ AbstractController::Base ActionController::Metal ActionController::Base ActionController::API
151
+ ActionMailer::Base
152
+ ].freeze
153
+ VIEW_PATH_BASES = %w[ActionController::Base ActionController::API ActionMailer::Base].freeze
154
+ URL_FOR_BASES = %w[ActionController::Base ActionController::API ActionMailer::Base].freeze
155
+ FRAMEWORK_METHODS = {
156
+ "controller_path" => FRAMEWORK_CONTROLLERS,
157
+ "append_view_path" => VIEW_PATH_BASES,
158
+ "prepend_view_path" => VIEW_PATH_BASES,
159
+ "helpers_path" => %w[ActionController::Base].freeze,
160
+ "edit_polymorphic_path" => URL_FOR_BASES,
161
+ "edit_polymorphic_url" => URL_FOR_BASES,
162
+ "new_polymorphic_path" => URL_FOR_BASES,
163
+ "new_polymorphic_url" => URL_FOR_BASES
164
+ }.freeze
165
+
166
+ # What a call site's lexical position says about `self`: the names the enclosing class or module can be
167
+ # known by, whether the owner is a module, and the kinds (`:instance`, `:singleton`) `self` may be.
168
+ CallSite = Struct.new(:names, :module_owner, :kinds, keyword_init: true)
169
+
170
+ # The {CallSite} of a node under `ancestors` (outermost first), or nil when it is in no class or module,
171
+ # which leaves the route-table verdict as it was.
172
+ def call_site(ancestors)
173
+ owner_index = ancestors.rindex { |n| n.is_a?(Prism::ClassNode) || n.is_a?(Prism::ModuleNode) }
174
+ member_site = member_class_site(ancestors, owner_index)
175
+ return member_site if member_site
176
+ return nil if owner_index.nil?
177
+
178
+ CallSite.new(names: owner_names(ancestors, owner_index),
179
+ module_owner: ancestors[owner_index].is_a?(Prism::ModuleNode),
180
+ kinds: self_kinds(ancestors[(owner_index + 1)..]))
181
+ end
182
+
183
+ BOTH_KINDS = %i[instance singleton].freeze
184
+ MEMBER_CLASS_CONSTRUCTORS = { Struct: :new, Data: :define }.freeze
185
+
186
+ # The call site of a node inside the block of `Struct.new(...) { }` / `Data.define(...) { }`, which is the
187
+ # body of a class the constructor creates: `self` is that class, not the lexical `class` around it (or
188
+ # none). A plain constant write (`Point = …`, `Outer::Point = …`, nested under the lexical owner) names it,
189
+ # and the engine knows its members. Everything else declines (`names` empty): an unassigned block, `P ||= …`,
190
+ # `… end.freeze`, a dynamic path. nil when the node is not in such a block below its lexical owner.
191
+ def member_class_site(ancestors, owner_index)
192
+ index = ancestors.rindex { |n| member_class_call?(n) && ancestors[ancestors.index(n) + 1].equal?(n.block) }
193
+ return nil if index.nil? || (owner_index && index < owner_index)
194
+
195
+ kinds = self_kinds(ancestors[(index + 1)..])
196
+ names = index.positive? ? written_class_names(ancestors, owner_index, ancestors[index - 1], ancestors[index]) : []
197
+ names = struct_class_names(ancestors[index]) if names.empty?
198
+ CallSite.new(names: names, module_owner: false, kinds: kinds)
199
+ end
200
+
201
+ # Issue #2968 — `Struct.new("Named", :a) { }` names its class `Struct::Named` without a constant write.
202
+ def struct_class_names(call)
203
+ first = call.arguments&.arguments&.first
204
+ return [] unless call.name == :new && first.is_a?(Prism::StringNode)
205
+
206
+ name = first.unescaped
207
+ name.match?(/\A[A-Z][A-Za-z0-9_]*\z/) ? ["Struct::#{name}"] : []
208
+ end
209
+
210
+ def written_class_names(ancestors, owner_index, written, call)
211
+ local =
212
+ case written
213
+ when Prism::ConstantWriteNode then written.value.equal?(call) ? written.name.to_s : nil
214
+ when Prism::ConstantPathWriteNode then written.value.equal?(call) ? declared_name(written.target) : nil
215
+ end
216
+ return [] if local.nil?
217
+ return [local.delete_prefix("::")] if local.start_with?("::")
218
+
219
+ prefixes = owner_index ? owner_names(ancestors, owner_index) : [""]
220
+ nested = prefixes.map { |prefix| prefix.empty? ? local : "#{prefix}::#{local}" }
221
+ # `Outer::R = …` resolves `Outer` lexically or at the root, as a compact `class Foo::Bar` does.
222
+ written.is_a?(Prism::ConstantPathWriteNode) ? (nested + [local]).uniq : nested
223
+ end
224
+
225
+ def member_class_call?(node)
226
+ return false unless node.is_a?(Prism::CallNode) && node.block.is_a?(Prism::BlockNode)
227
+
228
+ receiver = node.receiver
229
+ receiver_name =
230
+ case receiver
231
+ when Prism::ConstantReadNode then receiver.name
232
+ when Prism::ConstantPathNode then receiver.parent.nil? ? receiver.name : nil
233
+ end
234
+ !receiver_name.nil? && MEMBER_CLASS_CONSTRUCTORS[receiver_name] == node.name
235
+ end
236
+
237
+ # The kinds `self` may be for a node under the nodes between its owner and itself. The nearest `def`
238
+ # decides (blocks inside a method keep its `self`); with no `def`, a bare class body is the class object
239
+ # itself, while a block or lambda there may run on another object (`before_action { }`, `included do`,
240
+ # `define_method`, `Class.new`), so both kinds are asked.
241
+ def self_kinds(inner)
242
+ def_index = inner.rindex { |n| n.is_a?(Prism::DefNode) }
243
+ if def_index
244
+ node = inner[def_index]
245
+ return BOTH_KINDS if node.receiver && !node.receiver.is_a?(Prism::SelfNode)
246
+ return [:singleton] if node.receiver
247
+
248
+ return inner[0...def_index].any?(Prism::SingletonClassNode) ? [:singleton] : [:instance]
249
+ end
250
+ return BOTH_KINDS if inner.any? { |n| block_scope?(n) }
251
+ if inner.any? { |n| n.is_a?(Prism::SingletonClassNode) && !n.expression.is_a?(Prism::SelfNode) }
252
+ return BOTH_KINDS
253
+ end
254
+
255
+ [:singleton]
256
+ end
257
+
258
+ def block_scope?(node)
259
+ node.is_a?(Prism::BlockNode) || node.is_a?(Prism::LambdaNode)
260
+ end
261
+
262
+ # The names the enclosing class or module can be known by, `[]` when a declaration's name is dynamic. A
263
+ # bare `class Foo` is exactly `Enclosing::Foo`. A compact `class Foo::Bar` may resolve `Foo` lexically or
264
+ # at the root, so both spellings are tried. A rooted declaration (`class ::Foo`) names the root constant
265
+ # alone.
266
+ def owner_names(ancestors, owner_index)
267
+ chain = ancestors[0..owner_index].select { |n| n.is_a?(Prism::ClassNode) || n.is_a?(Prism::ModuleNode) }
268
+ names = [""]
269
+ chain.each do |decl|
270
+ local = declared_name(decl.constant_path)
271
+ return [] if local.nil?
272
+
273
+ names = if local.start_with?("::")
274
+ [local.delete_prefix("::")]
275
+ else
276
+ nested = names.map { |prefix| prefix.empty? ? local : "#{prefix}::#{local}" }
277
+ decl.constant_path.is_a?(Prism::ConstantPathNode) ? (nested + [local]).uniq : nested
278
+ end
279
+ end
280
+ names
281
+ end
282
+
283
+ def declared_name(node)
284
+ case node
285
+ when Prism::ConstantReadNode then node.name.to_s
286
+ when Prism::ConstantPathNode
287
+ return "::#{node.name}" if node.parent.nil?
288
+
289
+ parent = declared_name(node.parent)
290
+ parent && "#{parent}::#{node.name}"
291
+ end
292
+ end
293
+
96
294
  # Walks the AST once and returns the Set of names that shadow a route helper for this file. Includes:
97
295
  #
98
296
  # - `def name` declarations (any level — the per-method-scope visibility model Ruby uses means a
@@ -205,11 +205,12 @@ module Rigor
205
205
  Analyzer.collect_shadowing_names(root)
206
206
  end
207
207
 
208
- node_rule Prism::CallNode do |node, _scope, path, shadowing|
208
+ node_rule Prism::CallNode do |node, scope, path, shadowing, context|
209
209
  table = helper_table_or_nil
210
210
  next [] if table.nil? || table.empty?
211
211
 
212
- Analyzer.violations_for(call_node: node, helper_table: table, shadowing: shadowing, path: path).map do |violation|
212
+ Analyzer.violations_for(call_node: node, helper_table: table, shadowing: shadowing, path: path,
213
+ scope: scope, context: context).map do |violation|
213
214
  diagnostic(node, path: path, message: violation.message, severity: violation.severity, rule: violation.rule)
214
215
  end
215
216
  end