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
@@ -8,6 +8,7 @@ require_relative "configuration/dependencies"
8
8
  require_relative "configuration/severity_profile"
9
9
  require_relative "config_path"
10
10
  require_relative "configuration_error"
11
+ require_relative "rooted_glob"
11
12
  require_relative "effects/entry_points"
12
13
  require_relative "effects/label"
13
14
  require_relative "effects/method_key"
@@ -320,12 +321,22 @@ module Rigor
320
321
 
321
322
  # The test roots a run uses: the declared `test_paths:` as written (already resolved against the
322
323
  # config file's directory by {.load}), or — when the key is unset — whichever of
323
- # {CONVENTIONAL_TEST_PATHS} is a directory under `root`, as absolute paths. `root` is the directory
324
- # the run treats as the project root (the CLI's CWD), as for `signature_paths:` auto-detection.
325
- def resolved_test_paths(root: Dir.pwd)
324
+ # {CONVENTIONAL_TEST_PATHS} is a directory under a root, as absolute paths. The roots, unless `root` names
325
+ # one, are the directory of the config file in use, the one `paths:` and a declared `test_paths:` resolve
326
+ # against, and then the working directory, read only when the first holds neither. Auto-detecting against
327
+ # the working directory alone found nothing for a run from a subdirectory, or for the MCP server started
328
+ # elsewhere, though both name the config file (#2078); a config file kept in a subdirectory
329
+ # (`--config=config/rigor.yml`) still finds the project's `spec/` from the project root.
330
+ def resolved_test_paths(root: nil)
326
331
  return test_paths unless test_paths.nil?
327
332
 
328
- CONVENTIONAL_TEST_PATHS.map { |dir| File.absolute_path(dir, root) }.select { |dir| File.directory?(dir) }
333
+ roots = root ? [root] : [exclude_base, Dir.pwd].uniq
334
+ roots.each do |dir|
335
+ found = CONVENTIONAL_TEST_PATHS.map { |test_dir| File.absolute_path(test_dir, dir) }
336
+ .select { File.directory?(it) }
337
+ return found unless found.empty?
338
+ end
339
+ []
329
340
  end
330
341
 
331
342
  # Loads a configuration file.
@@ -995,9 +1006,34 @@ module Rigor
995
1006
  # this entry" so a templated `**` pattern in a fresh project doesn't generate an error per match-less
996
1007
  # pattern.
997
1008
  def expand_pre_eval_entries(entries)
998
- entries.flat_map do |entry|
999
- glob_pattern?(entry) ? Dir.glob(entry, sort: true) : [entry]
1000
- end.uniq.freeze
1009
+ entries.flat_map { |entry| expand_pre_eval_entry(entry) }.uniq.freeze
1010
+ end
1011
+
1012
+ # #1956 — a relative entry arrives joined onto the config file's directory, so the metacharacters the
1013
+ # check above sees may be that directory's own (`ds [x]/`). The leading run of segments that name existing
1014
+ # directories is read literally and only the rest is globbed under it: a literal entry under such a
1015
+ # directory stays a literal path, and a pattern matches what it matches under a plain directory.
1016
+ def expand_pre_eval_entry(entry)
1017
+ return [entry] unless glob_pattern?(entry)
1018
+
1019
+ base, pattern = literal_directory_prefix(entry)
1020
+ return [entry] unless glob_pattern?(pattern)
1021
+
1022
+ base ? RootedGlob.glob(base, pattern) : Dir.glob(entry, sort: true)
1023
+ end
1024
+
1025
+ def literal_directory_prefix(entry)
1026
+ segments = entry.split("/")
1027
+ count = (1...segments.size).take_while { |n| File.directory?(join_segments(segments.take(n))) }.size
1028
+ return [nil, entry] if count.zero?
1029
+
1030
+ [join_segments(segments.take(count)), segments.drop(count).join("/")]
1031
+ end
1032
+
1033
+ # `"/a".split("/")` leads with an empty segment, which spells the filesystem root.
1034
+ def join_segments(segments)
1035
+ path = segments.join("/")
1036
+ path.empty? ? "/" : path
1001
1037
  end
1002
1038
 
1003
1039
  def glob_pattern?(path)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../rooted_glob"
4
+
3
5
  module Rigor
4
6
  module Effects
5
7
  # Everything an effect envelope may be written in, as `[buffer name, RBS source]` pairs
@@ -55,7 +57,7 @@ module Rigor
55
57
  # @param signature_paths — the configured roots; empty / nil takes the default.
56
58
  # @param virtual_rbs — the loader's virtual entries.
57
59
  def collect(signature_paths:, virtual_rbs: nil)
58
- files = roots(signature_paths).flat_map { |root| Dir.glob(File.join(root.to_s, "**", "*.rbs")).sort }
60
+ files = roots(signature_paths).flat_map { |root| RootedGlob.glob(root, "**/*.rbs").sort }
59
61
  sources = files.filter_map do |path|
60
62
  [path, File.read(path)]
61
63
  rescue StandardError
@@ -3,6 +3,7 @@
3
3
  require "yaml"
4
4
 
5
5
  require_relative "../config_path"
6
+ require_relative "../rooted_glob"
6
7
 
7
8
  module Rigor
8
9
  class Environment
@@ -91,8 +92,8 @@ module Rigor
91
92
  # all (`Bundler::Source::Path#path` is the user's own `path:` directory, used in place), so there is
92
93
  # no bundle-relative location to walk. A `path:`-sourced gem's `sig/` is only reachable by adding it
93
94
  # directly to the project's `signature_paths:` — which already works today and needs no discovery.
94
- all = (Dir.glob(resolved.join("ruby", "*", "gems", "*", "sig")) +
95
- Dir.glob(resolved.join("ruby", "*", "bundler", "gems", "*", "sig"))).map { |d| Pathname.new(d) }
95
+ all = (RootedGlob.glob(resolved, "ruby/*/gems/*/sig") +
96
+ RootedGlob.glob(resolved, "ruby/*/bundler/gems/*/sig")).map { |d| Pathname.new(d) }
96
97
  filtered = all.reject { |sig_dir| skip_gems.include?(gem_name_from_sig_path(sig_dir)) }
97
98
  return filtered if locked_gems.nil? || locked_gems.empty?
98
99
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rbs"
4
+ require_relative "../rooted_glob"
4
5
 
5
6
  module Rigor
6
7
  class Environment
@@ -15,7 +16,7 @@ module Rigor
15
16
  # @param dir — the vendored directory (Pathname).
16
17
  # @return the absolute-less names (`"Prime::PseudoPrimeGenerator"`) of every class and module it declares.
17
18
  def type_names(dir)
18
- Dir.glob(File.join(dir.to_s, "*.rbs")).flat_map do |file|
19
+ RootedGlob.glob(dir, "*.rbs").flat_map do |file|
19
20
  _, _, decls = RBS::Parser.parse_signature(File.read(file))
20
21
  names(decls, nil)
21
22
  rescue RBS::BaseError, SystemCallError
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rbs"
4
+ require_relative "../rooted_glob"
4
5
 
5
6
  module Rigor
6
7
  class Environment
@@ -132,7 +133,7 @@ module Rigor
132
133
  end
133
134
 
134
135
  def signature_files(roots)
135
- roots.flat_map { |root| Dir.glob(File.join(root.to_s, "*.rbs")).sort }
136
+ roots.flat_map { |root| RootedGlob.glob(root, "*.rbs").sort }
136
137
  end
137
138
 
138
139
  def shell_buffer
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "lockfile_resolver"
4
+ require_relative "../rooted_glob"
4
5
 
5
6
  module Rigor
6
7
  class Environment
@@ -47,7 +48,7 @@ module Rigor
47
48
  base = Pathname.new(bundle_path)
48
49
  return {} unless base.directory?
49
50
 
50
- Dir.glob(base.join("ruby", "*", "gems", "*")).each_with_object({}) do |dir, acc|
51
+ RootedGlob.glob(base, "ruby/*/gems/*").each_with_object({}) do |dir, acc|
51
52
  name, version, platform = split_install_dirname(File.basename(dir))
52
53
  next unless name
53
54
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "prism"
4
+ require_relative "../rooted_glob"
4
5
 
5
6
  module Rigor
6
7
  class Environment
@@ -71,7 +72,7 @@ module Rigor
71
72
  base = Pathname.new(bundle_path)
72
73
  return {} unless base.directory?
73
74
 
74
- Dir.glob(base.join("ruby", "*", "gems", "*")).each_with_object({}) do |dir, acc|
75
+ RootedGlob.glob(base, "ruby/*/gems/*").each_with_object({}) do |dir, acc|
75
76
  acc[File.basename(dir)] ||= dir
76
77
  end
77
78
  end
@@ -114,7 +115,7 @@ module Rigor
114
115
  base = Pathname.new(bundle_path)
115
116
  return {} unless base.directory?
116
117
 
117
- Dir.glob(base.join("ruby", "*", "bundler", "gems", "*")).each_with_object({}) do |dir, acc|
118
+ RootedGlob.glob(base, "ruby/*/bundler/gems/*").each_with_object({}) do |dir, acc|
118
119
  name = gemspec_gem_name(dir)
119
120
  acc[name] ||= dir if name
120
121
  end
@@ -124,7 +125,7 @@ module Rigor
124
125
  # exactly one. Bundler requires a git-sourced gem to ship one, so "not exactly one" means this is not
125
126
  # the shape we think it is — and a guess from the directory basename is what this exists to avoid.
126
127
  def gemspec_gem_name(dir)
127
- specs = Dir.glob(File.join(dir, "*.gemspec"))
128
+ specs = RootedGlob.glob(dir, "*.gemspec")
128
129
  return nil unless specs.size == 1
129
130
 
130
131
  File.basename(specs.first, ".gemspec")
@@ -10,6 +10,8 @@ require_relative "member_consistency"
10
10
  require_relative "required_features"
11
11
  require_relative "gated_signature_guard"
12
12
  require_relative "gated_signature_plan"
13
+ require_relative "ruby_library_roots"
14
+ require_relative "../rooted_glob"
13
15
 
14
16
  module Rigor
15
17
  class Environment
@@ -549,7 +551,7 @@ module Rigor
549
551
  path = Pathname(path) unless path.is_a?(Pathname)
550
552
  next [] unless path.directory?
551
553
 
552
- Dir.glob(path.join("**", "*.rbs")).map { |p| File.expand_path(p) }
554
+ RootedGlob.glob(path, "**/*.rbs").map { |p| File.expand_path(p) }
553
555
  end.to_set
554
556
  end
555
557
 
@@ -1652,9 +1654,10 @@ module Rigor
1652
1654
  # arity. Read back by the env-cache producer, which rebuilds the env from the loader's readers on
1653
1655
  # a miss — a list it cannot read is a stand-down that runs only on a `--no-cache` build.
1654
1656
  def initialize(libraries: [], signature_paths: [], cache_store: nil, virtual_rbs: [],
1655
- deferred_signature_paths: [])
1657
+ deferred_signature_paths: [], project_signature_paths: [], required_features: [])
1656
1658
  @libraries = libraries.map(&:to_s).freeze
1657
1659
  @signature_paths = signature_paths.map { |p| Pathname(p) }.freeze
1660
+ assign_reopen_inputs(project_signature_paths, required_features)
1658
1661
  # Issue #610 — the subset of `signature_paths:` a bundled PLUGIN contributed. Loaded last and
1659
1662
  # allowed to stand down against a colliding generic arity ({.add_deferred_signatures}). Every entry
1660
1663
  # is also in `@signature_paths`, so the env cache descriptor already digests these files.
@@ -1763,6 +1766,9 @@ module Rigor
1763
1766
  # a partial sidecar as authoritative made `sig-gen --write` on redmine report 5.9x more
1764
1767
  # `call.undefined-method` than the same project with no `sig/` at all.
1765
1768
  #
1769
+ # A class declared only by RBS synthesized from the project's own source joins the set too
1770
+ # ({#inline_only_entry?}, issue #1934).
1771
+ #
1766
1772
  # Memoised per loader: one pass over `class_decls` (~1-2k entries), and every consumer is a check
1767
1773
  # rule on the analysis hot path. Attribution is by buffer NAME, which survives the ADR-54 environment
1768
1774
  # cache since #725 — an environment blob written before that carries the `<cached>` sentinel instead,
@@ -1776,10 +1782,14 @@ module Rigor
1776
1782
  return Set.new.freeze if environment.nil?
1777
1783
 
1778
1784
  project_files = self.class.project_sig_files(@signature_paths)
1779
- return Set.new.freeze if project_files.empty?
1785
+ virtual_names = @virtual_rbs.to_set(&:first)
1786
+ return Set.new.freeze if project_files.empty? && virtual_names.empty?
1780
1787
 
1781
1788
  names = environment.class_decls.each_with_object(Set.new) do |(rbs_name, entry), acc|
1782
- acc << rbs_name.to_s.delete_prefix("::") if self.class.project_entry?(entry, project_files)
1789
+ next unless self.class.project_entry?(entry, project_files) ||
1790
+ inline_only_entry?(entry, virtual_names, project_files)
1791
+
1792
+ acc << rbs_name.to_s.delete_prefix("::")
1783
1793
  end
1784
1794
  names.freeze
1785
1795
  rescue ::RBS::BaseError
@@ -1787,6 +1797,92 @@ module Rigor
1787
1797
  end
1788
1798
  private :build_project_declared_classes
1789
1799
 
1800
+ # Issue #1934 — a class whose every declaration a source RBS synthesizer (`rigor-rbs-inline`) wrote from
1801
+ # the project's own `.rb` files is the project's as much as a `sig/` sidecar is: the declaration is the
1802
+ # inline comments of the source under analysis, so a method another project file adds to the class (a
1803
+ # plain `def`, or an `alias_method` whose target the inline declaration names) is the second file of an
1804
+ # ordinary class, not an ADR-17 monkey-patch. Reading the synthesized RBS as bundled reported
1805
+ # `alias_method :title, :label` from another file while the `alias` keyword, which upstream translates,
1806
+ # read clean.
1807
+ #
1808
+ # EVERY declaration, not only the primary one, and only virtual or `sig/` buffers: a core, stdlib or gem
1809
+ # class an inline-annotated file reopens (`class String` with a `#:` method) keeps its bundled
1810
+ # declaration, and a project `def` on it stays the monkey-patch ADR-17 reports. A declaration whose
1811
+ # buffer cannot be attributed (the pre-#725 `<cached>` sentinel) fails the test, which keeps the
1812
+ # pre-#1934 reading.
1813
+ def inline_only_entry?(entry, virtual_names, project_files)
1814
+ return false if virtual_names.empty?
1815
+
1816
+ project_authored_entry?(entry, virtual_names, project_files)
1817
+ end
1818
+ private :inline_only_entry?
1819
+
1820
+ # Every declaration of `entry` sits in a project `sig/` file or in RBS synthesized from the project's
1821
+ # source; a declaration whose buffer cannot be attributed fails the test.
1822
+ def project_authored_entry?(entry, virtual_names, project_files)
1823
+ buffers = self.class.entry_declarations(entry).map { |decl| self.class.declaration_buffer_name(decl) }
1824
+ return false if buffers.empty? || buffers.any?(&:nil?)
1825
+
1826
+ buffers.all? { |name| virtual_names.include?(name) || project_files.include?(File.absolute_path(name)) }
1827
+ end
1828
+ private :project_authored_entry?
1829
+
1830
+ # Issue #2174 — true when every RBS declaration of `class_name` is the project's own (rbs-inline `#:`
1831
+ # annotations, or a file under the configured `signature_paths:`) and its root constant belongs to a library
1832
+ # Ruby ships ({RubyLibraryRoots}) that the project's source requires ({RequiredFeatures.features_of}). Such
1833
+ # a declaration reopens a library class whose real signatures did not load (`class Matrix` with one
1834
+ # annotated method beside `require "matrix"`, and no `matrix` RBS), so it lists only the methods the
1835
+ # project added: the class's surface is not enumerable from it, and a call it omits is not evidence of an
1836
+ # undefined method. A library whose RBS did load contributes a declaration of its own (core, stdlib, a gem's
1837
+ # `sig/`, an rbs collection, a plugin's `sig/`), which takes the class out of this predicate. A project
1838
+ # class that shares a required library's root name declines too; that is the accepted cost.
1839
+ def ruby_library_reopen?(class_name)
1840
+ name = class_name.to_s.delete_prefix("::")
1841
+ return false if @required_features.empty? || !project_authored_classes.include?(name)
1842
+
1843
+ segments = ruby_library_roots[name.split("::").first]
1844
+ !segments.nil? && @required_features.any? { |feature| segments.include?(feature.split("/").first) }
1845
+ end
1846
+
1847
+ def ruby_library_roots
1848
+ @state[:ruby_library_roots] ||= RubyLibraryRoots.shared
1849
+ end
1850
+
1851
+ # Issue #2174 — the subset of `signature_paths:` the user configured (every entry is also in
1852
+ # `@signature_paths`), and the features the project's source requires: read only by
1853
+ # {#ruby_library_reopen?}.
1854
+ def assign_reopen_inputs(project_signature_paths, required_features)
1855
+ @project_signature_paths = project_signature_paths.map { |p| Pathname(p) }.freeze
1856
+ @required_features = required_features.map { |feature| feature.to_s.dup.freeze }.freeze
1857
+ end
1858
+ private :assign_reopen_inputs
1859
+ private :ruby_library_roots
1860
+
1861
+ # The class / module names every one of whose declarations is the project's own
1862
+ # ({#project_authored_entry?}, which {#inline_only_entry?} asks behind its inline-only gate).
1863
+ def project_authored_classes
1864
+ @state[:project_authored_classes] ||= build_project_authored_classes
1865
+ end
1866
+ private :project_authored_classes
1867
+
1868
+ def build_project_authored_classes
1869
+ environment = env
1870
+ return Set.new.freeze if environment.nil?
1871
+
1872
+ # Only the user's configured `signature_paths:`: `@signature_paths` also holds plugin, gem-shipped,
1873
+ # rbs-collection and overlay directories, whose declarations are a library's own.
1874
+ project_files = self.class.project_sig_files(@project_signature_paths)
1875
+ virtual_names = @virtual_rbs.to_set(&:first)
1876
+ return Set.new.freeze if project_files.empty? && virtual_names.empty?
1877
+
1878
+ environment.class_decls.each_with_object(Set.new) do |(rbs_name, entry), acc|
1879
+ acc << rbs_name.to_s.delete_prefix("::") if project_authored_entry?(entry, virtual_names, project_files)
1880
+ end.freeze
1881
+ rescue ::RBS::BaseError
1882
+ Set.new.freeze
1883
+ end
1884
+ private :build_project_authored_classes
1885
+
1790
1886
  # Issue #527 — the class / module names whose PRIMARY declaration lives in the `rbs` gem's own
1791
1887
  # `core/` or `stdlib/` tree, top-level prefix stripped.
1792
1888
  #
@@ -1807,9 +1903,23 @@ module Rigor
1807
1903
  @state[:core_stdlib_classes] ||= build_core_stdlib_classes
1808
1904
  end
1809
1905
 
1810
- def build_core_stdlib_classes
1906
+ # Issue #1154 — the subset of {#core_stdlib_classes} whose primary declaration is in `core/` alone: the classes
1907
+ # the interpreter implements, as opposed to a stdlib library written in Ruby (`Logger` rewrites `@level` in
1908
+ # its `initialize`). Same attribution, same cache behaviour.
1909
+ def core_class?(class_name)
1910
+ (@state[:core_classes] ||= build_core_stdlib_classes(CORE_ROOTS)).include?(class_name.to_s.delete_prefix("::"))
1911
+ end
1912
+
1913
+ CORE_ROOTS = begin
1914
+ [::RBS::EnvironmentLoader::DEFAULT_CORE_ROOT].compact.map { |root| "#{root}/".freeze }.freeze
1915
+ rescue StandardError
1916
+ [].freeze
1917
+ end
1918
+ private_constant :CORE_ROOTS
1919
+
1920
+ def build_core_stdlib_classes(roots = CORE_STDLIB_ROOTS)
1811
1921
  environment = env
1812
- return Set.new.freeze if environment.nil? || CORE_STDLIB_ROOTS.empty?
1922
+ return Set.new.freeze if environment.nil? || roots.empty?
1813
1923
 
1814
1924
  names = environment.class_decls.each_with_object(Set.new) do |(rbs_name, entry), acc|
1815
1925
  decl = self.class.primary_decl_for(entry)
@@ -1818,7 +1928,7 @@ module Rigor
1818
1928
  path = self.class.declaration_buffer_name(decl)
1819
1929
  next if path.nil?
1820
1930
 
1821
- acc << rbs_name.to_s.delete_prefix("::") if CORE_STDLIB_ROOTS.any? { |root| path.start_with?(root) }
1931
+ acc << rbs_name.to_s.delete_prefix("::") if roots.any? { |root| path.start_with?(root) }
1822
1932
  end
1823
1933
  names.freeze
1824
1934
  rescue ::RBS::BaseError
@@ -1916,6 +2026,25 @@ module Rigor
1916
2026
  (@state[:definition_build_failures] || []).dup.freeze
1917
2027
  end
1918
2028
 
2029
+ # Issue #1929 — re-ask for `class_name`'s definition on behalf of a PREVIOUS run's analysis, which
2030
+ # demanded it and found it failing. An `--incremental` recheck replays that run's failures, and one
2031
+ # whose conflicting buffer is a file the recheck re-synthesized cannot be replayed as it stands (the
2032
+ # edit may have fixed it), while the file that demanded the class may sit outside the recheck's
2033
+ # closure and never ask again. Re-asking here re-derives the answer over the current signatures: a
2034
+ # class that still fails is reported with its current tuple, one that now builds is not.
2035
+ #
2036
+ # A real demand, not {#during_internal_demand}: it stands in for the analysis's own, which is what
2037
+ # #696 binds the reported set to. Both sides are asked, since the replayed tuple does not record which
2038
+ # side the analysis demanded ({#report_definition_build_failure} builds both anyway).
2039
+ #
2040
+ # @return the class's current tuple, or nil when it now builds.
2041
+ def redemand_definition(class_name)
2042
+ key = class_name.to_s.delete_prefix("::")
2043
+ instance_definition(key)
2044
+ singleton_definition(key)
2045
+ definition_build_failures.find { |failure| failure.first == key }
2046
+ end
2047
+
1919
2048
  # Virtual (inline-synthesized) contributions dropped by the collision quarantine
1920
2049
  # ({.resolve_quarantining_virtual_collisions}): buffer names absent from the built env even though the
1921
2050
  # entry's content is non-empty and parses (a parse failure is the synthesizer's own WD6 skip, reported