rigortype 0.4.1 → 0.4.3

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 (230) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/11-sig-gen.md +18 -5
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/02-cli-reference.md +19 -1
  27. data/docs/manual/03-configuration.md +7 -5
  28. data/docs/manual/04-diagnostics.md +2 -1
  29. data/docs/manual/05-inspecting-types.md +50 -0
  30. data/docs/manual/06-baseline.md +2 -0
  31. data/docs/manual/07-plugins.md +1 -1
  32. data/docs/manual/12-caching.md +18 -1
  33. data/docs/manual/14-rails-quickstart.md +2 -2
  34. data/docs/manual/18-removing-dead-code.md +41 -4
  35. data/docs/manual/plugins/README.md +6 -1
  36. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  37. data/docs/manual/plugins/rigor-alba.md +60 -0
  38. data/docs/manual/plugins/rigor-minitest.md +22 -4
  39. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  40. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  41. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  42. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  43. data/lib/rigor/analysis/check_rules/rule_ids.rb +11 -0
  44. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  45. data/lib/rigor/analysis/check_rules.rb +292 -59
  46. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  47. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  48. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  49. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  50. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  51. data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
  52. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  53. data/lib/rigor/analysis/incremental.rb +29 -9
  54. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  55. data/lib/rigor/analysis/incremental_run_slot.rb +6 -4
  56. data/lib/rigor/analysis/incremental_session.rb +103 -23
  57. data/lib/rigor/analysis/path_expansion.rb +62 -6
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
  59. data/lib/rigor/analysis/reachability/graph.rb +241 -37
  60. data/lib/rigor/analysis/reachability/scan.rb +490 -77
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  64. data/lib/rigor/analysis/run_cache_probe.rb +1 -1
  65. data/lib/rigor/analysis/run_stats.rb +28 -6
  66. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +113 -6
  67. data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -19
  68. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  70. data/lib/rigor/analysis/runner.rb +62 -13
  71. data/lib/rigor/analysis/worker_session.rb +8 -5
  72. data/lib/rigor/cache/engine_source.rb +21 -7
  73. data/lib/rigor/cache/incremental_snapshot.rb +55 -5
  74. data/lib/rigor/cli/annotate_command.rb +5 -0
  75. data/lib/rigor/cli/baseline_command.rb +7 -18
  76. data/lib/rigor/cli/check_command.rb +140 -40
  77. data/lib/rigor/cli/docs_command.rb +5 -4
  78. data/lib/rigor/cli/lens_command.rb +144 -0
  79. data/lib/rigor/cli/lsp_command.rb +5 -2
  80. data/lib/rigor/cli/skill_command.rb +3 -3
  81. data/lib/rigor/cli/skill_describe.rb +1 -1
  82. data/lib/rigor/cli/trace_command.rb +5 -0
  83. data/lib/rigor/cli/type_of_command.rb +6 -3
  84. data/lib/rigor/cli/unused_command.rb +166 -27
  85. data/lib/rigor/cli.rb +39 -5
  86. data/lib/rigor/configuration/severity_profile.rb +6 -0
  87. data/lib/rigor/configuration.rb +61 -2
  88. data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
  89. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  90. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  91. data/lib/rigor/environment/installed_gem_set.rb +38 -11
  92. data/lib/rigor/environment/lockfile_resolver.rb +37 -6
  93. data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
  94. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  95. data/lib/rigor/environment/rbs_loader.rb +316 -14
  96. data/lib/rigor/environment/required_features.rb +208 -0
  97. data/lib/rigor/environment.rb +33 -8
  98. data/lib/rigor/inference/acceptance.rb +28 -3
  99. data/lib/rigor/inference/builtins/cruby_definers.rb +78 -0
  100. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  101. data/lib/rigor/inference/captured_locals.rb +43 -3
  102. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  103. data/lib/rigor/inference/expression_typer.rb +261 -29
  104. data/lib/rigor/inference/global_write_census.rb +7 -4
  105. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  106. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  107. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  108. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +286 -0
  109. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  110. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  111. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  112. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  113. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  114. data/lib/rigor/inference/match_rebinding.rb +1 -1
  115. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  116. data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
  117. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  118. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  119. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  120. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  121. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
  122. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  123. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
  124. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
  125. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  126. data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
  127. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  128. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +247 -28
  129. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  130. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
  131. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
  132. data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
  133. data/lib/rigor/inference/method_dispatcher.rb +96 -14
  134. data/lib/rigor/inference/module_function_state.rb +2 -1
  135. data/lib/rigor/inference/narrowing.rb +20 -6
  136. data/lib/rigor/inference/object_mixins.rb +180 -0
  137. data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
  138. data/lib/rigor/inference/project_patched_methods.rb +12 -7
  139. data/lib/rigor/inference/project_patched_scanner.rb +108 -26
  140. data/lib/rigor/inference/refine_census.rb +239 -0
  141. data/lib/rigor/inference/refine_self.rb +317 -0
  142. data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
  143. data/lib/rigor/inference/refined_dispatch/own_method.rb +475 -0
  144. data/lib/rigor/inference/refined_dispatch.rb +320 -0
  145. data/lib/rigor/inference/repeated_or_writes.rb +2 -0
  146. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  147. data/lib/rigor/inference/scope_indexer.rb +827 -65
  148. data/lib/rigor/inference/statement_evaluator.rb +294 -51
  149. data/lib/rigor/inference/string_mutation.rb +9 -3
  150. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  151. data/lib/rigor/inference/untyped_rejoin.rb +75 -0
  152. data/lib/rigor/inference/version_guard.rb +19 -10
  153. data/lib/rigor/language_server/project_context.rb +87 -5
  154. data/lib/rigor/language_server/server.rb +18 -0
  155. data/lib/rigor/lens/anchor.rb +115 -0
  156. data/lib/rigor/lens/contract_reader.rb +118 -0
  157. data/lib/rigor/lens/document.rb +113 -0
  158. data/lib/rigor/lens/file_reader.rb +433 -0
  159. data/lib/rigor/lens/members.rb +153 -0
  160. data/lib/rigor/lens/parameters.rb +91 -0
  161. data/lib/rigor/lens/plugins.rb +40 -0
  162. data/lib/rigor/lens/run.rb +280 -0
  163. data/lib/rigor/lens/slot.rb +79 -0
  164. data/lib/rigor/lens.rb +40 -0
  165. data/lib/rigor/plugin/inflector.rb +1 -1
  166. data/lib/rigor/plugin/io_boundary.rb +42 -3
  167. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  168. data/lib/rigor/project_environment.rb +48 -6
  169. data/lib/rigor/protection/mutation_cache.rb +3 -2
  170. data/lib/rigor/protection/mutator.rb +1 -1
  171. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  172. data/lib/rigor/reflection/constant_path.rb +13 -2
  173. data/lib/rigor/reflection.rb +40 -8
  174. data/lib/rigor/scope/discovery_index.rb +40 -1
  175. data/lib/rigor/scope/resolution_chain.rb +159 -11
  176. data/lib/rigor/scope.rb +211 -21
  177. data/lib/rigor/sig_gen/classification.rb +4 -1
  178. data/lib/rigor/sig_gen/generator.rb +100 -10
  179. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  180. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  181. data/lib/rigor/source/alias_names.rb +52 -0
  182. data/lib/rigor/source/literals.rb +3 -2
  183. data/lib/rigor/source/symbol_name.rb +20 -0
  184. data/lib/rigor/type/combinator.rb +34 -2
  185. data/lib/rigor/type/float_range.rb +4 -1
  186. data/lib/rigor/type/nominal.rb +6 -1
  187. data/lib/rigor/version.rb +1 -1
  188. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  189. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  190. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  191. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  192. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  193. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  194. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  195. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  196. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  197. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  198. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
  199. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
  200. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  201. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  202. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  203. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  204. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  205. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  206. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  207. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  208. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  209. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  210. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  211. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  212. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  213. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  214. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  215. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  216. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  217. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  218. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  219. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  220. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  221. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  222. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  223. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  224. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  225. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  226. data/sig/rigor/scope.rbs +30 -4
  227. data/sig/rigor/source.rbs +4 -0
  228. data/sig/rigor.rbs +9 -4
  229. data/skills/rigor-project-init/references/02-configure.md +2 -2
  230. metadata +83 -1
@@ -3,6 +3,7 @@
3
3
  require "digest"
4
4
 
5
5
  require_relative "environment/default_libraries"
6
+ require_relative "environment/required_features"
6
7
  require_relative "environment/class_registry"
7
8
  require_relative "environment/rbs_loader"
8
9
  require_relative "environment/reflection"
@@ -96,7 +97,7 @@ module Rigor
96
97
  # for the overlay, instead of aborting the run outright (see {#hkt_registry}).
97
98
  @hkt_scan_failure = FailureSlot.new
98
99
  @constant_type_cache = ConstantTypeCacheHolder.new
99
- # ADR-82 WD9 — `[gem_name, version]` pairs for the locked gems with no resolvable RBS. The
100
+ # ADR-82 WD9 — `[gem_name, version, variants]` entries for the locked gems with no resolvable RBS. The
100
101
  # root-constant ownership index over them is built lazily (first unresolved constant read) so runs
101
102
  # whose constants all resolve never pay the entry-file scan.
102
103
  @missing_rbs_gems = missing_rbs_gems.freeze
@@ -275,7 +276,7 @@ module Rigor
275
276
  bundler_lockfile: nil,
276
277
  rbs_collection_lockfile: nil, rbs_collection_auto_detect: false,
277
278
  synthetic_method_index: nil, project_patched_methods: nil,
278
- source_files: [], locked_gems: nil)
279
+ source_files: [], locked_gems: nil, required_features: nil)
279
280
  resolved_paths = project_signature_roots(signature_paths, root: root)
280
281
  # O4 MVP — append per-gem `sig/` directories discovered under the target project's bundler install
281
282
  # root. Empty array when neither an explicit path nor auto-detection finds a bundle. Order: user
@@ -314,8 +315,10 @@ module Rigor
314
315
  # and the `data/vendored_gem_sigs/` bundle (`redis`, `nokogiri`, `pg`, …). `skip_gem_names:` passes
315
316
  # both sets so the collection copy doesn't double-declare against rigor's bundled RBS (the
316
317
  # `RBS::DuplicatedDeclarationError` hazard).
317
- merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s)).uniq
318
- skip_gem_names = merged_libraries + RbsLoader.vendored_gem_names
318
+ # Issue #1700 — plus the gated vendored directories this run's source activates ({#feature_tokens}).
319
+ merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s) +
320
+ feature_tokens(required_features, source_files, libraries)).uniq
321
+ skip_gem_names = collection_skip_gem_names(merged_libraries)
319
322
  collection_paths = RbsCollectionDiscovery.discover(
320
323
  lockfile_path: rbs_collection_lockfile,
321
324
  project_root: root,
@@ -400,6 +403,21 @@ module Rigor
400
403
 
401
404
  private
402
405
 
406
+ # Issue #1700 — the `RequiredFeatures` library tokens for the gated vendored directories this run asks for:
407
+ # each feature its source requires and each one the configuration's `libraries:` names. `required_features`
408
+ # is the scan a caller already took; nil scans `source_files`. Whether a directory then loads is decided
409
+ # by `RbsLoader.build_env_for` against the environment it builds.
410
+ def feature_tokens(required_features, source_files, libraries)
411
+ RequiredFeatures.tokens(required_features || RequiredFeatures.scan(source_files), libraries)
412
+ end
413
+
414
+ # The gem names an `rbs collection` copy must not double-declare against: the library list and every
415
+ # vendored directory except a `RequiredFeatures`-gated one. A gated directory stands down for any other
416
+ # copy of its declarations (`GatedSignatureGuard`), so the collection's copy is kept and wins.
417
+ def collection_skip_gem_names(library_names)
418
+ library_names + RbsLoader.vendored_gem_names.reject { |name| RequiredFeatures.gated_dir?(name) }
419
+ end
420
+
403
421
  def default_signature_paths(root)
404
422
  sig = Pathname(root) / DEFAULT_PROJECT_SIG_DIR
405
423
  sig.directory? ? [sig] : []
@@ -426,8 +444,8 @@ module Rigor
426
444
  end
427
445
 
428
446
  # The `:missing` classification (locked gems with no RBS through any resolution path), computed once
429
- # and consumed twice: as `[gem_name, version]` pairs for the ADR-82 WD9 constant-ownership index, and
430
- # as the eligible-gem set for the ADR-72 overlay resolution. Returns `[pairs, overlay_paths]`.
447
+ # and consumed twice: as `[gem_name, version, variants]` entries for the ADR-82 WD9 constant-ownership
448
+ # index, and as the eligible-gem set for the ADR-72 overlay resolution. Returns `[entries, overlay_paths]`.
431
449
  def missing_gems_and_overlay_paths(locked:, default_libraries:, bundle_sig_paths:,
432
450
  rbs_collection_paths:, plugin_registry:, signature_paths: [],
433
451
  bundle_root: nil)
@@ -440,7 +458,7 @@ module Rigor
440
458
  missing_gem_names: rows.map(&:gem_name), plugin_registry: plugin_registry,
441
459
  signature_paths: signature_paths
442
460
  )
443
- [rows.map { |row| [row.gem_name, row.version] }, overlays]
461
+ [index_entries(rows, locked), overlays]
444
462
  end
445
463
 
446
464
  # Issue #530 — a project with no `Gemfile.lock` still has gems on disk, and before this it handed the
@@ -454,7 +472,14 @@ module Rigor
454
472
  return [[], []] if installed.empty?
455
473
 
456
474
  rows = missing_rows(installed, default_libraries, bundle_sig_paths, rbs_collection_paths)
457
- [rows.map { |row| [row.gem_name, row.version] }, []]
475
+ [index_entries(rows, installed), []]
476
+ end
477
+
478
+ # The constant-ownership index's input: each `:missing` row with every `[version, platform]` variant its
479
+ # gem was locked (or found installed) under, because Bundler installs the variant matching the host —
480
+ # `commonmarker-2.10.0-arm64-darwin/` under a lockfile whose representative is the `ruby` one (#1844).
481
+ def index_entries(rows, gems)
482
+ rows.map { |row| [row.gem_name, row.version, gems[row.gem_name]&.variants] }
458
483
  end
459
484
 
460
485
  def missing_rows(gems, default_libraries, bundle_sig_paths, rbs_collection_paths)
@@ -486,7 +486,17 @@ module Rigor
486
486
  )
487
487
  end
488
488
 
489
- nil
489
+ project_sequence_to_enumerator(self_type, other_type)
490
+ end
491
+
492
+ # `Enumerator::ArithmeticSequence[E]` carries only the element of its `Enumerator[Numeric, void]` superclass
493
+ # (#1794), so against a two-argument `Enumerator` it projects to `Enumerator[E, top]`, `top` being what RBS
494
+ # `void` translates to: `Enumerator[Integer, void]` and `[Integer, untyped]` accept it, as RBS would.
495
+ def project_sequence_to_enumerator(self_type, other_type)
496
+ return nil unless self_type.class_name == "Enumerator" && self_type.type_args.size == 2
497
+ return nil unless other_type.class_name == "Enumerator::ArithmeticSequence" && other_type.type_args.size == 1
498
+
499
+ Type::Combinator.nominal_of("Enumerator", type_args: [other_type.type_args.first, Type::Combinator.top])
490
500
  end
491
501
 
492
502
  def project_tuple_to_nominal(tuple)
@@ -963,11 +973,14 @@ module Rigor
963
973
  end
964
974
  end
965
975
 
966
- # Constant[v] accepts only Constant[v'] with structurally equal value. Any other type is rejected
967
- # (modulo the universal Bot/Dynamic short-circuits already applied upstream).
976
+ # Constant[v] accepts Constant[v'] with structurally equal value, and the plain nominal of a class whose
977
+ # only instance is `v`. Any other type is rejected (modulo the universal Bot/Dynamic short-circuits
978
+ # already applied upstream).
968
979
  def accepts_constant(self_type, other_type, mode)
969
980
  if other_type.is_a?(Type::Constant) && self_type == other_type
970
981
  Type::AcceptsResult.yes(mode: mode, reasons: "structural literal match")
982
+ elsif sole_instance_nominal?(self_type, other_type)
983
+ Type::AcceptsResult.yes(mode: mode, reasons: "#{other_type.class_name} has no instance but the literal")
971
984
  else
972
985
  Type::AcceptsResult.no(
973
986
  mode: mode,
@@ -976,6 +989,18 @@ module Rigor
976
989
  end
977
990
  end
978
991
 
992
+ # `true`, `false` and `nil` are the only instances of their classes, so `FalseClass` holds exactly the
993
+ # values `false` holds. Read apart, `Array.new(n, false)`, typed `Array[FalseClass]` through the
994
+ # `[T] (Integer, T)` binding, failed a declared `-> Array[bool]` and fired `def.return-type-mismatch` on
995
+ # correct code.
996
+ SOLE_INSTANCE_CLASSES = { true => "TrueClass", false => "FalseClass", nil => "NilClass" }.freeze
997
+ private_constant :SOLE_INSTANCE_CLASSES
998
+
999
+ def sole_instance_nominal?(constant, other_type)
1000
+ other_type.is_a?(Type::Nominal) && other_type.type_args.empty? &&
1001
+ SOLE_INSTANCE_CLASSES.key?(constant.value) && SOLE_INSTANCE_CLASSES[constant.value] == other_type.class_name
1002
+ end
1003
+
979
1004
  # Tuple[A1..An] accepts:
980
1005
  # - Tuple[B1..Bn] when arities match and each Ai accepts Bi (covariant per-position).
981
1006
  # - Anything else: no (we cannot prove the arity from a generic nominal alone).
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require_relative "method_catalog"
5
+
6
+ module Rigor
7
+ module Inference
8
+ module Builtins
9
+ # Issue #1740 — which instance methods CRuby itself defines on a core class or module, read from the offline
10
+ # catalogues under `data/builtins/ruby_core/` (`tool/extract_builtin_catalog.rb` records each `rb_define_method`
11
+ # and prelude `def` of an `Init_*` function on the class it names, aliases included). A registration inside an
12
+ # `#if 0` block CRuby keeps for RDoc carries `rdoc_only: true` and is left out: `random.c`'s registers
13
+ # `Random#seed` on `Random`, though it runs on `Random::Base`.
14
+ # `spec/rigor/inference/builtins/c_ruby_definers_spec.rb` checks every listed method the running Ruby has
15
+ # against its runtime owner. RBS is not that record: core RBS redeclares some inherited methods on a subclass
16
+ # (`Integer#quo` and `File#to_path`, whose CRuby owners are `Numeric` and `IO`), and declares a few CRuby no
17
+ # longer defines (`Process::Status#&`). A class no catalogue covers answers false, as does a method its
18
+ # catalogue does not list: the caller reads both as "not proven". The catalogue is one CRuby snapshot.
19
+ #
20
+ # Issue #1752 — the singleton side reads the catalogue's `singleton_methods` (each `rb_define_singleton_method`
21
+ # on the class), the record that `Integer.sqrt` is CRuby's own method of `Integer`'s singleton class.
22
+ module CRubyDefiners
23
+ module_function
24
+
25
+ # Does the catalogue prove CRuby defines `method_name` on `class_name` itself?
26
+ def defines?(class_name, method_name)
27
+ names = INDEX[class_name.to_s.delete_prefix("::")]
28
+ !names.nil? && names.include?(method_name.to_s)
29
+ end
30
+
31
+ # Does the catalogue prove CRuby defines `method_name` on `class_name`'s singleton class itself?
32
+ def singleton_defines?(class_name, method_name)
33
+ names = SINGLETON_INDEX[class_name.to_s.delete_prefix("::")]
34
+ !names.nil? && names.include?(method_name.to_s)
35
+ end
36
+
37
+ # `{class name => Set[method name]}` from each catalogued class's instance methods and aliases, or from its
38
+ # `singleton_methods` when `side` is `:singleton`.
39
+ def build_index(side = :instance)
40
+ index = Hash.new { |hash, key| hash[key] = Set.new }
41
+ MethodCatalog.topic_paths.each do |path|
42
+ data = YAML.safe_load_file(path, permitted_classes: [Symbol])
43
+ classes = data.is_a?(Hash) ? data["classes"] : nil
44
+ next unless classes.is_a?(Hash)
45
+
46
+ classes.each do |name, klass|
47
+ next if klass["rdoc_only"]
48
+
49
+ merge_names(index[name], klass, side)
50
+ end
51
+ end
52
+ Ractor.make_shareable(index.to_h { |name, names| [name, names.freeze] })
53
+ rescue Psych::SyntaxError, SystemCallError
54
+ Ractor.make_shareable({})
55
+ end
56
+
57
+ def merge_names(names, klass, side)
58
+ return names.merge(defined_names(klass["singleton_methods"])) if side == :singleton
59
+
60
+ names.merge(defined_names(klass["instance_methods"]))
61
+ names.merge(defined_names(klass["aliases"]))
62
+ end
63
+
64
+ # The names of `entries` CRuby defines at runtime: one the catalogue marks `rdoc_only` was registered only for
65
+ # RDoc, inside an `#if 0` block.
66
+ def defined_names(entries)
67
+ return [] unless entries.is_a?(Hash)
68
+
69
+ entries.filter_map { |method, entry| method unless entry.is_a?(Hash) && entry["rdoc_only"] }
70
+ end
71
+
72
+ INDEX = build_index
73
+ SINGLETON_INDEX = build_index(:singleton)
74
+ private_constant :INDEX, :SINGLETON_INDEX
75
+ end
76
+ end
77
+ end
78
+ end
@@ -48,6 +48,9 @@ module Rigor
48
48
  new(path: File.join(DATA_ROOT, "#{topic}.yml"), mutating_selectors: mutating_selectors)
49
49
  end
50
50
 
51
+ # Every catalogue file under {DATA_ROOT}, in a stable order.
52
+ def self.topic_paths = Dir[File.join(DATA_ROOT, "*.yml")]
53
+
51
54
  def initialize(path:, mutating_selectors: {})
52
55
  @path = path
53
56
  @mutating_selectors = mutating_selectors.transform_values(&:freeze).freeze
@@ -310,23 +310,63 @@ module Rigor
310
310
 
311
311
  # The mutation sites under `body` whose reads name a variable {.captured_target?} accepts, keyed by name.
312
312
  # The block yields the names the enclosing construct introduces, asked only when a local site is found.
313
+ #
314
+ # A site whose receiver is a body local that aliases a capture (`l2 = log; l2 << e`) mutates that capture's
315
+ # object, so it is attributed to the capture as well ({.aliased_captures}): left out, the next iteration
316
+ # read `log` at its entry `[]` and `[1, 2].inject(0) { |a, e| r = a + log.size; l2 = log; l2 << e; r }`
317
+ # folded to `0` where Ruby answers `1` (issue #1332).
313
318
  def body_mutation_sites(body, base_scope, non_locals, &)
314
319
  sites = nil
315
320
  evaluator = nil
321
+ aliases = nil
316
322
  Source::NodeWalker.each_with_ancestors(body) do |descendant, ancestors|
317
323
  # A callee is resolved in the call-site scope, as the straight-line callee floor resolves it.
318
324
  site = mutation_site(descendant) { evaluator ||= StatementEvaluator.new(scope: base_scope) }
319
325
  next if site.nil?
320
326
 
321
327
  site_reads(site).each do |read|
322
- next unless captured_target?(read, ancestors, base_scope, non_locals, &)
323
-
324
- ((sites ||= {})[read.name] ||= []) << site
328
+ targets = captured_target?(read, ancestors, base_scope, non_locals, &) ? [read] : []
329
+ if read.is_a?(Prism::LocalVariableReadNode)
330
+ aliases ||= local_aliases(body)
331
+ targets.concat(aliased_captures(read.name, aliases, base_scope, non_locals, Set.new, &))
332
+ end
333
+ targets.each { |target| ((sites ||= {})[target.name] ||= []) << site }
325
334
  end
326
335
  end
327
336
  sites || NO_SITES
328
337
  end
329
338
 
339
+ # `{ name => [[read, ancestors], ...] }`: every variable read a local write under `body` may store
340
+ # ({ReceiverAlias.candidates} of its value), with the ancestors {.captured_target?} resolves it against.
341
+ def local_aliases(body)
342
+ aliases = {}
343
+ Source::NodeWalker.each_with_ancestors(body) do |node, ancestors|
344
+ next unless node.is_a?(Prism::LocalVariableWriteNode) || node.is_a?(Prism::LocalVariableOrWriteNode)
345
+
346
+ ReceiverAlias.candidates(node.value).each do |read|
347
+ (aliases[node.name] ||= []) << [read, ancestors + [node]]
348
+ end
349
+ end
350
+ aliases
351
+ end
352
+
353
+ # The captured reads the local `name` may alias, followed through local-to-local copies (`l3 = l2 = log`).
354
+ # It over-approximates — a write in any branch, or in a nested scope reusing the name, counts — which only
355
+ # widens a capture's binding.
356
+ def aliased_captures(name, aliases, base_scope, non_locals, visited, &)
357
+ return [] unless visited.add?(name)
358
+
359
+ aliases.fetch(name, NO_NAMES).flat_map do |read, ancestors|
360
+ if captured_target?(read, ancestors, base_scope, non_locals, &)
361
+ [read]
362
+ elsif read.is_a?(Prism::LocalVariableReadNode)
363
+ aliased_captures(read.name, aliases, base_scope, non_locals, visited, &)
364
+ else
365
+ NO_NAMES
366
+ end
367
+ end
368
+ end
369
+
330
370
  # The mutation site `node` is — the node itself, or a {UnknownStoreWidening::CalleeStore} for a self-call
331
371
  # whose callee content-mutates a local it is passed — or nil. A self-call is resolved by the
332
372
  # `StatementEvaluator` the block yields, which the caller builds only when one is needed. A mutator name
@@ -33,7 +33,7 @@ module Rigor
33
33
  # ({.rehydrated_nesting}).
34
34
  def self.with_run
35
35
  previous = Thread.current[MEMO_KEY]
36
- Thread.current[MEMO_KEY] = { nodes: {}, indexes: {}, nestings: {}.compare_by_identity }
36
+ Thread.current[MEMO_KEY] = { nodes: {}, indexes: {}, nestings: {}.compare_by_identity, refinement_queries: {} }
37
37
  yield
38
38
  ensure
39
39
  Thread.current[MEMO_KEY] = previous
@@ -113,6 +113,28 @@ module Rigor
113
113
  end
114
114
  private_class_method :record_nesting
115
115
 
116
+ # Issue #1664 — the {InEffectRefinements} over `path`'s own parse, once a run, or nil when the file cannot be
117
+ # read: the typed refined arm reads another file's refine body through it. The bodies' `Module.nesting`
118
+ # is recorded as a handle's is, so the re-typed body reads its constants where it is written.
119
+ def self.refinement_query(path)
120
+ memo = Thread.current[MEMO_KEY]
121
+ return build_refinement_query(path, nil) if memo.nil?
122
+
123
+ queries = memo[:refinement_queries]
124
+ return queries[path] if queries.key?(path)
125
+
126
+ queries[path] = build_refinement_query(path, memo)
127
+ end
128
+
129
+ def self.build_refinement_query(path, memo)
130
+ query = InEffectRefinements.new(Prism.parse(File.read(path)).value)
131
+ query.refine_def_nestings.each { |node, nesting| record_nesting(memo, node, nesting) } if memo
132
+ query
133
+ rescue StandardError
134
+ nil
135
+ end
136
+ private_class_method :build_refinement_query
137
+
116
138
  # Finds the node for `handle` using a per-file `{node_id => DefNode}` + `{name => DefNode}` index cache.
117
139
  def self.locate(handle, index_cache)
118
140
  index = index_cache[handle.path] ||= build_file_index(handle.path)