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
@@ -22,6 +22,9 @@ module Rigor
22
22
  # the effect classifier and the effect catalogue's `mutators: string` cite it rather than keep a list of their
23
23
  # own (ADR-103 WD3). A spec holds it to every bang method `String` defines plus the non-bang mutators reflection
24
24
  # cannot find by name.
25
+ #
26
+ # The last row is Ruby 4.1's bit operations (#1691), which `data/core_overlay/string.rbs` declares whatever Ruby
27
+ # runs the analysis; they rewrite bytes in place and keep the byte length.
25
28
  MUTATORS = %i[
26
29
  << concat insert prepend replace clear []= slice!
27
30
  setbyte bytesplice append_as_bytes force_encoding
@@ -29,18 +32,21 @@ module Rigor
29
32
  upcase! downcase! capitalize! swapcase! reverse!
30
33
  strip! lstrip! rstrip! chomp! chop!
31
34
  delete_prefix! delete_suffix! encode! scrub! unicode_normalize!
35
+ bit_set bit_clear bit_flip bitwise_not! bitwise_and! bitwise_or! bitwise_xor!
32
36
  ].to_set.freeze
33
37
 
34
38
  # The {MUTATORS} that CANNOT leave a non-empty receiver empty, and so keep a `non-empty-string` refinement:
35
39
  # appenders (`<<`, `concat`, `insert`, `prepend`, `append_as_bytes`), and rewrites that keep at least one
36
40
  # character (`setbyte`, `force_encoding`, the case mappings, `reverse!`, `succ!` / `next!`, `squeeze!`,
37
- # `unicode_normalize!`). A whitelist, as `RefinementMutation::EMPTY_PRESERVING` is for Array and Hash: a mutator
38
- # missing here retracts the witness, which costs precision, where a mutator missing from a list of emptiers kept
39
- # the witness of a string it had just emptied — `tr!` and `tr_s!` did, since `"a".tr!("a", "")` is `""`.
41
+ # `unicode_normalize!`, and the bit operations, which keep every byte). A whitelist, as
42
+ # `RefinementMutation::EMPTY_PRESERVING` is for Array and Hash: a mutator missing here retracts the witness, which
43
+ # costs precision, where a mutator missing from a list of emptiers kept the witness of a string it had just
44
+ # emptied — `tr!` and `tr_s!` did, since `"a".tr!("a", "")` is `""`.
40
45
  EMPTY_PRESERVING = %i[
41
46
  << concat insert prepend append_as_bytes
42
47
  setbyte force_encoding
43
48
  upcase! downcase! capitalize! swapcase! reverse! succ! next! squeeze! unicode_normalize!
49
+ bit_set bit_clear bit_flip bitwise_not! bitwise_and! bitwise_or! bitwise_xor!
44
50
  ].to_set.freeze
45
51
 
46
52
  # `tr!` / `tr_s!` map every character they match to one character of the replacement (padded with its last), so
@@ -333,7 +333,8 @@ module Rigor
333
333
 
334
334
  def collect_included_do_calls(body)
335
335
  body.body.flat_map do |stmt|
336
- next [] unless stmt.is_a?(Prism::CallNode) && stmt.receiver.nil? && stmt.name == :included && stmt.block
336
+ included_do = stmt.is_a?(Prism::CallNode) && stmt.receiver.nil? && stmt.name == :included
337
+ next [] unless included_do && stmt.block.is_a?(Prism::BlockNode)
337
338
 
338
339
  block_body = stmt.block.body
339
340
  next [] unless block_body.respond_to?(:body)
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../type"
4
+ require_relative "../source/node_walker"
5
+ require_relative "captured_locals"
6
+
7
+ module Rigor
8
+ module Inference
9
+ # Issue #1840 — a branch merge that re-joins the narrowed edges of an untyped local gives it back its
10
+ # untyped binding.
11
+ #
12
+ # Flow narrowing concretizes a `Dynamic[top]` receiver (ADR-83): `x.nil?` splits it into `nil` and
13
+ # `Dynamic[top]`, and `when String` into `String` and `Dynamic[top]`. `Scope#join` unions the edges, so
14
+ # after both merges the local reads `Dynamic[top] | String | nil`. That union is not wrong as a set, but
15
+ # every member is a narrowing of the one untyped value, so it says nothing the pre-branch binding did
16
+ # not. It does, however, hold `nil` beside a nameable class, and `call.possible-nil-receiver` reports
17
+ # `x + "a"` on correct code although neither narrowing alone did.
18
+ #
19
+ # The rule: at a merge, a local whose pre-branch binding is exactly `Dynamic[top]`, that no merged arm
20
+ # writes, and whose joined binding is a union still carrying a `Dynamic` member, is bound to
21
+ # `Dynamic[top]` again.
22
+ #
23
+ # - The write test keeps a nil the program puts there: `x = nil if c` writes `x`, so the join stays
24
+ # `Dynamic[top] | nil` and still reports. It is syntactic, over every local-write form under the arms
25
+ # (a pattern capture and a block's write to the outer local included), so a doubtful arm keeps today's
26
+ # join. It does not see a written nil that a `nil?` edge stripped before the merge (`x = nil if c; if
27
+ # x.nil? … elsif x.is_a?(String) … end`): that merge's entry is exactly `Dynamic[top]`, and the outer
28
+ # join's `Dynamic[top] | nil` is one the possible-nil gate declines anyway (#1876).
29
+ # - The `Dynamic`-member test keeps a narrowing whose surviving arms no longer cover the untyped value:
30
+ # `case x when String then … when Integer then … else raise end` leaves `Integer | String`, which is
31
+ # what `x` is past it.
32
+ # - Only the `Dynamic[top]` facet: a faceted `Dynamic[T]` carries static knowledge of its own (a
33
+ # plugin's or a signature's `T`), so a `nil` its narrowing leaves may be one the facet promised. Those
34
+ # keep today's join.
35
+ #
36
+ # Restoring `Dynamic[top]` is always sound, since every narrowed edge is a subset of it; what it drops
37
+ # is only the nameable arms, which an untyped union never needed to dispatch.
38
+ module UntypedRejoin
39
+ module_function
40
+
41
+ # `joined` with each qualifying local rebound to its `entry` binding. `arms` are the nodes whose
42
+ # evaluation produced the merged scopes (`nil` entries are skipped).
43
+ def apply(joined, entry, arms)
44
+ candidates = nil
45
+ joined.locals.each do |name, type|
46
+ next unless candidate?(entry.locals[name], type)
47
+
48
+ (candidates ||= []) << name
49
+ end
50
+ return joined if candidates.nil?
51
+
52
+ written = written_locals(arms)
53
+ kept = candidates.reject { |name| written.include?(name) }
54
+ return joined if kept.empty?
55
+
56
+ joined.with_rejoined_locals(kept.to_h { |name| [name, entry.locals[name]] })
57
+ end
58
+
59
+ def candidate?(entry_type, joined_type)
60
+ entry_type.is_a?(Type::Dynamic) && entry_type.static_facet.is_a?(Type::Top) &&
61
+ joined_type.is_a?(Type::Union) && joined_type.members.any?(Type::Dynamic)
62
+ end
63
+
64
+ def written_locals(arms)
65
+ names = Set.new
66
+ arms.each do |arm|
67
+ Source::NodeWalker.each(arm) do |node|
68
+ names << node.name if CapturedLocals::LOCAL_WRITE_NODES.include?(node.class)
69
+ end
70
+ end
71
+ names
72
+ end
73
+ end
74
+ end
75
+ end
@@ -33,6 +33,10 @@ module Rigor
33
33
  # Prism *parse* version (it decides which syntax is accepted), it is not threaded to the inference
34
34
  # layer, and its default (`"4.0"`) is a default rather than a user statement about the runtime.
35
35
  #
36
+ # The one exception is the Ruby-deprecation rule (the ADR-47 WD5 amendment, #1692). An explicit
37
+ # `target_ruby` is a user statement about the runtime, and that rule passes it as `stated_ruby:` to ask
38
+ # whether a deprecated call can run on the stated Ruby. Narrowing and the dead-arm filter never pass it.
39
+ #
36
40
  # ## What is folded (and what is deliberately not)
37
41
  #
38
42
  # * `RUBY_VERSION <cmp> "x.y.z"` — compared with **String** semantics, because that is what runs.
@@ -91,9 +95,13 @@ module Rigor
91
95
  private_constant :GEM_VERSION_PATH
92
96
 
93
97
  # @param node — an `if` / `unless` predicate
94
- # @return `:truthy` / `:falsey` when the guard is decidable on the analyzer's Ruby,
98
+ # @param stated_ruby — ADR-47 WD5 amendment (#1692): a `x.y.z` version to read `RUBY_VERSION` as instead of
99
+ # the analyzer's own, for the Ruby-deprecation rules only ({Configuration#stated_runtime_ruby}). Under it
100
+ # nothing else is readable — `RUBY_ENGINE` and the default gems' `VERSION`s belong to the analyzer's
101
+ # interpreter, not to the stated one — so a guard on them answers nil.
102
+ # @return `:truthy` / `:falsey` when the guard is decidable on the analyzer's Ruby (or on `stated_ruby`),
95
103
  # otherwise nil (both arms stay live)
96
- def verdict(node)
104
+ def verdict(node, stated_ruby: nil)
97
105
  return nil unless node.is_a?(Prism::CallNode)
98
106
 
99
107
  operator = node.name
@@ -103,19 +111,19 @@ module Rigor
103
111
  arguments = node.arguments&.arguments
104
112
  return nil unless arguments && arguments.length == 1
105
113
 
106
- left = read_operand(node.receiver)
107
- right = read_operand(arguments.first)
114
+ left = read_operand(node.receiver, stated_ruby)
115
+ right = read_operand(arguments.first, stated_ruby)
108
116
  return nil if left.nil? || right.nil?
109
117
 
110
118
  decide(left, right, operator)
111
119
  end
112
120
 
113
121
  # Reads one side of the comparison into `[kind, value]`, or nil when it is not readable.
114
- def read_operand(node)
122
+ def read_operand(node, stated_ruby = nil)
115
123
  case node
116
124
  when Prism::StringNode then [:literal_string, node.unescaped]
117
- when Prism::ConstantReadNode, Prism::ConstantPathNode then read_constant(node)
118
- when Prism::CallNode then read_gem_version(node)
125
+ when Prism::ConstantReadNode, Prism::ConstantPathNode then read_constant(node, stated_ruby)
126
+ when Prism::CallNode then read_gem_version(node, stated_ruby)
119
127
  end
120
128
  end
121
129
  private_class_method :read_operand
@@ -129,9 +137,10 @@ module Rigor
129
137
  #
130
138
  # The foldable set is unchanged and stays closed: an unqualified name folds only when it is one of
131
139
  # {PREDEFINED}, a qualified one only when it is in {VERSION_CONSTANTS}.
132
- def read_constant(node)
140
+ def read_constant(node, stated_ruby = nil)
133
141
  path = Source::ConstantPath.qualified_name_or_nil(node)
134
142
  return nil unless path
143
+ return (path == "RUBY_VERSION" ? [:string, stated_ruby] : nil) if stated_ruby
135
144
 
136
145
  kind =
137
146
  if path.include?("::")
@@ -149,14 +158,14 @@ module Rigor
149
158
  # `Gem::Version.new(<readable>)`. The inner operand must be a plain version String — an engine name
150
159
  # is not a version, and `Gem::Version.new` raises on anything `Gem::Version.correct?` rejects, so a
151
160
  # malformed literal keeps both arms live instead of folding a guard the program cannot even reach.
152
- def read_gem_version(node)
161
+ def read_gem_version(node, stated_ruby = nil)
153
162
  return nil unless node.name == :new && node.block.nil?
154
163
  return nil unless Source::ConstantPath.qualified_name_or_nil(node.receiver) == GEM_VERSION_PATH
155
164
 
156
165
  arguments = node.arguments&.arguments
157
166
  return nil unless arguments && arguments.length == 1
158
167
 
159
- inner = read_operand(arguments.first)
168
+ inner = read_operand(arguments.first, stated_ruby)
160
169
  return nil unless inner && STRING_KINDS.include?(inner.first)
161
170
  return nil unless ::Gem::Version.correct?(inner.last)
162
171
 
@@ -6,6 +6,7 @@ require_relative "../cache/store"
6
6
  require_relative "../analysis/runner"
7
7
  require_relative "../analysis/incremental_session"
8
8
  require_relative "../cache/incremental_snapshot"
9
+ require_relative "../effects/identity"
9
10
 
10
11
  module Rigor
11
12
  module LanguageServer
@@ -24,6 +25,10 @@ module Rigor
24
25
  # are small (the per-publish path is already ≈2ms once Environment is warm).
25
26
  #
26
27
  # Invalidation:
28
+ # - Every whole-project round (`#project_diagnostics`) re-reads the configuration and compares the
29
+ # incremental snapshot fingerprint and the `effects:` identity with the ones its session was primed with,
30
+ # re-baselining on any change — so no file watcher or `didChangeConfiguration` is needed for
31
+ # `.rigor.yml`, `sig/`, `pre_eval:` or lockfile edits (#1613).
27
32
  # - `#invalidate!` drops the cached environment AND project scan + bumps the generation counter; the next
28
33
  # reader rebuilds. Watched-file changes (`workspace/didChangeWatchedFiles`) and configuration refreshes
29
34
  # (`workspace/didChangeConfiguration`) both trigger this — the next publish observes the new project
@@ -44,8 +49,16 @@ module Rigor
44
49
  # invalidated never reach the editor.
45
50
  attr_reader :configuration, :generation
46
51
 
47
- def initialize(configuration:)
52
+ # `configuration_loader` re-reads the configuration (e.g. `-> { Configuration.load(path) }`); a save round
53
+ # calls it so an edit to `.rigor.yml` (or a file it `includes:`) reaches the server without a restart. Nil
54
+ # keeps the configuration the context was built with.
55
+ def initialize(configuration:, configuration_loader: nil)
48
56
  @configuration = configuration
57
+ @configuration_loader = configuration_loader
58
+ @primed_key = nil
59
+ @buffer_sources = {}
60
+ @disk_feature_files = nil
61
+ @environment_features = nil
49
62
  @generation = 0
50
63
  @environment = nil
51
64
  @cache_store = nil
@@ -77,6 +90,9 @@ module Rigor
77
90
  dependency_source_index: project_scan.dependency_source_index,
78
91
  synthetic_method_index: project_scan.synthetic_method_index,
79
92
  project_patched_methods: project_scan.project_patched_methods,
93
+ # Issue #1700 — the configured paths' files overlaid with the open buffers; {#note_buffers}
94
+ # rebuilds this when an edit or a save changes the set.
95
+ required_features: (@environment_features = required_features),
80
96
  **ProjectEnvironment.dependency_discovery_options(@configuration)
81
97
  )
82
98
  end
@@ -104,38 +120,104 @@ module Rigor
104
120
  # The session is never persisted. Its state lives as long as this context, which is what a long-running
105
121
  # server needs, and writing it would race exactly the way the read-only {#cache_store} exists to avoid.
106
122
  # Seeding is therefore one-directional: the terminal warms the server, not the reverse.
123
+ #
124
+ # Every round recomputes {Cache::IncrementalSnapshot.fingerprint} (the function the CLI's `--incremental`
125
+ # keys its snapshot on — one input list) and compares it with the one the session was primed with. A
126
+ # session's recheck only sees source-file digests, so an edit to a `pre_eval:` file, an auto-detected
127
+ # `sig/*.rbs`, a lockfile or `.rigor.yml` would otherwise keep serving answers computed from the old
128
+ # inputs until the server restarts (#1613). A change — including a fingerprint that newly cannot be
129
+ # computed — drops the session and the world built from the old inputs and re-baselines, so the round equals
130
+ # a cold run. The `effects:` identity rides along because `Configuration#to_h` omits `effects:`, so the
131
+ # fingerprint alone does not move on an `effects:` edit. A fingerprint that was ALREADY uncomputable at
132
+ # priming is not a change: that session keeps rechecking.
133
+ # {#generation} is deliberately NOT bumped: the round that notices the change is itself computed from the
134
+ # new inputs, and the publisher discards a round whose generation moved.
107
135
  def project_diagnostics
136
+ reload_configuration
137
+ fingerprint = current_fingerprint
138
+ key = [fingerprint, Effects::Identity.digest(configuration: @configuration)]
139
+ rebaseline if @session_primed && key != @primed_key
108
140
  session = (@incremental_session ||= build_incremental_session)
109
141
  return session.recheck.diagnostics if @session_primed
110
142
 
111
143
  @session_primed = true
144
+ @primed_key = key
112
145
  diagnostics, = session.run_incremental(
113
146
  snapshot: Cache::IncrementalSnapshot.new(root: @configuration.cache_path),
114
- fingerprint: Cache::IncrementalSnapshot.fingerprint(
115
- configuration: @configuration, roots: @configuration.paths
116
- ),
147
+ fingerprint: fingerprint,
117
148
  persist: false
118
149
  )
119
150
  diagnostics
120
151
  end
121
152
 
153
+ # Issue #1700 — records the open buffers' text (absolute path => source) and rebuilds the environment
154
+ # when it changes the {Environment::RequiredFeatures} set the current one was built with: a buffer that
155
+ # gains or drops a `require "prime"` loads or unloads the vendored `prime` signatures. The server calls
156
+ # it on every open, change, save and close; neither `didSave` nor a `.rb` edit reaches
157
+ # `workspace/didChangeWatchedFiles`, which most clients send only for the files they were asked to watch.
158
+ # The configured paths are read once per environment and only the buffers are re-scanned, so a keystroke
159
+ # costs a substring test per open buffer.
160
+ def note_buffers(sources)
161
+ @buffer_sources = sources
162
+ return if @environment.nil? || required_features == @environment_features
163
+
164
+ invalidate!
165
+ end
166
+
167
+ # The required-feature set for the configured paths' files on disk, each open buffer read in place of its
168
+ # file.
169
+ def required_features
170
+ disk = (@disk_feature_files ||= Environment::RequiredFeatures.feature_files(@configuration))
171
+ found = disk.reject { |path, _| @buffer_sources.key?(path) }.values.flatten
172
+ found += @buffer_sources.values.flat_map { |source| Environment::RequiredFeatures.features_of(source) }
173
+ found.uniq.sort.freeze
174
+ end
175
+
122
176
  # Drops every cached collaborator and bumps the generation. The next reader rebuilds from scratch.
123
177
  # Triggered by `workspace/didChangeWatchedFiles` for project source files and by
124
178
  # `workspace/didChangeConfiguration`.
125
179
  def invalidate!
126
180
  @generation += 1
181
+ reset_world
182
+ end
183
+
184
+ private
185
+
186
+ # Drops what was built from the previous inputs, without touching {#generation}.
187
+ def reset_world
127
188
  @environment = nil
189
+ @environment_features = nil
190
+ @disk_feature_files = nil
128
191
  @project_scan = nil
129
192
  # The session's per-file cache was computed against the old environment / project scan, so it cannot
130
193
  # outlive them. The next save round primes a fresh one.
131
194
  @incremental_session = nil
132
195
  @session_primed = false
196
+ @primed_key = nil
133
197
  # Cache store stays — it's content-addressed; a stale env build won't be served because the file
134
198
  # digest mixed into the cache key has changed.
135
199
  nil
136
200
  end
137
201
 
138
- private
202
+ # A changed configuration may also move `cache_path`, so the store goes too (unlike {#invalidate!}).
203
+ def rebaseline
204
+ reset_world
205
+ @cache_store = nil
206
+ end
207
+
208
+ def current_fingerprint
209
+ Cache::IncrementalSnapshot.fingerprint(configuration: @configuration, roots: @configuration.paths)
210
+ end
211
+
212
+ # A load that raises (a half-typed `.rigor.yml`) keeps the previous configuration: the editor must not
213
+ # lose its diagnostics to a transient parse error, and the next round retries.
214
+ def reload_configuration
215
+ return unless @configuration_loader
216
+
217
+ @configuration = @configuration_loader.call
218
+ rescue StandardError
219
+ nil
220
+ end
139
221
 
140
222
  # The session shares this context's warm Environment, so a round does not rebuild the RBS universe. It
141
223
  # deliberately does NOT share the prebuilt ProjectScan: a round runs after a save, i.e. after the file
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../version"
4
4
  require_relative "buffer_table"
5
+ require_relative "uri"
5
6
 
6
7
  module Rigor
7
8
  module LanguageServer
@@ -221,6 +222,7 @@ module Rigor
221
222
  bytes: doc.fetch(:text),
222
223
  version: doc.fetch(:version)
223
224
  )
225
+ note_buffers
224
226
  @publisher&.publish_for(uri)
225
227
  nil
226
228
  end
@@ -242,6 +244,7 @@ module Rigor
242
244
  changes: changes,
243
245
  version: doc.fetch(:version)
244
246
  )
247
+ note_buffers
245
248
  @publisher&.publish_for(uri)
246
249
  nil
247
250
  end
@@ -257,6 +260,7 @@ module Rigor
257
260
  def handle_did_save(params)
258
261
  uri = params.fetch(:textDocument).fetch(:uri)
259
262
  @buffer_table.save(uri: uri)
263
+ note_buffers
260
264
  @publisher&.publish_project(uri)
261
265
  nil
262
266
  end
@@ -361,10 +365,24 @@ module Rigor
361
365
  doc = params.fetch(:textDocument)
362
366
  uri = doc.fetch(:uri)
363
367
  @buffer_table.close(uri: uri)
368
+ note_buffers
364
369
  @publisher&.publish_empty(uri)
365
370
  nil
366
371
  end
367
372
 
373
+ # Issue #1700 — hands the open Ruby buffers to the project context, which rebuilds its environment when
374
+ # they change the set of required features the vendored signatures are gated on.
375
+ def note_buffers
376
+ return unless @project_context.respond_to?(:note_buffers)
377
+
378
+ sources = @buffer_table.uris.each_with_object({}) do |uri, acc|
379
+ path = Uri.to_path(uri)
380
+ entry = @buffer_table[uri]
381
+ acc[File.expand_path(path)] = entry.bytes if path&.end_with?(".rb") && entry
382
+ end
383
+ @project_context.note_buffers(sources)
384
+ end
385
+
368
386
  def method_not_found(method)
369
387
  rpc_error(ERROR_METHOD_NOT_FOUND, "method not found: #{method.inspect}")
370
388
  end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../hashing/xxh3"
4
+
5
+ module Rigor
6
+ module Lens
7
+ # A site's anchor and contract-comment span (docs/internal-spec/lens.md § Anchors). The hashed material is the
8
+ # contract comment lines above the declaration (`#:`, `@rbs`, `@extrbs` and their continuation lines), the
9
+ # declaration's own bytes, and a `#:` comment trailing its last line. Documentation prose is not part of it.
10
+ class Anchor
11
+ Result = Data.define(:token, :line, :contract_comments)
12
+
13
+ # rbs-inline's annotation grammar (`#:`, `#[`, `#\s*@rbs`) plus `@extrbs`, matched from the `#`.
14
+ CONTRACT_COMMENT = /\A#(?::|\[|\s*@rbs\b|\s*@extrbs\b)/
15
+ # A `#:nodoc:`-style RDoc directive, which the rbs-inline plugin neutralises rather than reads.
16
+ RDOC_DIRECTIVE = /\A#:[a-z_][\w-]*:/
17
+ # A kept line that later comment lines may continue, with the spaces between its `#` and the tag.
18
+ CONTINUED_TAG = /\A#( *)@(?:rbs|extrbs)\b/
19
+
20
+ # @param source — the file's bytes (binary String)
21
+ # @param comments — the file's `Prism::Comment` list
22
+ def initialize(source, comments)
23
+ @source = source.b
24
+ @line_starts = [0]
25
+ @source.each_byte.with_index { |byte, index| @line_starts << (index + 1) if byte == 10 }
26
+ @comment_by_line = {}
27
+ comments.each do |comment|
28
+ next unless comment.is_a?(Prism::InlineComment)
29
+
30
+ @comment_by_line[comment.location.start_line] = comment
31
+ end
32
+ end
33
+
34
+ # The anchor of the declaration spanning bytes `start_offset...end_offset` and lines `start_line..end_line`.
35
+ def for_span(start_offset, end_offset, start_line, end_line)
36
+ leading = leading_contract_comments(start_line)
37
+ trailing = trailing_contract_comment(end_line, end_offset)
38
+ material = +"".b
39
+ leading.each { |comment| material << line_text_from(comment.location.start_offset) << "\n" }
40
+ material << @source.byteslice(start_offset, end_offset - start_offset)
41
+ material << "\n" << line_text_from(trailing.location.start_offset) if trailing
42
+ line = leading.empty? ? start_line : leading.first.location.start_line
43
+ Result.new(token: "#{line}:#{Hashing::XXH3.anchor_hash(material)}", line: line,
44
+ contract_comments: leading + [trailing].compact)
45
+ end
46
+
47
+ private
48
+
49
+ # The contract comments among the contiguous comment-only lines directly above `start_line`, in file order,
50
+ # with rbs-inline's continuation lines below a kept `@rbs` / `@extrbs` line.
51
+ def leading_contract_comments(start_line)
52
+ run = []
53
+ line = start_line - 1
54
+ while line >= 1 && (comment = comment_only_line(line))
55
+ run.unshift(comment)
56
+ line -= 1
57
+ end
58
+ kept = []
59
+ tag_indent = nil
60
+ run.each do |comment|
61
+ text = comment_text(comment)
62
+ if tag_indent && hash_indent(text) > tag_indent
63
+ kept << comment
64
+ next
65
+ end
66
+
67
+ tag_indent = nil
68
+ next unless contract_comment?(text)
69
+
70
+ kept << comment
71
+ continued = text.match(CONTINUED_TAG)
72
+ tag_indent = continued && continued[1].size
73
+ end
74
+ kept
75
+ end
76
+
77
+ def trailing_contract_comment(end_line, end_offset)
78
+ comment = @comment_by_line[end_line]
79
+ return nil if comment.nil? || comment.location.start_offset < end_offset
80
+
81
+ text = comment_text(comment)
82
+ contract_comment?(text) && text.start_with?("#:") ? comment : nil
83
+ end
84
+
85
+ def contract_comment?(text)
86
+ text.match?(CONTRACT_COMMENT) && !text.match?(RDOC_DIRECTIVE)
87
+ end
88
+
89
+ # The spaces between a comment's `#` and its first non-space byte.
90
+ def hash_indent(text)
91
+ text[/\A#( *)/, 1].size
92
+ end
93
+
94
+ # The comment on `line` when nothing but whitespace precedes it.
95
+ def comment_only_line(line)
96
+ comment = @comment_by_line[line]
97
+ return nil if comment.nil?
98
+
99
+ prefix = @source.byteslice(@line_starts[line - 1], comment.location.start_offset - @line_starts[line - 1])
100
+ prefix.match?(/\A[ \t]*\z/) ? comment : nil
101
+ end
102
+
103
+ # The comment's text from its `#`, without a line terminator.
104
+ def comment_text(comment)
105
+ comment.location.slice.b.chomp.delete_suffix("\r")
106
+ end
107
+
108
+ # The bytes from `offset` to the end of its line, without the line terminator.
109
+ def line_text_from(offset)
110
+ line_end = @source.index("\n", offset) || @source.bytesize
111
+ @source.byteslice(offset, line_end - offset).delete_suffix("\r")
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "slot"
4
+ require_relative "../reflection"
5
+
6
+ module Rigor
7
+ module Lens
8
+ # Reads a member's own RBS declaration into overloads with per-slot provenance (docs/internal-spec/lens.md
9
+ # § Members). A declaration the member inherits is not its contract; one written for its own class is, whether
10
+ # it comes from `sig/`, an inline comment, a plugin or a library.
11
+ class ContractReader
12
+ INFERRED_RETURN = "rigor:v1:inferred-return"
13
+ INFERRED_SIGNATURE = "rigor:v1:inferred-signature"
14
+
15
+ # `overloads` hold `{rbs, params, block, return}`; a return the contract leaves to inference is nil there,
16
+ # with `return_inferred` set. `buffers` are the declaring buffer names.
17
+ Contract = Data.define(:overloads, :provenance, :path, :return_inferred, :buffers)
18
+
19
+ def initialize(provenance)
20
+ @provenance = provenance
21
+ end
22
+
23
+ # The member's contract, or nil when it has none of its own. `name` is the Ruby method name.
24
+ def read(owner, scope, name, lookup_scope)
25
+ method = lookup(owner, scope, name, lookup_scope)
26
+ defs = method ? method.defs.select { |definition| definition.defined_in.to_s.delete_prefix("::") == owner } : []
27
+ return nil if defs.empty?
28
+
29
+ annotations = annotations_of(method, defs)
30
+ return nil if annotations.include?(INFERRED_SIGNATURE)
31
+
32
+ contract(defs, annotations)
33
+ rescue StandardError
34
+ unreadable("definition_build_failed", [])
35
+ end
36
+
37
+ private
38
+
39
+ def contract(defs, annotations)
40
+ buffers = defs.map { |definition| buffer_name(definition) }
41
+ classified = buffers.map { |buffer| @provenance.classify(buffer) }
42
+ return unreadable("not_computed", buffers) if classified.include?(nil)
43
+
44
+ return_inferred = annotations.include?(INFERRED_RETURN)
45
+ authored = annotations.select do |string|
46
+ string.start_with?("rigor:v1:") && !string.start_with?(INFERRED_RETURN)
47
+ end
48
+ # Each overload carries the provenance of the declaration it came from: a `sig/` overload extending a
49
+ # library declaration reads `sig`, the library one `library`.
50
+ overloads = defs.zip(classified).map do |definition, (provenance, path)|
51
+ overload(definition.type, provenance, path, return_inferred, authored)
52
+ end
53
+ provenance, path = classified.first
54
+ Contract.new(overloads: overloads, provenance: provenance, path: path,
55
+ return_inferred: return_inferred, buffers: buffers)
56
+ end
57
+
58
+ # The method's and its own declarations' annotation strings: the plugin's defaulted-slot marks and authored
59
+ # `%a{…}`.
60
+ def annotations_of(method, defs)
61
+ strings = method.annotations.map(&:string) + defs.flat_map { |definition| definition.annotations.map(&:string) }
62
+ strings.map(&:strip).uniq
63
+ end
64
+
65
+ def buffer_name(definition)
66
+ location = definition.member&.location
67
+ location ? location.buffer&.name.to_s : ""
68
+ end
69
+
70
+ def lookup(owner, scope, name, lookup_scope)
71
+ if scope == :singleton
72
+ Reflection.singleton_method_definition(owner, name.to_sym, scope: lookup_scope)
73
+ else
74
+ Reflection.instance_method_definition(owner, name.to_sym, scope: lookup_scope)
75
+ end
76
+ end
77
+
78
+ # A declaration Rigor cannot read: its buffer name did not survive the environment cache
79
+ # (`not_computed`), or building the member's definition failed (`definition_build_failed`). The member has a
80
+ # contract, so it is not inferred; every slot is unavailable, and so are its parameters (`params: null`).
81
+ def unreadable(reason, buffers)
82
+ Contract.new(overloads: [{ "rbs" => nil, "params" => nil, "block" => nil,
83
+ "return" => Slot.unavailable(reason) }],
84
+ provenance: nil, path: nil, return_inferred: false, buffers: buffers)
85
+ end
86
+
87
+ def overload(method_type, provenance, path, return_inferred, annotations)
88
+ function = method_type.type
89
+ params = function.respond_to?(:required_positionals) ? params_of(function, provenance, path) : nil
90
+ block = method_type.block && { "required" => method_type.block.required,
91
+ "type" => Slot.from_rbs(method_type.block.type.to_s, provenance, path) }
92
+ prefix = annotations.map { |string| "%a{#{string}} " }.join
93
+ return_slot = return_inferred ? nil : Slot.from_rbs(function.return_type.to_s, provenance, path)
94
+ { "rbs" => "#{prefix}#{method_type}", "params" => params, "block" => block, "return" => return_slot,
95
+ "return_rbs" => function.return_type.to_s }
96
+ end
97
+
98
+ def params_of(function, provenance, path)
99
+ (positional_params(function) + keyword_params(function)).map do |kind, name, param|
100
+ { "name" => name&.to_s, "kind" => kind, "type" => Slot.from_rbs(param.type.to_s, provenance, path) }
101
+ end
102
+ end
103
+
104
+ def positional_params(function)
105
+ function.required_positionals.map { |param| ["required", param.name, param] } +
106
+ function.optional_positionals.map { |param| ["optional", param.name, param] } +
107
+ [function.rest_positionals].compact.map { |param| ["rest", param.name, param] } +
108
+ function.trailing_positionals.map { |param| ["post", param.name, param] }
109
+ end
110
+
111
+ def keyword_params(function)
112
+ function.required_keywords.map { |name, param| ["keyreq", name, param] } +
113
+ function.optional_keywords.map { |name, param| ["key", name, param] } +
114
+ [function.rest_keywords].compact.map { |param| ["keyrest", param.name, param] }
115
+ end
116
+ end
117
+ end
118
+ end