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
@@ -209,7 +209,13 @@ module Rigor
209
209
  def table_digest(project_scan)
210
210
  payload = {
211
211
  "synthetic" => project_scan.synthetic_method_index.to_h,
212
- "patched" => patched_rows(project_scan.project_patched_methods)
212
+ "patched" => patched_rows(project_scan.project_patched_methods),
213
+ "patched_mixins" => project_scan.project_patched_methods.mixins.map do |mixin|
214
+ mixin.to_h.values.map(&:to_s)
215
+ end,
216
+ "patched_overrides" => project_scan.project_patched_methods.overrides.map do |key, visibility|
217
+ [*key, visibility].map(&:to_s)
218
+ end
213
219
  }
214
220
  Digest::SHA256.hexdigest(JSON.generate(payload))
215
221
  rescue StandardError
@@ -221,7 +227,7 @@ module Rigor
221
227
  def patched_rows(patched)
222
228
  patched.by_key.map do |(class_name, method_name, kind), entry|
223
229
  [class_name.to_s, method_name.to_s, kind.to_s, entry.source_path.to_s, entry.source_line,
224
- entry.return_type&.describe(:short).to_s, entry.alias_of.to_s]
230
+ entry.return_type&.describe(:short).to_s, entry.alias_of.to_s, entry.visibility.to_s]
225
231
  end.sort
226
232
  end
227
233
 
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ # `Dir.glob(File.join(root, pattern))` with `root` read as a literal path (#1956, #1959).
5
+ #
6
+ # Joining a directory into the pattern makes `Dir.glob` read the directory's own characters as glob syntax:
7
+ # a project at `ds [x]` or `ds{a}` matches nothing, `ds*` matches its siblings, `fx[1]` matches `fx1`, and
8
+ # a directory literally named `{..,zz}` expands into the parent tree. Each glob metacharacter of `root` is
9
+ # backslash-escaped before the join, so `root` matches only itself while `pattern` keeps exactly the meaning the
10
+ # joined spelling gave it: a leading `/` collapses into the join (`/etc/*` reads `root/etc/*`), and so does one
11
+ # inside a brace alternative (`{/etc,lib}/*` reads `root/etc/*` and `root/lib/*`). `Dir.glob(pattern, base:)`
12
+ # would not do that: it reads an absolute pattern, or an absolute alternative, outside `base`. A root without
13
+ # metacharacters is joined unchanged, so the results are the joined glob's strings in its order, `root` kept as
14
+ # given (relative, symlinked or with a trailing `/`).
15
+ #
16
+ # `literal(name)` is the same escape for a single name (a file's basename, a configured path) that is joined into
17
+ # a pattern as a literal: `literal("a[1].rb")` matches `a[1].rb` and not `a1.rb`.
18
+ module RootedGlob
19
+ METACHARACTERS = /[\[\]{}*?\\]/
20
+
21
+ module_function
22
+
23
+ # @param root — a directory path (String or Pathname), taken literally
24
+ # @param pattern — a `Dir.glob` pattern relative to `root`
25
+ # @return the matches spelled under `root`, in `Dir.glob` order; empty when `root` is not a directory
26
+ def glob(root, pattern)
27
+ Dir.glob(File.join(literal(root), pattern.to_s))
28
+ end
29
+
30
+ # @param name — a file or directory name, taken literally
31
+ # @return the pattern that matches exactly `name` under `Dir.glob`, with its metacharacters escaped
32
+ def literal(name)
33
+ name.to_s.gsub(METACHARACTERS, "\\\\\\0")
34
+ end
35
+ end
36
+ end
@@ -231,6 +231,11 @@ module Rigor
231
231
  # (the meta-new walk does not route `Parent` through ancestor discovery), so a reader that must place `K` in a
232
232
  # hierarchy reads its superclass as unknown, as it reads the `nil` the table records for `class K < <expr>`.
233
233
  ENVELOPE_META_SUPERCLASS_MARK = :"<meta-superclass>"
234
+ # Issue #1979 — the kind of a `[ENVELOPE_CONSTRUCTED_KIND, constant_name]` bucket entry: `class K < <expr>` whose
235
+ # superclass expression is a core constructor call (`Data.define(...)`, `Struct.new(...)`, `Class.new(...)`)
236
+ # naming that constant, as the receiver or as `Class.new`'s single bare-constant argument (`Object` for none).
237
+ # Read only by the #1596 proof's subclass walk (`RefinedDispatch::OwnMethod.each_subclass`).
238
+ ENVELOPE_CONSTRUCTED_KIND = :constructed_from
234
239
  ENVELOPE_MODULE_MARK = :"<module>"
235
240
  ENVELOPE_DYNAMIC_MARK = :"<dynamic>"
236
241
  ENVELOPE_OBJECT_EXTENDED_MARK = :"<object-extended>"
@@ -84,6 +84,20 @@ module Rigor
84
84
  false
85
85
  end
86
86
 
87
+ # {.external_lacks?} on the side an entry stands for: an external superclass on a singleton chain
88
+ # (`:singleton`, its whole tail) is asked for a singleton method, an extended module (`:instance`) and every
89
+ # instance-side entry for an instance method.
90
+ def external_lacks_on?(scope, candidates, side, name)
91
+ return external_lacks?(scope, candidates, name) if side == :instance
92
+
93
+ known = candidates.find { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: scope) }
94
+ return false if known.nil?
95
+
96
+ Rigor::Reflection.singleton_method_definition(known, name, scope: scope).nil?
97
+ rescue StandardError
98
+ false
99
+ end
100
+
87
101
  # One verdict's working state: the scope, the resolver, and the edges the tests read.
88
102
  class Context
89
103
  attr_reader :mark, :edges
@@ -300,17 +300,24 @@ module Rigor
300
300
  # `Kernel` or `Enumerable` on any reopening. So each candidate name files the negative method edge of every
301
301
  # {Relevance::HOOKS} name on its singleton side (`method:Comparable.included`, the key a new `def
302
302
  # self.included` satisfies); a file adding any other method to the module re-checks nothing, and once the
303
- # module is declared its sites carry later edits and the removal. The edges are a function of the chain alone,
304
- # so a consumer files them once per chain ({Analysis::DependencyRecorder.file_chain_once}, #1590). A hook
305
- # written another way — an instance `def included` in a module extended onto the external, a
306
- # `define_singleton_method(:included)` — satisfies no such key and is not seen.
303
+ # module is declared its sites carry later edits and the removal. #1661 — each name also files `mixin:<name>`,
304
+ # which `IncrementalSession#mark_affected` fires when the module's unpositioned mixin edges move: a new file
305
+ # that taints it (`module Comparable; include(*MODS); end`) without a hook. The key is the qualified candidate,
306
+ # the spelling the chain resolves a project entry by. The unpositioned table may key the owner by a relative
307
+ # spelling (`Constants` inside `class File`), so the producer also fires every recorded `mixin:` key ending in
308
+ # `::<owner>` (`IncrementalSession#mixin_keys`); a taint on a project `Foo::Base` still re-checks no reader of
309
+ # an external `Lib::Base`. An ambiguous spelling has no candidate and files none: no project module can resolve
310
+ # it, so no taint can mark it. The edges are a function of the chain alone, so a consumer files them once per
311
+ # chain ({Analysis::DependencyRecorder.file_chain_once}, #1590). A hook written another way — an instance `def
312
+ # included` in a module extended onto the external, a `define_singleton_method(:included)` — satisfies no such
313
+ # key and is not seen.
307
314
  def record_hooks(scope)
308
315
  externals = (@memo[:externals] ||= @entries.select(&:external?).freeze)
309
316
  return if externals.empty?
310
317
 
311
318
  Analysis::DependencyRecorder.file_chain_once(self, :hooks, scope.discovery) do
312
319
  externals.each do |entry|
313
- entry.candidates.each { |name| Analysis::DependencyRecorder.read_keys(ResolutionChain.hook_keys(name)) }
320
+ entry.candidates.each { |name| Analysis::DependencyRecorder.read_keys(ResolutionChain.external_keys(name)) }
314
321
  end
315
322
  end
316
323
  end
@@ -393,12 +400,18 @@ module Rigor
393
400
  end
394
401
 
395
402
  @hook_keys = {}
403
+ @external_keys = {}
396
404
 
397
405
  # The frozen `method:Name.hook` negative keys of `name`'s {Relevance::HOOKS}, built once per name.
398
406
  def self.hook_keys(name)
399
407
  @hook_keys[name] ||= Relevance::HOOKS.map { |hook| "method:#{name}.#{hook}".freeze }.freeze
400
408
  end
401
409
 
410
+ # {hook_keys} plus the `mixin:<name>` key an external entry files for its unpositioned mixins (#1661).
411
+ def self.external_keys(name)
412
+ @external_keys[name] ||= (hook_keys(name) + ["mixin:#{name}".freeze]).freeze
413
+ end
414
+
402
415
  def record_head(scope, start, side)
403
416
  ResolutionChain.record_class(scope, @root) unless start.zero? && @entries.first&.name == @root
404
417
  start.times { |index| ResolutionChain.record_entry(scope, @entries[index], side) }
@@ -440,6 +453,28 @@ module Rigor
440
453
  @entries[@level_starts[index]...finish]
441
454
  end
442
455
 
456
+ # Issue #1624 — whether the chain's last class has a superclass the tables cannot name, so ancestors the chain
457
+ # does not list follow it where a chain otherwise ends in the implicit `Object`: a superclass written as an
458
+ # expression (`class K < Struct.new(:a)`, `< Data.define(:a)`, `< Class.new(Base)`, `< pick_base`), which the
459
+ # superclass table records as `nil`, or a class a constant write creates (`K = Class.new(Base)`,
460
+ # `K = Struct.new(:a)`, `K = Data.define(:a)`), which has no superclass entry. The constructor's methods
461
+ # (`members`, the member readers, a block's `def`s) and everything it inherits live in that unlisted part.
462
+ # Every fact read here is filed under the last class's class edge, which a read reaching the tail has passed.
463
+ def opaque_tail?(scope)
464
+ @memo.fetch(:opaque_tail) do
465
+ last = level_count.positive? ? @level_classes[level_count - 1] : nil
466
+ @memo[:opaque_tail] = !last.nil? && ResolutionChain.opaque_superclass?(scope, last)
467
+ end
468
+ end
469
+
470
+ def self.opaque_superclass?(scope, name)
471
+ superclasses = scope.discovery.discovered_superclasses
472
+ return superclasses[name].nil? if superclasses.key?(name)
473
+
474
+ scope.discovery.discovered_parameter_envelopes[name]&.key?(DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK) ||
475
+ !(scope.struct_member_layout(name) || scope.data_member_layout(name)).nil?
476
+ end
477
+
443
478
  # Each complete level up to the first superclass the project does not declare, as `[class_name,
444
479
  # modules, externals]`: the level's class, the project modules around it (each once), and the candidate
445
480
  # lists of the ancestors in it the project does not declare — the shape `call.wrong-arity` reads.
data/lib/rigor/scope.rb CHANGED
@@ -1213,13 +1213,28 @@ module Rigor
1213
1213
  # wrong-arity rules consult this map to suppress diagnostics for methods the user has defined dynamically,
1214
1214
  # even when no RBS sig describes them.
1215
1215
  # A name defined on both sides of one class records {DiscoveryIndex::METHOD_KIND_BOTH} and matches either kind.
1216
+ #
1217
+ # Issue #2121 — a hit is an ADR-46 read. The table carries what no `def` symbol edge covers — an `attr_*`, an
1218
+ # `alias` or a `define_method` (a cross-file `def` is withheld from it) — so an answer that read a hit records
1219
+ # the class edge to the files declaring `class_name` and the name edge `defines:<method_name>`: deleting the
1220
+ # `attr_accessor` re-checks the caller it silenced, whether or not the deleting file declares the class. A miss
1221
+ # records nothing here; the readers that act on one record their own negative edges.
1216
1222
  def discovered_method?(class_name, method_name, kind)
1217
1223
  table = @discovery.discovered_methods[class_name.to_s]
1218
1224
  return false unless table
1219
1225
 
1220
1226
  recorded = table[method_name.to_sym]
1221
- recorded == kind || recorded == DiscoveryIndex::METHOD_KIND_BOTH
1227
+ return false unless recorded == kind || recorded == DiscoveryIndex::METHOD_KIND_BOTH
1228
+
1229
+ record_discovered_method(class_name, method_name) if Analysis::DependencyRecorder.active?
1230
+ true
1231
+ end
1232
+
1233
+ def record_discovered_method(class_name, method_name)
1234
+ record_class_dependency(class_name)
1235
+ Analysis::DependencyRecorder.read_name(:defines, method_name)
1222
1236
  end
1237
+ private :record_discovered_method
1223
1238
 
1224
1239
  # ADR-34 § "Decision" — predicate identifying a toplevel-shaped scope (no enclosing `class` / `module` body).
1225
1240
  # True at the top of a file AND inside a top-level `def` body (since toplevel defs leave `self_type` nil per
@@ -1410,6 +1425,27 @@ module Rigor
1410
1425
  call_node)
1411
1426
  end
1412
1427
 
1428
+ # Issue #1569 — whether `def_node`, written in this file, is a module function of `class_name`: a `:both`
1429
+ # deferred-range row over the def (a bare `module_function` before it, or `module_function def x`;
1430
+ # `ModuleFunctionState.prescan_deferred`), or a `:singleton` row for the name after the def
1431
+ # (`module_function :x`) whose singleton copy is this def (`ModuleFunctionState.each_singleton_copy`).
1432
+ def module_function_def?(class_name, def_node)
1433
+ path = source_path
1434
+ return false if path.nil?
1435
+
1436
+ ranges = discovered_deferred_ranges[path]
1437
+ return false if ranges.nil?
1438
+
1439
+ start = def_node.location.start_offset
1440
+ ranges.any? do |(row_start, _finish, name, kind, owner)|
1441
+ next false unless name == def_node.name && owner == class_name.to_s
1442
+
1443
+ (kind == :both && row_start == start) ||
1444
+ (kind == :singleton && row_start > start &&
1445
+ singleton_def_for(class_name, name)&.location&.start_offset == start)
1446
+ end
1447
+ end
1448
+
1413
1449
  # Shared ordering half of the two `*_def_shadows_call?` predicates, over this file's
1414
1450
  # `discovered_deferred_ranges` (issue #1097). `exists` — a recorded `"path:line"` site or the
1415
1451
  # method-existence table — says a def of `method_name`/`kind` on `class_name` is known at all; a
@@ -1527,7 +1563,8 @@ module Rigor
1527
1563
  #
1528
1564
  # This is EVIDENCE FOR a singleton ancestor, never against one: the walk sees only what a constant
1529
1565
  # argument to a receiverless `extend` (or, since #915, a receiverless `include` / `prepend` inside a
1530
- # `class << self` body) written inside a declaration body spells, so a runtime `Widget.extend(m)` and an
1566
+ # `class << self` body) written inside a declaration body spells, or, since #2094, `Widget.extend(M)` and
1567
+ # `Widget.singleton_class.include(M)` with a constant receiver, so a runtime `Widget.extend(m)` and an
1531
1568
  # `extend` in a file outside the analysed set are both absent from it, as is an `extend` declared only
1532
1569
  # in RBS — that one reaches the same consumer through `Environment#singleton_extended_modules` instead.
1533
1570
  # Its one consumer ({Inference::Narrowing.narrow_class}) therefore reads it
@@ -1905,8 +1942,9 @@ module Rigor
1905
1942
  # declaration-level fact of the whole file, which is also the granularity
1906
1943
  # `ScopeIndexer#append_constant_signature` moves the declaration signature at.
1907
1944
  #
1908
- # `constant_sources` is seeded only when dependency recording is on, so on every ordinary run the table
1909
- # is empty and this is one Hash miss; the caller additionally gates on the recorder being active.
1945
+ # `constant_sources` is seeded when dependency recording is on and in the editor publish's discovery seed
1946
+ # (ADR-124), where the recorder is inactive; elsewhere the table is empty and this is one Hash miss. The caller
1947
+ # gates on the recorder being active.
1910
1948
  def record_constant_dependency(name)
1911
1949
  sites = @discovery.constant_sources[name]
1912
1950
  return if sites.nil?
@@ -1917,8 +1955,15 @@ module Rigor
1917
1955
  # Issue #992 — one class's `discovered_parameter_envelopes` bucket (`{[kind, method_name] => envelope}` plus
1918
1956
  # the class-wide marks), or an empty Hash. Records the ADR-46 class edge first: a verdict read off the
1919
1957
  # bucket depends on every file that declares the class, and a `memoize :f` added to one of them moves it.
1958
+ #
1959
+ # Issue #1833 — and the name edge `mark:<last segment>`: a file that does not declare the class may still mark it
1960
+ # (`x.singleton_class.prepend(Comparable)`, `o.extend(Helper)` in a method body, a `refine` block), and
1961
+ # `IncrementalSession#mark_affected` fires the key when such a mark appears or vanishes.
1920
1962
  def parameter_envelopes_of(class_name)
1921
- record_class_dependency(class_name) if Analysis::DependencyRecorder.active?
1963
+ if Analysis::DependencyRecorder.active?
1964
+ record_class_dependency(class_name)
1965
+ Analysis::DependencyRecorder.read_last_segment(:mark, class_name.to_s)
1966
+ end
1922
1967
  @discovery.discovered_parameter_envelopes[class_name.to_s] || EMPTY_PARAMETER_ENVELOPES
1923
1968
  end
1924
1969
 
@@ -1930,11 +1975,18 @@ module Rigor
1930
1975
  # a wrapping `private def` / `private attr_reader` call, else the surrounding `private` / `protected` / `public`
1931
1976
  # modifier state, plus any post-hoc `private :name, ...` named-argument calls. Consumed by the
1932
1977
  # `def.method-visibility-mismatch` rule so explicit-non-self calls to a private method surface a diagnostic.
1933
- def discovered_method_visibility(class_name, method_name)
1978
+ # `singleton: true` reads the class's singleton slot for the name (`[:singleton, name]`).
1979
+ #
1980
+ # Issue #2134 — every answer is an ADR-46 read of the slot, so it records `visibility:<class>#<method_name>`
1981
+ # whichever way it answers ({Analysis::DependencyRecorder.read_visibility}). A `private :foo` can be written from
1982
+ # a reopening or a `class_eval` block in a file the reader has no other edge to, and adding or removing it must
1983
+ # re-check the reader.
1984
+ def discovered_method_visibility(class_name, method_name, singleton: false)
1985
+ Analysis::DependencyRecorder.read_visibility(class_name, method_name) if Analysis::DependencyRecorder.active?
1934
1986
  table = @discovery.discovered_method_visibilities[class_name.to_s]
1935
1987
  return nil unless table
1936
1988
 
1937
- table[method_name.to_sym]
1989
+ table[singleton ? [:singleton, method_name.to_sym] : method_name.to_sym]
1938
1990
  end
1939
1991
 
1940
1992
  # Closes the "`params[:f] ||= []; params[:f] << x`" precision gap (ROADMAP § Type-language / engine —
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../inference/rbs_type_translator"
4
4
  require_relative "type_elaborator"
5
+ require_relative "../rooted_glob"
5
6
 
6
7
  module Rigor
7
8
  module SigGen
@@ -101,7 +102,7 @@ module Rigor
101
102
  dir = path.is_a?(Pathname) ? path : Pathname(path.to_s)
102
103
  next [] unless dir.directory?
103
104
 
104
- Dir.glob(dir.join("**", "*.rbs").to_s).map { |p| File.expand_path(p) }
105
+ RootedGlob.glob(dir, "**/*.rbs").map { |p| File.expand_path(p) }
105
106
  end.to_set
106
107
  end
107
108
 
@@ -60,7 +60,10 @@ module Rigor
60
60
  inline_differs: "sig.skipped.inline-differs",
61
61
  # #1676 — a plain top-level `def` defines a private `Object` method and has no class to write a
62
62
  # declaration under, so it is reported rather than silently dropped. Whether to emit it stays open.
63
- top_level_def: "sig.skipped.top-level-def"
63
+ top_level_def: "sig.skipped.top-level-def",
64
+ # #1968 — a bare `public` / `private` / `protected` that may not run (in control flow or a block) sits
65
+ # between a bare `module_function` and the def, so whether the def is a module function is unknown.
66
+ uncertain_module_function: "sig.skipped.uncertain-module-function"
64
67
  }.freeze
65
68
  end
66
69
  end