rigortype 0.4.4 → 0.4.5

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 (147) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/docs/handbook/11-sig-gen.md +90 -17
  4. data/docs/manual/02-cli-reference.md +39 -9
  5. data/docs/manual/03-configuration.md +10 -8
  6. data/docs/manual/04-diagnostics.md +16 -5
  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 +8 -1
  12. data/docs/manual/18-removing-dead-code.md +18 -7
  13. data/docs/manual/plugins/rigor-rspec.md +101 -1
  14. data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
  15. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
  16. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
  17. data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
  18. data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
  19. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
  20. data/lib/rigor/analysis/check_rules.rb +228 -15
  21. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  22. data/lib/rigor/analysis/incremental.rb +49 -0
  23. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
  24. data/lib/rigor/analysis/incremental_session.rb +166 -24
  25. data/lib/rigor/analysis/path_expansion.rb +3 -1
  26. data/lib/rigor/analysis/reachability/graph.rb +90 -13
  27. data/lib/rigor/analysis/reachability/scan.rb +279 -39
  28. data/lib/rigor/analysis/rule_catalog.rb +17 -4
  29. data/lib/rigor/analysis/runner/pool_coordinator.rb +69 -16
  30. data/lib/rigor/analysis/runner.rb +69 -65
  31. data/lib/rigor/analysis/severity_stamp.rb +11 -0
  32. data/lib/rigor/analysis/worker_session.rb +12 -1
  33. data/lib/rigor/cache/descriptor.rb +40 -7
  34. data/lib/rigor/cache/engine_source.rb +3 -2
  35. data/lib/rigor/cache/incremental_snapshot.rb +26 -8
  36. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  37. data/lib/rigor/cache/store.rb +4 -3
  38. data/lib/rigor/cli/command.rb +3 -1
  39. data/lib/rigor/cli/docs_command.rb +3 -2
  40. data/lib/rigor/cli/doctor_command.rb +1 -1
  41. data/lib/rigor/cli/explain_command.rb +3 -2
  42. data/lib/rigor/cli/plugin_command.rb +2 -1
  43. data/lib/rigor/cli/plugins_command.rb +2 -1
  44. data/lib/rigor/cli/sig_gen_command.rb +32 -11
  45. data/lib/rigor/cli/skill_command.rb +2 -1
  46. data/lib/rigor/cli/skill_describe.rb +3 -2
  47. data/lib/rigor/cli/unused_command.rb +10 -7
  48. data/lib/rigor/cli.rb +3 -2
  49. data/lib/rigor/config_audit.rb +60 -15
  50. data/lib/rigor/configuration/severity_profile.rb +20 -2
  51. data/lib/rigor/configuration.rb +29 -3
  52. data/lib/rigor/effects/signature_sources.rb +3 -1
  53. data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
  54. data/lib/rigor/environment/gated_signature_guard.rb +2 -1
  55. data/lib/rigor/environment/gated_signature_plan.rb +2 -1
  56. data/lib/rigor/environment/installed_gem_set.rb +2 -1
  57. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
  58. data/lib/rigor/environment/rbs_loader.rb +21 -1
  59. data/lib/rigor/inference/block_call_timing.rb +7 -1
  60. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  61. data/lib/rigor/inference/captured_locals.rb +44 -13
  62. data/lib/rigor/inference/clause_widening.rb +90 -0
  63. data/lib/rigor/inference/definer_resolution.rb +87 -20
  64. data/lib/rigor/inference/expression_typer.rb +35 -5
  65. data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
  66. data/lib/rigor/inference/macro_block_self_type.rb +20 -6
  67. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
  68. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
  69. data/lib/rigor/inference/method_dispatcher.rb +67 -3
  70. data/lib/rigor/inference/module_function_state.rb +137 -42
  71. data/lib/rigor/inference/narrowing.rb +54 -10
  72. data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
  73. data/lib/rigor/inference/project_patched_methods.rb +168 -4
  74. data/lib/rigor/inference/project_patched_mixins.rb +162 -0
  75. data/lib/rigor/inference/project_patched_scanner.rb +51 -12
  76. data/lib/rigor/inference/project_patched_visibility.rb +214 -0
  77. data/lib/rigor/inference/project_seed_tables.rb +120 -0
  78. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
  79. data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
  80. data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
  81. data/lib/rigor/inference/return_barrier.rb +16 -0
  82. data/lib/rigor/inference/scope_indexer.rb +675 -105
  83. data/lib/rigor/inference/self_call_ivar_writes.rb +11 -6
  84. data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
  85. data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
  86. data/lib/rigor/inference/statement_evaluator.rb +446 -139
  87. data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
  88. data/lib/rigor/lens/contract_reader.rb +45 -2
  89. data/lib/rigor/lens/document.rb +2 -1
  90. data/lib/rigor/lens/file_reader.rb +12 -2
  91. data/lib/rigor/lens/members.rb +16 -2
  92. data/lib/rigor/lens/run.rb +34 -7
  93. data/lib/rigor/mcp/server.rb +2 -2
  94. data/lib/rigor/plugin/base.rb +3 -2
  95. data/lib/rigor/plugin/io_boundary.rb +42 -6
  96. data/lib/rigor/plugin/isolation.rb +4 -3
  97. data/lib/rigor/protection/discovery_seed.rb +9 -36
  98. data/lib/rigor/protection/mutation_cache.rb +8 -2
  99. data/lib/rigor/rooted_glob.rb +27 -0
  100. data/lib/rigor/scope/discovery_index.rb +5 -0
  101. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
  102. data/lib/rigor/scope/resolution_chain.rb +40 -5
  103. data/lib/rigor/scope.rb +45 -2
  104. data/lib/rigor/sig_gen/alias_index.rb +2 -1
  105. data/lib/rigor/sig_gen/classification.rb +4 -1
  106. data/lib/rigor/sig_gen/generator.rb +167 -22
  107. data/lib/rigor/sig_gen/layout_index.rb +5 -2
  108. data/lib/rigor/sig_gen/observation_collector.rb +137 -11
  109. data/lib/rigor/sig_gen/renderer.rb +13 -155
  110. data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
  111. data/lib/rigor/sig_gen/write_reporter.rb +203 -0
  112. data/lib/rigor/sig_gen/write_result.rb +3 -1
  113. data/lib/rigor/sig_gen/writer.rb +5 -3
  114. data/lib/rigor/signature_path_audit.rb +4 -2
  115. data/lib/rigor/type/constant.rb +10 -1
  116. data/lib/rigor/version.rb +1 -1
  117. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  118. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
  119. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
  120. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
  121. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  122. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
  123. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
  124. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
  125. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
  126. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
  127. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
  128. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  129. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  130. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
  131. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
  132. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
  133. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
  134. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
  135. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
  136. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
  137. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  138. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
  139. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
  140. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
  141. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
  142. data/sig/rigor/plugin/io_boundary.rbs +5 -2
  143. data/sig/rigor/rooted_glob.rbs +7 -0
  144. data/sig/rigor/scope.rbs +4 -3
  145. data/skills/rigor-doctor/references/01-checks.md +23 -11
  146. data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
  147. metadata +13 -1
@@ -98,7 +98,8 @@ module Rigor
98
98
  source_path ||= candidates.first&.path
99
99
  unless inside_sig_root?(target)
100
100
  return WriteResult.new(source_path: source_path, target_path: target,
101
- action: :skipped_outside_sig_root)
101
+ action: :skipped_outside_sig_root,
102
+ skipped: candidates.map { |c| [c, :outside_sig_root] })
102
103
  end
103
104
 
104
105
  target.exist? ? update_existing(source_path, target, candidates) : create_new(source_path, target, candidates)
@@ -636,7 +637,8 @@ module Rigor
636
637
  end
637
638
 
638
639
  # Returns a list of `[method_name (Symbol), kind (Symbol)]` pairs for every method-like member in the
639
- # declaration ({LayoutIndex.member_keys}). ADR-14 slice 4 recognises the three `attr_*` declaration kinds
640
+ # declaration ({LayoutIndex.member_keys}); an `alias` declares its new name (#1975). ADR-14 slice 4
641
+ # recognises the three `attr_*` declaration kinds
640
642
  # so a source-side `attr_reader :name` and an RBS-side `attr_reader name: T` are treated as the same member
641
643
  # (i.e. user-authored); a `def self?.f` counts as both an instance and a singleton `f` (#1488).
642
644
  def collect_member_pairs(decl)
@@ -754,7 +756,7 @@ module Rigor
754
756
 
755
757
  def find_attr_member(decl, method_name, kind)
756
758
  decl.members.find do |m|
757
- next false if m.is_a?(RBS::AST::Members::MethodDefinition)
759
+ next false if m.is_a?(RBS::AST::Members::MethodDefinition) || m.is_a?(RBS::AST::Members::Alias)
758
760
 
759
761
  LayoutIndex.member_keys(m).include?([method_name, kind])
760
762
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "rooted_glob"
4
+
3
5
  module Rigor
4
6
  # Classifies each configured `signature_paths:` entry by what it actually contributes to
5
7
  # the RBS environment, so a caller can warn when a configured path resolves to nothing.
@@ -90,7 +92,7 @@ module Rigor
90
92
  return Entry.new(path: path, status: :missing, rbs_file_count: 0) unless File.exist?(path)
91
93
  return Entry.new(path: path, status: :not_directory, rbs_file_count: 0) unless File.directory?(path)
92
94
 
93
- count = Dir.glob(File.join(path, "**", "*.rbs")).size
95
+ count = RootedGlob.glob(path, "**/*.rbs").size
94
96
  Entry.new(path: path, status: count.zero? ? :empty : :ok, rbs_file_count: count)
95
97
  end
96
98
 
@@ -204,7 +206,7 @@ module Rigor
204
206
  private_class_method :listed?
205
207
 
206
208
  def self.canonical_rbs_files(dir)
207
- Dir.glob(File.join(dir, "**", "*.rbs")).filter_map { |file| canonical_path(file) }
209
+ RootedGlob.glob(dir, "**/*.rbs").filter_map { |file| canonical_path(file) }
208
210
  end
209
211
  private_class_method :canonical_rbs_files
210
212
 
@@ -78,7 +78,8 @@ module Rigor
78
78
  # `RbsTypeTranslator#translate_literal` already maps the parsed RBS Literal back to `Constant`.
79
79
  # Scalar carriers without RBS Literal support (Float, Range, Rational, Complex, Regexp, Pathname)
80
80
  # keep their pre-existing widen-to-class-name behaviour because RBS rejects their literal spellings
81
- # as syntax errors.
81
+ # as syntax errors. `Range` is generic (`Range[out E]`), and a bare `Range` fails `rbs validate`, so
82
+ # it carries the class of its endpoints, or `untyped` when both are nil (#1891).
82
83
  def erase_to_rbs
83
84
  case value
84
85
  when true then "true"
@@ -86,6 +87,7 @@ module Rigor
86
87
  when nil then "nil"
87
88
  when Integer then value.to_s
88
89
  when Symbol, String then value.inspect
90
+ when Range then "Range[#{range_element_rbs}]"
89
91
  else value.class.name
90
92
  end
91
93
  end
@@ -106,6 +108,13 @@ module Rigor
106
108
  def inspect
107
109
  "#<Rigor::Type::Constant #{describe(:short)}>"
108
110
  end
111
+
112
+ private
113
+
114
+ def range_element_rbs
115
+ classes = [value.begin, value.end].compact.map { |endpoint| endpoint.class.name }.uniq.sort
116
+ classes.empty? ? "untyped" : classes.join(" | ")
117
+ end
109
118
  end
110
119
  end
111
120
  end
data/lib/rigor/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rigor
4
- VERSION = "0.4.4"
4
+ VERSION = "0.4.5"
5
5
  end
@@ -99,7 +99,7 @@ module Rigor
99
99
  # ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
100
100
  next [] unless @io_boundary.directory?(absolute)
101
101
 
102
- Dir.glob(File.join(absolute, "**", "*.rb"))
102
+ Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
103
103
  end
104
104
  end
105
105
 
@@ -90,7 +90,7 @@ module Rigor
90
90
  # ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
91
91
  next [] unless @io_boundary.directory?(absolute)
92
92
 
93
- Dir.glob(File.join(absolute, "**", "*.rb"))
93
+ Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
94
94
  end
95
95
  end
96
96
 
@@ -58,7 +58,7 @@ module Rigor
58
58
  # ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
59
59
  next [] unless @io_boundary.directory?(absolute)
60
60
 
61
- Dir.glob(File.join(absolute, "**", "*.rb"))
61
+ Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
62
62
  end
63
63
  end
64
64
 
@@ -159,13 +159,18 @@ module Rigor
159
159
 
160
160
  # Project-relative paths, the spelling `#template_units_for_file` is handed and the spelling the
161
161
  # compiled cache is keyed by.
162
+ #
163
+ # #1803 — listed through the boundary's `#glob`, which records each root's membership: a helper
164
+ # added under `app/helpers`, or a template under a `view_search_paths:` root the template globs do
165
+ # not claim, is analysed by nobody, and without the row the run-result caches kept serving the
166
+ # index built before it appeared.
162
167
  def self.source_files(io_boundary, roots, pattern)
163
168
  root_prefix = "#{Dir.pwd}#{File::SEPARATOR}"
164
169
  roots.flat_map do |root|
165
170
  absolute = File.expand_path(root)
166
171
  next [] unless io_boundary.directory?(absolute)
167
172
 
168
- Dir.glob(File.join(absolute, "**", pattern)).map { |path| path.delete_prefix(root_prefix) }
173
+ io_boundary.glob(absolute, "**/#{pattern}").map { |path| path.delete_prefix(root_prefix) }
169
174
  end
170
175
  end
171
176
 
@@ -116,7 +116,7 @@ module Rigor
116
116
  absolute = File.expand_path(root)
117
117
  next [] unless @io_boundary.directory?(absolute)
118
118
 
119
- Dir.glob(File.join(absolute, "**", "*.rb"))
119
+ @io_boundary.glob(absolute, "**/*.rb")
120
120
  end
121
121
  end
122
122
 
@@ -127,7 +127,7 @@ module Rigor
127
127
  # ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
128
128
  next [] unless @io_boundary.directory?(absolute)
129
129
 
130
- Dir.glob(File.join(absolute, "**", "*.rb"))
130
+ Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
131
131
  end
132
132
  end
133
133
 
@@ -146,11 +146,12 @@ module Rigor
146
146
 
147
147
  # ADR-45 WD1b (#613) — `config/application.rb` is probed at a fixed path, so its ABSENCE is what
148
148
  # shapes "no adapter" on a project that has none yet; the probe goes through the boundary to record
149
- # that. The globbed `config/environments/*.rb` entries exist by enumeration and only pick up the
150
- # (immediately superseded) presence row.
149
+ # that. The `config/environments/*.rb` listing goes through `#glob` (#1803), whose row records the
150
+ # directory's membership; each listed entry only picks up the (immediately superseded) presence row.
151
151
  def config_files(io_boundary, root)
152
152
  application = File.join(root, CONFIG_PATHS.first)
153
- environments = Dir.glob(File.join(root, CONFIG_PATHS.last, "*.rb"))
153
+ environments_dir = File.join(root, CONFIG_PATHS.last)
154
+ environments = io_boundary.glob(environments_dir, "*.rb")
154
155
  ([application] + environments).select { |path| io_boundary.file?(path) }
155
156
  rescue StandardError
156
157
  []
@@ -92,7 +92,7 @@ module Rigor
92
92
  # ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
93
93
  next [] unless @io_boundary.directory?(absolute)
94
94
 
95
- Dir.glob(File.join(absolute, "**", "*.rb"))
95
+ Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
96
96
  end
97
97
  end
98
98
 
@@ -375,7 +375,7 @@ module Rigor
375
375
  # ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
376
376
  next [] unless @io_boundary.directory?(absolute)
377
377
 
378
- Dir.glob(File.join(absolute, "**", "*.rb"))
378
+ Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
379
379
  end
380
380
  end
381
381
 
@@ -69,7 +69,7 @@ module Rigor
69
69
  # ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
70
70
  next [] unless @io_boundary.directory?(absolute)
71
71
 
72
- Dir.glob(File.join(absolute, "**", "*.rb"))
72
+ Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
73
73
  end
74
74
  end
75
75
 
@@ -65,7 +65,7 @@ module Rigor
65
65
  # ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
66
66
  next [] unless @io_boundary.directory?(absolute)
67
67
 
68
- Dir.glob(File.join(absolute, "**", "*.rb"))
68
+ Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
69
69
  end
70
70
  end
71
71
  end
@@ -109,7 +109,7 @@ module Rigor
109
109
  def scannable_paths
110
110
  @scannable_paths ||= services.configuration.paths.flat_map do |entry|
111
111
  if io_boundary.directory?(entry)
112
- Dir.glob(File.join(entry, "**", "*.rb"), sort: true)
112
+ Dir.glob("**/*.rb", base: entry).map { |path| File.join(entry, path) }
113
113
  elsif io_boundary.file?(entry) && entry.end_with?(".rb")
114
114
  [entry]
115
115
  else
@@ -54,7 +54,7 @@ module Rigor
54
54
  if @io_boundary.file?(absolute)
55
55
  [absolute]
56
56
  elsif @io_boundary.directory?(absolute)
57
- Dir.glob(File.join(absolute, "**", "*.rb"))
57
+ Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
58
58
  else
59
59
  []
60
60
  end
@@ -11,6 +11,8 @@ module Rigor
11
11
  # carried no row at all and was served after any edit, an analysed file's included, until `--no-cache`.
12
12
  class Discoverer
13
13
  RUBY_GLOB = "**/*.rb"
14
+ RUBY_FILE = "*.rb"
15
+ SKIPPED_DIRECTORIES = %w[vendor .git].freeze
14
16
 
15
17
  def self.discover(root:, io:)
16
18
  new(root: root, io: io).discover
@@ -67,20 +69,47 @@ module Rigor
67
69
 
68
70
  private
69
71
 
72
+ # #1959 — one `:kinds` listing row over the root's top-level entries, then one `:names` `**/*.rb` row per
73
+ # real directory among them other than `vendor/`. A single `**/*.rb` row over the root re-walked an in-tree
74
+ # `vendor/bundle` on every warm validation and read stale on every `bundle install`. The listing carries
75
+ # each entry's kind and not just its name: the walk descends a directory but not a symlinked one (`**` never
76
+ # followed it), so a file `gen` that becomes a directory `gen/`, or a symlink that becomes a real
77
+ # directory, changes what it reads while every name stays put.
78
+ #
79
+ # The answer is `Dir.glob("**/*.rb")` minus the skipped paths and minus every match that is not a regular
80
+ # file (#2113), in its order: entry by entry, a top-level name matching `*.rb`, then that directory's
81
+ # matches. A directory so named is walked, never read. The boundary answers absolute paths; they are spelled
82
+ # back under `@root` as `Dir.glob` spells them.
70
83
  def ruby_files
71
84
  return [] unless @root && @io.directory?(@root.to_s)
72
85
 
73
- boundary_glob.reject { |p| p.include?("/vendor/") || p.include?("/.git/") }
74
- end
75
-
76
- # One `:names` `**/*.rb` row over the root, `vendor/` included: a per-entry walk that skipped it recorded
77
- # names only, so a top-level entry that changed kind under the same name went unseen. The boundary
78
- # answers absolute paths; they are spelled back under `@root` as `Dir.glob` spells them, so the filter
79
- # above sees the same strings it always did.
80
- def boundary_glob
81
86
  root = @root.to_s
82
87
  prefix = "#{File.absolute_path(root)}/"
83
- @io.glob(root, RUBY_GLOB).map { |path| File.join(root, path.delete_prefix(prefix)) }
88
+ @io.list_directory_kinds(root).flat_map do |path, kind|
89
+ name = path.delete_prefix(prefix)
90
+ own = File.fnmatch?(RUBY_FILE, name) && readable_file?(path, kind) ? [File.join(root, name)] : []
91
+ next own unless kind == :directory && !SKIPPED_DIRECTORIES.include?(name)
92
+
93
+ own + @io.glob(path, RUBY_GLOB).filter_map do |nested|
94
+ relative = nested.delete_prefix(prefix)
95
+ File.join(root, relative) if !skipped?(relative) && @io.file?(nested)
96
+ end
97
+ end
98
+ end
99
+
100
+ # #2113 — `*.rb` names a directory too, and a fifo, socket or device can carry the name. Only a regular file
101
+ # is read: `read_file` on a directory raises `EISDIR` and aborts the whole walk, and on a fifo it blocks
102
+ # until a writer appears. A top-level entry is judged by its `lstat` kind, and a symlink by what it
103
+ # resolves to; a nested match is judged by `file?` (which follows a symlink as `File.file?` does).
104
+ def readable_file?(path, kind)
105
+ kind == :file || (kind == :symlink && @io.file?(path))
106
+ end
107
+
108
+ # #1956 — the skip reads the path relative to the root, a `vendor` or `.git` directory segment at any
109
+ # depth under it. Matching `/vendor/` in the whole path also dropped every file of a project that itself
110
+ # lives under some `…/vendor/…` directory, and the catalog came out empty.
111
+ def skipped?(relative)
112
+ relative.split("/")[0...-1].intersect?(SKIPPED_DIRECTORIES)
84
113
  end
85
114
 
86
115
  # The glob's rows record which files match; this read's row carries an edit.
@@ -26,7 +26,7 @@ module Rigor
26
26
  def extconf_uses_ffx?(root, io = nil)
27
27
  return false if root.nil?
28
28
 
29
- extconfs = io ? io.glob(root.to_s, EXTCONF_GLOB) : Dir.glob(File.join(root.to_s, EXTCONF_GLOB))
29
+ extconfs = io ? io.glob(root.to_s, EXTCONF_GLOB) : Dir.glob(EXTCONF_GLOB, base: root.to_s).map { |path| File.join(root.to_s, path) }
30
30
  extconfs.any? do |extconf|
31
31
  read_text(extconf, io).include?("FFX.create_makefile")
32
32
  rescue SystemCallError
@@ -72,7 +72,7 @@ module Rigor
72
72
  # ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
73
73
  next [] unless @io_boundary.directory?(absolute)
74
74
 
75
- Dir.glob(File.join(absolute, "**", "*.rb"))
75
+ Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
76
76
  end
77
77
  end
78
78
 
@@ -60,7 +60,7 @@ module Rigor
60
60
  # ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
61
61
  next [] unless @io_boundary.directory?(absolute)
62
62
 
63
- Dir.glob(File.join(absolute, "**", "*.rb"))
63
+ Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
64
64
  end
65
65
  end
66
66
 
@@ -34,12 +34,17 @@ module Rigor
34
34
  end
35
35
  end
36
36
 
37
- attr_reader :entries, :locales
37
+ attr_reader :entries, :locales, :load_errors
38
38
 
39
39
  # @param locales — all locale names that contributed at least one key.
40
- def initialize(entries, locales:)
40
+ # @param load_errors — the {LocaleLoader::LoadError}s of the files that did not parse (#1954). They
41
+ # ride in the index, not beside it, because the index is the cached producer value: a warm run
42
+ # serves it without re-reading a single locale file, so an error kept anywhere else is lost exactly
43
+ # when the cache hits.
44
+ def initialize(entries, locales:, load_errors: [])
41
45
  @entries = entries.freeze
42
46
  @locales = locales.dup.freeze
47
+ @load_errors = load_errors.map(&:freeze).freeze
43
48
  @by_key = entries.to_h { |e| [e.dotted_key, e] }.freeze
44
49
  freeze
45
50
  end
@@ -15,9 +15,9 @@ module Rigor
15
15
  # flattened into dotted keys (`users.welcome`, `errors.messages.blank`, …). For each leaf string,
16
16
  # `%{var}` placeholders are extracted via a simple regex.
17
17
  #
18
- # Files that fail to parse are skipped with a load-error diagnostic surfaced through the plugin's
19
- # error channel. Non-Hash YAML roots (e.g. a top-level sequence) are also skipped — the format is
20
- # locale-keyed by convention.
18
+ # Files that fail to parse are skipped and recorded in the returned index's `load_errors`, which the
19
+ # plugin discloses as a run-scoped load-error row. Non-Hash YAML roots (e.g. a top-level sequence) are
20
+ # also skipped — the format is locale-keyed by convention.
21
21
  class LocaleLoader
22
22
  PLACEHOLDER_RE = /%\{(?<name>[^}]+)\}/
23
23
 
@@ -66,7 +66,7 @@ module Rigor
66
66
  value_kinds: per_key_kinds[dotted_key].freeze
67
67
  )
68
68
  end
69
- LocaleIndex.new(entries, locales: locales.to_a.sort)
69
+ LocaleIndex.new(entries, locales: locales.to_a.sort, load_errors: @load_errors)
70
70
  end
71
71
 
72
72
  private
@@ -90,7 +90,7 @@ module Rigor
90
90
  # ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
91
91
  next [] unless @io_boundary.directory?(absolute)
92
92
 
93
- Dir.glob(File.join(absolute, "**", "*.{yml,yaml}"))
93
+ Dir.glob("**/*.{yml,yaml}", base: absolute).map { |path| File.join(absolute, path) }
94
94
  end.sort
95
95
  end
96
96
 
@@ -57,7 +57,11 @@ module Rigor
57
57
  target_gems: ["i18n"],
58
58
  # Bumped 2026-06-23 — view template lazy-key scanning (`t('.key')` inside ERB / Haml / Slim under
59
59
  # `view_search_paths`).
60
- version: "0.3.0",
60
+ # Bumped 2026-10-17 (#1954) — the cached `:locale_index` value now carries the YAML load errors. A
61
+ # belt-and-braces key change: a released build already drops pre-change entries with the cache ABI
62
+ # (`Rigor::VERSION`) and a checkout with its engine-source identity, and the read below tolerates
63
+ # an index without errors either way.
64
+ version: "0.3.1",
61
65
  description: "Validates I18n `t(key)` calls against `config/locales/*.yml`.",
62
66
  config_schema: {
63
67
  "locale_search_paths" => { kind: :array, default: ["config/locales"] },
@@ -72,17 +76,12 @@ module Rigor
72
76
  )
73
77
 
74
78
  # `watch:` covers every `.yml` / `.yaml` file under the locale search paths so the cache invalidates
75
- # when locale files are added, removed, or edited (ADR-60 WD3). `@load_errors` is a producer-side
76
- # capture: it is populated only when the block runs (a cache miss / a watched file changed), which is
77
- # exactly when a malformed YAML must re-surface.
79
+ # when locale files are added, removed, or edited (ADR-60 WD3). The YAML load errors travel inside the
80
+ # returned index (`LocaleIndex#load_errors`), not in an instance variable the block sets (#1954): a
81
+ # warm producer-cache hit skips the block, so a block-side capture lost the load-error row on every
82
+ # run whose result was not itself served from the run-result cache.
78
83
  producer :locale_index, watch: -> { [[@locale_search_paths, "**/*.yml", "**/*.yaml"]] } do |_params|
79
- loader = LocaleLoader.new(
80
- io_boundary: io_boundary,
81
- search_paths: @locale_search_paths
82
- )
83
- index = loader.load
84
- @load_errors = loader.load_errors
85
- index
84
+ LocaleLoader.new(io_boundary: io_boundary, search_paths: @locale_search_paths).load
86
85
  end
87
86
 
88
87
  # Scans view templates under `view_search_paths` for lazy `t('.key')` / `I18n.translate('.key')`
@@ -102,7 +101,6 @@ module Rigor
102
101
  @locale_search_paths = Array(config.fetch("locale_search_paths")).map(&:to_s)
103
102
  @view_search_paths = Array(config.fetch("view_search_paths")).map(&:to_s)
104
103
  @configured_locales = Array(config.fetch("configured_locales")).map(&:to_s)
105
- @load_errors = []
106
104
  end
107
105
 
108
106
  # Returns no rows of its own: every file-level outcome here is run-scoped. The YAML load errors and the
@@ -114,8 +112,11 @@ module Rigor
114
112
  # are lazily loaded + memoised by `producer_value`, so the repeat registrations are no-ops.
115
113
  def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
116
114
  index = producer_value(:locale_index)
117
- disclose_load_errors unless @load_errors.empty?
118
- disclose_locale_runtime_error if index.nil? && producer_error(:locale_index)
115
+ if index
116
+ disclose_load_errors(Array(index.load_errors))
117
+ elsif producer_error(:locale_index)
118
+ disclose_locale_runtime_error
119
+ end
119
120
  view_diags = producer_value(:view_diagnostics) || []
120
121
  if (view_err = producer_error(:view_diagnostics))
121
122
  disclose_view_runtime_error(view_err)
@@ -175,7 +176,7 @@ module Rigor
175
176
  # ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
176
177
  next [] unless io_boundary.directory?(absolute)
177
178
 
178
- Dir.glob(File.join(absolute, "**", "*.{erb,haml,slim}"))
179
+ Dir.glob("**/*.{erb,haml,slim}", base: absolute).map { |path| File.join(absolute, path) }
179
180
  end.sort
180
181
  end
181
182
 
@@ -209,8 +210,8 @@ module Rigor
209
210
  # be pinned to the first analysed Ruby file — once per plugin INSTANCE, so once per fork-pool worker
210
211
  # (#1051). They go through the run-scoped channel instead: keyed by the locale path that failed, so a
211
212
  # project with two broken YAML files still discloses both, each exactly once, at `.rigor.yml:1:1`.
212
- def disclose_load_errors
213
- @load_errors.each do |err|
213
+ def disclose_load_errors(load_errors)
214
+ load_errors.each do |err|
214
215
  disclose_once(
215
216
  "locale-parse:#{err.path}",
216
217
  message: "rigor-rails-i18n: failed to parse `#{err.path}`: #{err.message}",
@@ -168,7 +168,7 @@ module Rigor
168
168
  # ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
169
169
  next unless io_boundary.directory?(absolute)
170
170
 
171
- Dir.glob(File.join(absolute, "**", "*.rb")).each(&)
171
+ Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }.each(&)
172
172
  end
173
173
  end
174
174
 
@@ -92,7 +92,7 @@ module Rigor
92
92
  # ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
93
93
  next [] unless @io_boundary.directory?(absolute)
94
94
 
95
- Dir.glob(File.join(absolute, "**", "*.rb"))
95
+ Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
96
96
  end
97
97
  end
98
98
 
@@ -29,7 +29,9 @@ module Rigor
29
29
  # ## Reach
30
30
  #
31
31
  # All nine class-level HTTP verb methods Sinatra exposes: `get`, `post`, `put`, `delete`, `head`,
32
- # `options`, `patch`, `link`, `unlink` (`lib/sinatra/base.rb:1531-1553`). Both modular-style subclasses
32
+ # `options`, `patch`, `link`, `unlink` (`lib/sinatra/base.rb:1531-1553`), plus the `before` / `after`
33
+ # filters (`add_filter`) and the `error` / `not_found` handlers, whose blocks `compile!` turns into
34
+ # instance methods through the same `generate_method` (issue #1768). Both modular-style subclasses
33
35
  # of `Sinatra::Base` and `Sinatra::Application` (classic top-level style, when used via `class App <
34
36
  # Sinatra::Application`) match because the receiver constraint accepts every subclass.
35
37
  #
@@ -57,7 +59,7 @@ module Rigor
57
59
  block_as_methods: [
58
60
  Rigor::Plugin::Macro::BlockAsMethod.new(
59
61
  receiver_constraint: "Sinatra::Base",
60
- method_names: %i[get post put delete head options patch link unlink]
62
+ method_names: %i[get post put delete head options patch link unlink before after error not_found]
61
63
  )
62
64
  ]
63
65
  )
@@ -504,8 +504,8 @@ module Rigor
504
504
  # must not vary by machine / run. `Dir.glob` sorts its results by default on Ruby 3.0+, so the fold
505
505
  # order is already stable (the slice-1 comment's "filesystem order" caveat predates that default);
506
506
  # this walk relies on it rather than re-sorting.
507
- Dir.glob(File.join(absolute, "**", "*#{ext}")).each do |path|
508
- harvest_file(canonicalize(path), catalog, sigil_by_path, parse_errors_by_path)
507
+ Dir.glob("**/*#{ext}", base: absolute).each do |relative|
508
+ harvest_file(canonicalize(File.join(absolute, relative)), catalog, sigil_by_path, parse_errors_by_path)
509
509
  end
510
510
  end
511
511
  elsif io_boundary.file?(absolute) && extensions.any? { |ext| absolute.end_with?(ext) }
@@ -63,7 +63,7 @@ module Rigor
63
63
  @project_paths.flat_map do |entry|
64
64
  absolute = File.expand_path(entry)
65
65
  if @io_boundary.directory?(absolute)
66
- Dir.glob(File.join(absolute, "**", "*.rb"))
66
+ Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
67
67
  elsif absolute.end_with?(".rb") && @io_boundary.file?(absolute)
68
68
  [absolute]
69
69
  else
@@ -78,7 +78,7 @@ module Rigor
78
78
  # ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
79
79
  next [] unless @io_boundary.directory?(absolute)
80
80
 
81
- Dir.glob(File.join(absolute, "**", "*.rb"))
81
+ Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
82
82
  end
83
83
  end
84
84
  end
@@ -4,11 +4,14 @@ class Rigor::Plugin::IoBoundary
4
4
  def file?: (untyped) -> bool
5
5
  def directory?: (untyped) -> bool
6
6
  def list_directory: (untyped) -> Array[String]
7
+ def list_directory_kinds: (untyped) -> Hash[String, untyped]
7
8
  # Records a `:names` glob row (membership only); a caller needing a match's content `read_file`s it. A `..`
8
9
  # segment, a brace or a backslash in `pattern` raises `AccessDeniedError`.
9
10
  def glob: (untyped root, untyped pattern) -> Array[String]
10
- # sig-gen gap: #1156 — the generator pins the empty-summary literal (`count: 0, first_path: nil`); the
11
- # record's fields are the running count and the first refused path, nil when nothing was refused.
11
+ # sig-gen gap: #1154 — the proposal leaves both string fields untyped. `nearest_root` comes from `@policy`, an
12
+ # ivar assigned from an `initialize` parameter (the #1154 shape); `first_path` comes from `@first_refused_path`,
13
+ # written from `record_refusal(path)`'s parameter outside `initialize`, a case #1154 as titled does not yet cover.
14
+ # The record's fields are the running count and the first refused path, nil when nothing was refused.
12
15
  def refusal_summary: () -> { count: Integer, first_path: String?, nearest_root: String? }?
13
16
  def open_url: (untyped url) -> String
14
17
  # Returns a frozen `Rigor::Cache::Descriptor` (no RBS yet -> untyped).
@@ -0,0 +1,7 @@
1
+ module Rigor
2
+ module RootedGlob
3
+ METACHARACTERS: Regexp
4
+
5
+ def self?.glob: (untyped, untyped) -> Array[String]
6
+ end
7
+ end
data/sig/rigor/scope.rbs CHANGED
@@ -61,7 +61,7 @@ module Rigor
61
61
  def discovered_singleton_def_nodes: () -> Hash[String, Hash[Symbol, untyped]]
62
62
  def discovered_def_sources: () -> Hash[String, Hash[Symbol, String]]
63
63
  def discovered_singleton_def_sources: () -> Hash[String, Hash[Symbol, String]]
64
- def discovered_method_visibilities: () -> Hash[String, Hash[Symbol, Symbol]]
64
+ def discovered_method_visibilities: () -> Hash[String, Hash[Symbol | [:singleton, Symbol], Symbol]]
65
65
  # sig-gen gap: #1148 — sig-gen skips an endless-def reader (untyped return); the row is hand-written.
66
66
  def discovered_parameter_envelopes: () -> Hash[String, Hash[untyped, untyped]]
67
67
  # sig-gen gap: #1149 — sig-gen skips the `||` fallback reader (untyped return); the row is hand-written.
@@ -128,7 +128,7 @@ module Rigor
128
128
  attr_reader discovered_singleton_def_nodes: Hash[String, Hash[Symbol, untyped]]
129
129
  attr_reader discovered_def_sources: Hash[String, Hash[Symbol, String]]
130
130
  attr_reader discovered_singleton_def_sources: Hash[String, Hash[Symbol, String]]
131
- attr_reader discovered_method_visibilities: Hash[String, Hash[Symbol, Symbol]]
131
+ attr_reader discovered_method_visibilities: Hash[String, Hash[Symbol | [:singleton, Symbol], Symbol]]
132
132
  # sig-gen gap: #1150 — `Data.define` members carry no type, as for every other row in this class.
133
133
  attr_reader discovered_parameter_envelopes: Hash[String, Hash[untyped, untyped]]
134
134
  attr_reader discovered_superclasses: Hash[String, String]
@@ -201,7 +201,7 @@ module Rigor
201
201
  # authored: ADR-119 WD1 sibling pairing; no inferred signature exists for it (#1150).
202
202
  def sibling_tables: () -> Hash[Symbol, untyped]
203
203
 
204
- def with: (?declared_types: Hash[untyped, Type::t], ?class_ivars: Hash[String, Hash[Symbol, Type::t]], ?class_cvars: Hash[String, Hash[Symbol, Type::t]], ?program_globals: Hash[Symbol, Type::t], ?program_global_seeds: Hash[Symbol, Type::t], ?discovered_classes: Hash[String, Type::Singleton], ?in_source_constants: Hash[String, Type::t], ?discovered_methods: Hash[String, Hash[Symbol, Symbol]], ?discovered_def_nodes: Hash[String, Hash[Symbol, untyped]], ?discovered_def_nestings: Hash[untyped, Array[String]], ?discovered_singleton_def_nodes: Hash[String, Hash[Symbol, untyped]], ?discovered_def_sources: Hash[String, Hash[Symbol, String]], ?discovered_singleton_def_sources: Hash[String, Hash[Symbol, String]], ?discovered_method_visibilities: Hash[String, Hash[Symbol, Symbol]], ?discovered_parameter_envelopes: Hash[String, Hash[untyped, untyped]], ?discovered_superclasses: Hash[String, String], ?discovered_includes: Hash[String, Array[String]], ?discovered_prepends: Hash[String, Array[String]], ?discovered_extends: Hash[String, Array[String]], ?discovered_class_sources: Hash[String, Set[String]], ?constant_sources: Hash[String, Set[String]], ?constant_writers: Hash[String, Array[String]], ?constant_shadowers: Hash[String, Array[String]], ?published_constant_names: Set[String], ?local_constant_names: Set[String], ?published_constant_alias_names: Set[String], ?published_constant_ivars: Hash[String, Set[Symbol]], ?data_member_layouts: Hash[String, Array[Symbol]], ?struct_member_layouts: Hash[String, { members: Array[Symbol], keyword_init: bool }], ?param_inferred_types: Hash[[String, Symbol, Symbol], Hash[Symbol, Type::t]], ?discovered_deferred_ranges: Hash[String, Array[[Integer, Integer, Symbol?, Symbol?, String?]]], ?discovered_refinements: Hash[String, Hash[Symbol | String, Array[String]]], ?discovered_global_write_census: Set[Array[untyped]], ?run_generation: Object?, ?patched_line_readers: Set[Symbol], ?clears_last_status: bool, ?defines_case_equality: bool, ?implicit_self_evidence: untyped, ?in_effect_refinements: untyped, ?possible_discovered_methods: Hash[String, Hash[Symbol, Symbol]], ?possible_discovered_deferred_ranges: Hash[String, Array[[Integer, Integer, Symbol?, Symbol?, String?]]], ?contested_discovered_def_nodes: Set[Array[untyped]], ?contested_discovered_singleton_def_nodes: Set[Array[untyped]], ?contested_discovered_method_visibilities: Set[Array[untyped]], ?contested_discovered_parameter_envelopes: Set[Array[untyped]]) -> DiscoveryIndex
204
+ def with: (?declared_types: Hash[untyped, Type::t], ?class_ivars: Hash[String, Hash[Symbol, Type::t]], ?class_cvars: Hash[String, Hash[Symbol, Type::t]], ?program_globals: Hash[Symbol, Type::t], ?program_global_seeds: Hash[Symbol, Type::t], ?discovered_classes: Hash[String, Type::Singleton], ?in_source_constants: Hash[String, Type::t], ?discovered_methods: Hash[String, Hash[Symbol, Symbol]], ?discovered_def_nodes: Hash[String, Hash[Symbol, untyped]], ?discovered_def_nestings: Hash[untyped, Array[String]], ?discovered_singleton_def_nodes: Hash[String, Hash[Symbol, untyped]], ?discovered_def_sources: Hash[String, Hash[Symbol, String]], ?discovered_singleton_def_sources: Hash[String, Hash[Symbol, String]], ?discovered_method_visibilities: Hash[String, Hash[Symbol | [:singleton, Symbol], Symbol]], ?discovered_parameter_envelopes: Hash[String, Hash[untyped, untyped]], ?discovered_superclasses: Hash[String, String], ?discovered_includes: Hash[String, Array[String]], ?discovered_prepends: Hash[String, Array[String]], ?discovered_extends: Hash[String, Array[String]], ?discovered_class_sources: Hash[String, Set[String]], ?constant_sources: Hash[String, Set[String]], ?constant_writers: Hash[String, Array[String]], ?constant_shadowers: Hash[String, Array[String]], ?published_constant_names: Set[String], ?local_constant_names: Set[String], ?published_constant_alias_names: Set[String], ?published_constant_ivars: Hash[String, Set[Symbol]], ?data_member_layouts: Hash[String, Array[Symbol]], ?struct_member_layouts: Hash[String, { members: Array[Symbol], keyword_init: bool }], ?param_inferred_types: Hash[[String, Symbol, Symbol], Hash[Symbol, Type::t]], ?discovered_deferred_ranges: Hash[String, Array[[Integer, Integer, Symbol?, Symbol?, String?]]], ?discovered_refinements: Hash[String, Hash[Symbol | String, Array[String]]], ?discovered_global_write_census: Set[Array[untyped]], ?run_generation: Object?, ?patched_line_readers: Set[Symbol], ?clears_last_status: bool, ?defines_case_equality: bool, ?implicit_self_evidence: untyped, ?in_effect_refinements: untyped, ?possible_discovered_methods: Hash[String, Hash[Symbol, Symbol]], ?possible_discovered_deferred_ranges: Hash[String, Array[[Integer, Integer, Symbol?, Symbol?, String?]]], ?contested_discovered_def_nodes: Set[Array[untyped]], ?contested_discovered_singleton_def_nodes: Set[Array[untyped]], ?contested_discovered_method_visibilities: Set[Array[untyped]], ?contested_discovered_parameter_envelopes: Set[Array[untyped]]) -> DiscoveryIndex
205
205
  end
206
206
 
207
207
  class IndexedKey
@@ -338,6 +338,7 @@ module Rigor
338
338
  # Issue #530 — public so the external-gem ancestry probe asks the same question the walk does.
339
339
  def known_user_class?: (String | Symbol name) -> bool
340
340
  def singleton_def_for: (String | Symbol class_name, String | Symbol method_name) -> ::Prism::DefNode?
341
+ def module_function_def?: (untyped, untyped) -> (false | true)
341
342
  def singleton_def_through_ancestors: (String | Symbol class_name, String | Symbol method_name, ?name_memo: Hash[untyped, untyped]) -> ([::Prism::DefNode, String] | [nil, nil])
342
343
  def user_def_site_for: (String | Symbol class_name, String | Symbol method_name) -> String?
343
344
  def user_singleton_def_site_for: (String | Symbol class_name, String | Symbol method_name) -> String?