rigortype 0.2.9 → 0.3.0

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 (184) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/docs/handbook/03-narrowing.md +2 -1
  4. data/docs/handbook/04-tuples-and-shapes.md +7 -3
  5. data/docs/handbook/06-classes.md +18 -9
  6. data/docs/handbook/08-understanding-errors.md +1 -0
  7. data/docs/handbook/11-sig-gen.md +11 -1
  8. data/docs/handbook/appendix-mypy.md +4 -3
  9. data/docs/install.md +14 -6
  10. data/docs/manual/01-installation.md +98 -4
  11. data/docs/manual/02-cli-reference.md +49 -12
  12. data/docs/manual/03-configuration.md +34 -2
  13. data/docs/manual/04-diagnostics.md +23 -0
  14. data/docs/manual/07-plugins.md +4 -4
  15. data/docs/manual/09-editor-integration.md +4 -4
  16. data/docs/manual/11-ci.md +51 -0
  17. data/docs/manual/12-caching.md +65 -0
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
  19. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
  20. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
  21. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
  22. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  23. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
  24. data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
  25. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  26. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
  27. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
  28. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
  29. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +93 -0
  30. data/lib/rigor/analysis/check_rules.rb +557 -95
  31. data/lib/rigor/analysis/dependency_recorder.rb +93 -9
  32. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  33. data/lib/rigor/analysis/incremental_session.rb +456 -51
  34. data/lib/rigor/analysis/path_expansion.rb +42 -0
  35. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
  36. data/lib/rigor/analysis/rule_catalog.rb +226 -1
  37. data/lib/rigor/analysis/run_cache_key.rb +68 -0
  38. data/lib/rigor/analysis/run_cache_probe.rb +72 -0
  39. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
  40. data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
  41. data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
  42. data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
  43. data/lib/rigor/analysis/runner.rb +343 -68
  44. data/lib/rigor/analysis/severity_stamp.rb +43 -0
  45. data/lib/rigor/analysis/worker_session.rb +30 -2
  46. data/lib/rigor/bleeding_edge.rb +37 -8
  47. data/lib/rigor/builtins/regex_refinement.rb +36 -1
  48. data/lib/rigor/cache/descriptor.rb +88 -28
  49. data/lib/rigor/cache/file_digest.rb +158 -0
  50. data/lib/rigor/cache/incremental_snapshot.rb +41 -5
  51. data/lib/rigor/cache/rbs_descriptor.rb +56 -10
  52. data/lib/rigor/cache/store.rb +33 -3
  53. data/lib/rigor/ci_detector.rb +90 -0
  54. data/lib/rigor/cli/annotate_command.rb +15 -9
  55. data/lib/rigor/cli/baseline_command.rb +4 -1
  56. data/lib/rigor/cli/check_command.rb +212 -27
  57. data/lib/rigor/cli/coverage_command.rb +6 -1
  58. data/lib/rigor/cli/docs_command.rb +0 -30
  59. data/lib/rigor/cli/doctor_command.rb +163 -23
  60. data/lib/rigor/cli/lsp_command.rb +5 -0
  61. data/lib/rigor/cli/mcp_command.rb +5 -0
  62. data/lib/rigor/cli/plugins_command.rb +36 -6
  63. data/lib/rigor/cli/plugins_renderer.rb +44 -12
  64. data/lib/rigor/cli/probe_environment.rb +85 -0
  65. data/lib/rigor/cli/sig_gen_command.rb +36 -9
  66. data/lib/rigor/cli/skill_command.rb +1 -32
  67. data/lib/rigor/cli/trace_command.rb +5 -6
  68. data/lib/rigor/cli/type_of_command.rb +9 -8
  69. data/lib/rigor/cli/type_scan_command.rb +8 -8
  70. data/lib/rigor/cli.rb +15 -4
  71. data/lib/rigor/config_audit.rb +48 -2
  72. data/lib/rigor/configuration/severity_profile.rb +31 -0
  73. data/lib/rigor/configuration.rb +156 -8
  74. data/lib/rigor/environment/default_libraries.rb +35 -0
  75. data/lib/rigor/environment/rbs_loader.rb +164 -5
  76. data/lib/rigor/environment.rb +19 -27
  77. data/lib/rigor/inference/budget_trace.rb +77 -2
  78. data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
  79. data/lib/rigor/inference/def_handle.rb +23 -0
  80. data/lib/rigor/inference/def_node_resolver.rb +90 -0
  81. data/lib/rigor/inference/def_return_typer.rb +2 -1
  82. data/lib/rigor/inference/expression_typer.rb +361 -113
  83. data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
  84. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
  85. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
  86. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
  87. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +11 -0
  88. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
  89. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
  90. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
  91. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
  92. data/lib/rigor/inference/method_dispatcher.rb +57 -1
  93. data/lib/rigor/inference/mutation_widening.rb +42 -8
  94. data/lib/rigor/inference/narrowing.rb +127 -17
  95. data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
  96. data/lib/rigor/inference/project_patched_scanner.rb +2 -1
  97. data/lib/rigor/inference/rbs_type_translator.rb +8 -1
  98. data/lib/rigor/inference/scope_indexer.rb +451 -36
  99. data/lib/rigor/inference/statement_evaluator.rb +64 -15
  100. data/lib/rigor/inference/struct_fold_safety.rb +48 -11
  101. data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
  102. data/lib/rigor/inference/void_origin.rb +25 -0
  103. data/lib/rigor/inference/void_tail_summary.rb +220 -0
  104. data/lib/rigor/language_server/completion_provider.rb +2 -1
  105. data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
  106. data/lib/rigor/language_server/folding_range_provider.rb +2 -1
  107. data/lib/rigor/language_server/selection_range_provider.rb +2 -1
  108. data/lib/rigor/language_server/signature_help_provider.rb +2 -1
  109. data/lib/rigor/plugin/base.rb +9 -22
  110. data/lib/rigor/plugin/inflector.rb +12 -3
  111. data/lib/rigor/plugin/io_boundary.rb +5 -1
  112. data/lib/rigor/plugin/isolation.rb +81 -11
  113. data/lib/rigor/plugin/load_error.rb +10 -1
  114. data/lib/rigor/plugin/loader.rb +96 -14
  115. data/lib/rigor/plugin/node_rule_walk.rb +5 -3
  116. data/lib/rigor/plugin/registry.rb +32 -23
  117. data/lib/rigor/plugin.rb +26 -0
  118. data/lib/rigor/protection/mutator.rb +3 -2
  119. data/lib/rigor/reflection.rb +64 -0
  120. data/lib/rigor/runtime/jit.rb +128 -0
  121. data/lib/rigor/scope/discovery_index.rb +12 -2
  122. data/lib/rigor/scope.rb +122 -28
  123. data/lib/rigor/sig_gen/classification.rb +5 -1
  124. data/lib/rigor/sig_gen/generator.rb +38 -6
  125. data/lib/rigor/sig_gen/observation_collector.rb +4 -3
  126. data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
  127. data/lib/rigor/sig_gen/renderer.rb +10 -0
  128. data/lib/rigor/sig_gen/write_result.rb +9 -4
  129. data/lib/rigor/sig_gen/writer.rb +24 -2
  130. data/lib/rigor/source/node_children.rb +116 -0
  131. data/lib/rigor/source/node_locator.rb +3 -1
  132. data/lib/rigor/source/node_walker.rb +4 -2
  133. data/lib/rigor/source.rb +1 -0
  134. data/lib/rigor/type/difference.rb +28 -24
  135. data/lib/rigor/type/hash_shape.rb +34 -10
  136. data/lib/rigor/version.rb +1 -1
  137. data/lib/rigortype.rb +24 -0
  138. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
  139. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
  140. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
  141. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
  142. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
  143. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
  144. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
  145. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
  146. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
  147. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
  148. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
  149. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
  150. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
  151. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
  152. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
  153. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
  154. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
  155. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
  156. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
  157. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
  158. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
  159. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
  160. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
  161. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
  162. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
  163. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
  164. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
  165. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
  166. data/sig/prism_node_children.rbs +9 -0
  167. data/sig/rigor/inference.rbs +7 -0
  168. data/sig/rigor/plugin/base.rbs +2 -3
  169. data/sig/rigor/reflection.rbs +1 -0
  170. data/sig/rigor/scope.rbs +12 -1
  171. data/skills/rigor-ci-setup/SKILL.md +10 -0
  172. data/skills/rigor-editor-setup/SKILL.md +11 -0
  173. data/skills/rigor-next-steps/SKILL.md +23 -2
  174. data/skills/rigor-plugin-author/SKILL.md +3 -3
  175. data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
  176. data/skills/rigor-plugin-review/SKILL.md +2 -2
  177. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
  178. data/skills/rigor-project-init/SKILL.md +29 -3
  179. data/skills/rigor-project-init/references/01-detect.md +1 -1
  180. data/skills/rigor-project-init/references/02-configure.md +39 -5
  181. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
  182. data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
  183. metadata +42 -24
  184. data/lib/rigor/cli/ci_detector.rb +0 -89
@@ -11,9 +11,11 @@ module Rigor
11
11
  # and degrading to today's behaviour (no carrier / the `Data` nominal) the moment a premise is
12
12
  # uncertain, so the tier is precision-additive and adds no false-positive surface:
13
13
  #
14
- # 1. `Data.define(:x, :y)` on a `Singleton[Data]` receiver with literal-Symbol args and NO block ->
15
- # `DataClass{members: [...]}`. A block (`Data.define(:x) do ... end`) defers (slice 4 hardens the
16
- # block-body case); non-literal members (`Data.define(*names)`) defer.
14
+ # 1. `Data.define(:x, :y)` on a `Singleton[Data]` receiver with literal-Symbol args ->
15
+ # `DataClass{members: [...]}`. A `do ... end` block folds too when its body redefines no member
16
+ # reader (bare-local block-form parity the block AST stands in for the class name the read-time
17
+ # guard would otherwise need); a `&proc` block, a reader-redefining block, or non-literal members
18
+ # (`Data.define(*names)`) defer.
17
19
  # 2. `.new` / `.[]` on a `DataClass` receiver -> a `DataInstance` whose member map is built from the
18
20
  # call's positional or keyword arguments. An arity / key mismatch degrades to the `Data` (or the
19
21
  # tagged class) nominal rather than a wrong member map.
@@ -63,12 +65,32 @@ module Rigor
63
65
 
64
66
  def fold_define(context)
65
67
  return nil unless context.method_name == :define
66
- # Block-form (`Data.define(:x) do ... end`) defers — slice 4.
67
- return nil unless context.block_type.nil?
68
68
 
69
69
  members = member_names_from_args(context.args)
70
70
  return nil if members.nil? || members.empty?
71
71
 
72
+ # Block-form (`Data.define(:x) do ... end`) bare-local parity (ADR-48 "Remaining"): the assigned
73
+ # constant / subclass forms already fold via the layout side-table, with the reader-redefinition
74
+ # guard consulting `Scope#user_def_for`; the bare-local form has no resolvable class name for it
75
+ # to key on. But the block AST is in hand, so the guard is applied directly against it: fold when
76
+ # the block redefines no member's synthesised reader (folding `inst.x` would otherwise run the
77
+ # redefined `def x`), and bail conservatively — the prior behaviour — the moment the block cannot
78
+ # be soundly cleared (a `&proc` argument, or any `def <member>` in the body).
79
+ return fold_define_block(context, members) unless context.block_type.nil?
80
+
81
+ Type::Combinator.data_class_of(members: members)
82
+ end
83
+
84
+ # The block-form fold decision. Only a literal `do ... end` block whose body redefines no member
85
+ # reader folds; a `&proc` block argument (no scannable body) or a body with a member-reader `def`
86
+ # stays unfolded. The resulting `DataClass` carries no `class_name` — folding is already proven
87
+ # reader-safe here, so the read-time `reader_overridden?` guard (which keys on the class name) has
88
+ # nothing left to catch.
89
+ def fold_define_block(context, members)
90
+ block = context.call_node&.block
91
+ return nil unless block.is_a?(Prism::BlockNode)
92
+ return nil if block_redefines_member_reader?(block, members)
93
+
72
94
  Type::Combinator.data_class_of(members: members)
73
95
  end
74
96
 
@@ -126,6 +148,9 @@ module Rigor
126
148
  shape = args.first
127
149
  return nil unless shape.is_a?(Type::HashShape) && shape.closed?
128
150
  return nil unless shape.optional_keys.empty?
151
+ # Non-Symbol keys (a HashShape may carry String / numeric / bool / nil scalar keys) can never
152
+ # match the Symbol member list — and mixed-class keys would make the `.sort` below raise.
153
+ return nil unless shape.pairs.keys.all?(Symbol)
129
154
  return nil unless shape.pairs.keys.sort == members.sort
130
155
 
131
156
  members.to_h { |name| [name, shape.pairs.fetch(name)] }
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../type"
4
+ require_relative "constant_folding"
4
5
 
5
6
  module Rigor
6
7
  module Inference
@@ -24,9 +25,27 @@ module Rigor
24
25
  #
25
26
  # See `docs/type-specification/value-lattice.md` for the union-distribution contract this tier
26
27
  # mirrors.
28
+ #
29
+ # ADR-91 — this tier is the single owner of the `Kernel` module-function fold surface, covering
30
+ # the conversion/identity folds (`Array` / `Integer` / `Float` / `Rational` / `Complex` / `p` /
31
+ # `pp` / `String` / `Hash`) AND the `format` / `sprintf` template fold (moved here from
32
+ # `LiteralStringFolding` in ADR-91 WD2 — `String#%` stays there as a receiver-typed String
33
+ # method). Ownership gating no longer happens per-fold: the dispatcher consults this tier only
34
+ # when `context.method_name` is in {INTRINSIC_NAMES} and {kernel_owned_call?} passes (ADR-91
35
+ # WD1), so a fold body never re-derives the guard. Callers reaching `try_dispatch` directly
36
+ # (internal dispatcher, unit probes) vouch for ownership themselves.
27
37
  module KernelDispatch
28
38
  module_function
29
39
 
40
+ # ADR-91 WD1 — the compiled method-name surface of Kernel's foldable module functions. The
41
+ # dispatcher gates on membership here before consulting the tier, and the WD3 parity spec
42
+ # derives its case list from this same table so a fold added without an entry never runs and
43
+ # every entry is spelling-parity checked. Public (not private_constant) and Ractor-shareable
44
+ # because the gate and the spec both read it.
45
+ INTRINSIC_NAMES = Ractor.make_shareable(
46
+ Set[:Array, :Integer, :Float, :Rational, :Complex, :p, :pp, :String, :Hash, :format, :sprintf]
47
+ )
48
+
30
49
  # `Kernel#Rational` / `Kernel#Complex` constructor folds. When every argument is a `Type::Constant`
31
50
  # whose value is numeric, we can run the actual Ruby constructor and lift the result into a
32
51
  # `Constant<Rational>` / `Constant<Complex>`. The factory accepts the same shapes as Ruby:
@@ -49,6 +68,26 @@ module Rigor
49
68
  INTEGER_REFINEMENT_PREDICATES = Set[:decimal_int].freeze
50
69
  private_constant :INTEGER_REFINEMENT_PREDICATES
51
70
 
71
+ # `Kernel#p` / `Kernel#pp` return their argument: one argument comes back verbatim, several come
72
+ # back as an Array of them, zero returns nil. The zero-arg form declines here (the RBS `nil` is
73
+ # already exact); the 1-arg form is a precision-PRESERVING identity (the argument type passes
74
+ # through unchanged — shapes, constants, and `Dynamic` alike); the n-arg form is a `Tuple`.
75
+ IDENTITY_PRINTERS = Set[:p, :pp].freeze
76
+ private_constant :IDENTITY_PRINTERS
77
+
78
+ # `Kernel#format` / `Kernel#sprintf` — a String-template fold moved here from
79
+ # `LiteralStringFolding` in ADR-91 WD2 so every Kernel module-function fold sits behind the
80
+ # WD1 gate.
81
+ FORMAT_METHODS = Set[:format, :sprintf].freeze
82
+ private_constant :FORMAT_METHODS
83
+
84
+ # Value classes `Kernel#String` folds over. Each is a literal-carrier class whose `to_s` is a
85
+ # deterministic pure function of the value, so running the real `String()` at fold time is sound.
86
+ STRING_SAFE_CLASSES = [
87
+ String, Symbol, Integer, Float, Rational, Complex, NilClass, TrueClass, FalseClass
88
+ ].freeze
89
+ private_constant :STRING_SAFE_CLASSES
90
+
52
91
  def try_dispatch(context)
53
92
  receiver = context.receiver
54
93
  method_name = context.method_name
@@ -58,10 +97,171 @@ module Rigor
58
97
  return try_numeric_constructor(method_name, args) if NUMERIC_CONSTRUCTORS.key?(method_name)
59
98
  return try_integer(args) if method_name == :Integer
60
99
  return try_float(args) if method_name == :Float
100
+ return try_identity_printer(context) if IDENTITY_PRINTERS.include?(method_name)
101
+ return try_string(context) if method_name == :String
102
+ return try_hash(context) if method_name == :Hash
103
+ return try_format(context) if FORMAT_METHODS.include?(method_name)
104
+
105
+ nil
106
+ end
107
+
108
+ # `Kernel#p(x)` / `Kernel#pp(x)` identity typing. Runtime contract: `p x` returns `x`, `p a, b`
109
+ # returns `[a, b]`, bare `p` returns nil. The 1-arg path returns the argument's type object
110
+ # UNCHANGED — never widened, never concretized (a `Dynamic` argument stays `Dynamic`) — so a
111
+ # `p`-wrapped expression is transparent to downstream inference. The n-arg path materialises the
112
+ # positional types as a `Tuple`. The 0-arg path declines (RBS `nil` is already exact).
113
+ #
114
+ # FP envelope: declines on a splat / forwarding argument (the runtime arity — and hence
115
+ # 1-arg-identity vs n-arg-Array — is not statically known). Ownership is gated once at the
116
+ # dispatcher (ADR-91 WD1), so this body no longer re-checks {#kernel_owned_call?}.
117
+ def try_identity_printer(context)
118
+ args = context.args
119
+ return nil if args.empty?
120
+ return nil if unresolved_argument_arity?(context.call_node)
121
+ return args.first if args.size == 1
122
+
123
+ Type::Combinator.tuple_of(*args)
124
+ end
125
+
126
+ # `Kernel#String(v)` — folds a value-pinned scalar `Constant` to `Constant[String]`
127
+ # (`String(42)` → `"42"`, `String(nil)` → `""`). Restricted to {STRING_SAFE_CLASSES} so the fold
128
+ # never models a user-defined `to_s` / `to_str`; anything else declines to the RBS `String`.
129
+ def try_string(context)
130
+ args = context.args
131
+ return nil unless args.size == 1
132
+ return nil if unresolved_argument_arity?(context.call_node)
133
+
134
+ arg = args.first
135
+ return nil unless arg.is_a?(Type::Constant)
136
+ return nil unless STRING_SAFE_CLASSES.any? { |klass| arg.value.instance_of?(klass) }
61
137
 
138
+ Type::Combinator.constant_of(String(arg.value))
139
+ rescue StandardError
62
140
  nil
63
141
  end
64
142
 
143
+ # `Kernel#Hash(v)` — the trivially-sound slice only. A `HashShape` argument passes through
144
+ # unchanged (`Hash(h)` returns `h` for a real Hash); `Hash(nil)` and `Hash([])` (an empty Tuple)
145
+ # collapse to the empty `HashShape`. Arguments relying on the `to_hash` protocol are not decidable
146
+ # from types alone and decline to the RBS envelope.
147
+ def try_hash(context)
148
+ args = context.args
149
+ return nil unless args.size == 1
150
+ return nil if unresolved_argument_arity?(context.call_node)
151
+
152
+ case (arg = args.first)
153
+ when Type::HashShape then arg
154
+ when Type::Constant then arg.value.nil? ? Type::Combinator.hash_shape_of({}) : nil
155
+ when Type::Tuple then arg.elements.empty? ? Type::Combinator.hash_shape_of({}) : nil
156
+ end
157
+ end
158
+
159
+ # `Kernel#format("hello %s", lit)` / `Kernel#sprintf(...)` — moved here from
160
+ # `LiteralStringFolding` in ADR-91 WD2 so it sits behind the WD1 ownership gate with every
161
+ # other Kernel module-function fold. The template plus every value argument must be
162
+ # literal-bearing ({Type::Combinator.literal_string_compatible?}) or a `Type::Constant` of
163
+ # any value (Constants are always provably literal). The template arg specifically must be
164
+ # literal-bearing — a `Constant<Integer>` first arg would not be a valid format template, so
165
+ # the `Type::Constant` allowance applies only to subsequent value args.
166
+ #
167
+ # When the template AND every value argument are value-pinned `Constant`s, the fold sharpens
168
+ # to the exact `Constant[String]` (`format("%d", 1)` → `"1"`) — the module-function sibling
169
+ # of `ConstantFolding#try_fold_string_format`'s `String#%` fold. This is a strict refinement
170
+ # inside the same firing envelope: every exact-foldable input was already lifted to
171
+ # `literal-string`. Ownership is gated at the dispatcher, so the fold itself needs no check.
172
+ def try_format(context)
173
+ args = context.args
174
+ return nil if args.empty?
175
+
176
+ exact = fold_format_constant(args)
177
+ return exact if exact
178
+ return nil unless Type::Combinator.literal_string_compatible?(args.first)
179
+ return nil unless args.drop(1).all? { |arg| literal_or_constant?(arg) }
180
+
181
+ Type::Combinator.literal_string
182
+ end
183
+
184
+ # Runs the real `Kernel#format` when everything is value-pinned. A malformed directive or an
185
+ # argument-count mismatch raises at fold time; the handler declines so the literal-string
186
+ # lift (or the RBS `String` envelope) answers instead. Results larger than the shared
187
+ # `ConstantFolding::STRING_FOLD_BYTE_LIMIT` decline too, keeping the carrier-size convention.
188
+ def fold_format_constant(args)
189
+ return nil unless args.all?(Type::Constant)
190
+ return nil unless args.first.value.is_a?(String)
191
+
192
+ result = format(args.first.value, *args.drop(1).map(&:value))
193
+ return nil if result.bytesize > ConstantFolding::STRING_FOLD_BYTE_LIMIT
194
+
195
+ Type::Combinator.constant_of(result)
196
+ rescue StandardError
197
+ nil
198
+ end
199
+
200
+ # KernelDispatch's own copy of the literal-or-Constant predicate (LiteralStringFolding keeps
201
+ # its own for the `String#%` fold that stayed there).
202
+ def literal_or_constant?(type)
203
+ Type::Combinator.literal_string_compatible?(type) || type.is_a?(Type::Constant)
204
+ end
205
+
206
+ # True when the call can be attributed to Kernel's own module function. Kernel's instance-side
207
+ # surface is PRIVATE, so a call with an explicit non-`self` receiver is a user-defined method —
208
+ # decline — EXCEPT the `Kernel` module object itself: `module_function` exposes every intrinsic
209
+ # as a public singleton too, so `Kernel.p(x)` / `Kernel.format(...)` dispatch to the same
210
+ # intrinsic as the implicit-self spelling. Likewise decline when the receiver class (or the
211
+ # toplevel) carries a discovered user redefinition of the name (`def p(node)` in a printer class
212
+ # must not be hijacked by the fold; the precise tiers run ahead of user-method inference). With
213
+ # no call_node / scope (internal dispatcher callers, unit probes) the guards pass — the caller
214
+ # vouches for the shape.
215
+ #
216
+ # ADR-91 WD1: the dispatcher calls this ONCE, before consulting the tier, so the fold bodies
217
+ # no longer re-check it. Kept public so the dispatcher (and unit probes) can reach it.
218
+ def kernel_owned_call?(context)
219
+ return false if user_redefined?(context)
220
+
221
+ !explicit_foreign_receiver?(context.call_node) || kernel_module_receiver?(context)
222
+ end
223
+
224
+ # The explicit-receiver spelling of a module function: the receiver TYPE is the `Kernel` module
225
+ # object itself. Typed (not node-spelled) so a user constant that happens to be named `Kernel`
226
+ # inside a namespace does not slip through.
227
+ def kernel_module_receiver?(context)
228
+ receiver = context.receiver
229
+ receiver.is_a?(Type::Singleton) && receiver.class_name == "Kernel"
230
+ end
231
+
232
+ def explicit_foreign_receiver?(call_node)
233
+ return false if call_node.nil?
234
+
235
+ receiver = call_node.receiver
236
+ !(receiver.nil? || receiver.is_a?(Prism::SelfNode))
237
+ end
238
+
239
+ def user_redefined?(context)
240
+ scope = context.scope
241
+ return false if scope.nil?
242
+
243
+ name = context.method_name
244
+ return true if scope.top_level_def_for(name)
245
+
246
+ case (receiver = context.receiver)
247
+ when Type::Nominal then scope.discovered_method?(receiver.class_name, name, :instance)
248
+ when Type::Singleton then scope.discovered_method?(receiver.class_name, name, :singleton)
249
+ else false
250
+ end
251
+ end
252
+
253
+ # True when the argument list carries a splat or an argument-forwarding node — the positional
254
+ # arity (and therefore which fold shape applies) is not statically known. `nil` call_node
255
+ # (internal callers) reads as resolved: the args array is authoritative there.
256
+ def unresolved_argument_arity?(call_node)
257
+ arguments = call_node&.arguments
258
+ return false if arguments.nil?
259
+
260
+ arguments.arguments.any? do |argument|
261
+ argument.is_a?(Prism::SplatNode) || argument.is_a?(Prism::ForwardingArgumentsNode)
262
+ end
263
+ end
264
+
65
265
  # `Kernel#Integer(arg)` / `Integer(arg, base)`. Two folding paths, tried in order:
66
266
  #
67
267
  # 1. A `Refined[String, predicate]` argument whose predicate is a total-parse carrier narrows to
@@ -15,8 +15,9 @@ module Rigor
15
15
  # size cap).
16
16
  # - `Array#join` on `Tuple[…]` receivers whose every element plus the separator argument (when given)
17
17
  # is literal-bearing.
18
- # - `Kernel#format` / `Kernel#sprintf` (any receiver) and `String#%` (literal-bearing receiver) when
19
- # every value argument is literal-bearing or a Type::Constant of any value.
18
+ # - `String#%` (literal-bearing receiver) when every value argument is literal-bearing or a
19
+ # Type::Constant of any value. (`Kernel#format` / `Kernel#sprintf` moved to {KernelDispatch} in
20
+ # ADR-91 WD2 — they are Kernel module functions, not receiver-typed String methods.)
20
21
  #
21
22
  # Result rule:
22
23
  #
@@ -38,7 +39,6 @@ module Rigor
38
39
  module_function
39
40
 
40
41
  CONCAT_METHODS = %i[+ << concat].freeze
41
- FORMAT_METHODS = %i[format sprintf].freeze
42
42
  # v0.1.1 Track 1 slice 5a — methods that, called with no arguments on a literal-bearing receiver,
43
43
  # return a value that is also literal-bearing. `#strip` / `#lstrip` / `#rstrip` / `#chomp` (no-arg)
44
44
  # / `#chop` strip a known subset of characters from the ends, so the survivors are always a
@@ -55,7 +55,7 @@ module Rigor
55
55
  # Integer plus an optional literal padding `String`. When the receiver and the (default or
56
56
  # supplied) padding are both literal-bearing, the result is literal-bearing too.
57
57
  WIDTH_PADDING_METHODS = %i[center ljust rjust].freeze
58
- private_constant :CONCAT_METHODS, :FORMAT_METHODS,
58
+ private_constant :CONCAT_METHODS,
59
59
  :LITERAL_PRESERVING_METHODS, :NON_EMPTY_LITERAL_PRESERVING_METHODS,
60
60
  :WIDTH_PADDING_METHODS
61
61
 
@@ -64,7 +64,6 @@ module Rigor
64
64
  method_name = context.method_name
65
65
  args = context.args
66
66
  return fold_array_join(receiver, args) if method_name == :join
67
- return fold_format(args) if FORMAT_METHODS.include?(method_name)
68
67
  return nil unless Type::Combinator.literal_string_compatible?(receiver)
69
68
  return fold_string_percent(args) if method_name == :%
70
69
  return fold_no_arg(receiver, method_name) if args.empty?
@@ -140,21 +139,27 @@ module Rigor
140
139
  return nil unless receiver.elements.all? { |el| Type::Combinator.literal_string_compatible?(el) }
141
140
  return nil unless args.size <= 1
142
141
  return nil if args.size == 1 && !Type::Combinator.literal_string_compatible?(args.first)
142
+ # Defer to {ShapeDispatch}'s `tuple_join` when the precise `Constant<String>` fold is reachable —
143
+ # every element is a `Constant` and the separator is absent or a `Constant<String>`. This tier runs
144
+ # AHEAD of ShapeDispatch, so returning the generic `literal-string` here would shadow that strictly
145
+ # more precise result (`["a", "b"].join("-")` → `Constant<"a-b">` rather than `literal-string`).
146
+ # Mixed tuples that carry a non-`Constant` `literal-string` element keep folding to `literal-string`
147
+ # here, because no exact value is knowable for them.
148
+ return nil if constant_join_reachable?(receiver, args)
143
149
 
144
150
  Type::Combinator.literal_string
145
151
  end
146
152
 
147
- # `format("hello %s", lit)` / `sprintf(...)` template plus every value argument must be
148
- # literal-bearing ({Type::Combinator.literal_string_compatible?}) or a `Type::Constant` of any
149
- # value (Constants are always provably literal). The template arg specifically must be
150
- # literal-bearing a Constant<Integer> first arg would not be a valid format template, so the
151
- # `Type::Constant` allowance applies only to subsequent value args.
152
- def fold_format(args)
153
- return nil if args.empty?
154
- return nil unless Type::Combinator.literal_string_compatible?(args.first)
155
- return nil unless args.drop(1).all? { |arg| literal_or_constant?(arg) }
153
+ # True when every tuple element is a `Constant` and the separator is absent or a `Constant<String>` —
154
+ # exactly the inputs for which `ShapeDispatch.tuple_join` materialises a precise `Constant<String>`.
155
+ # An empty tuple qualifies (`[].join` → `Constant<"">`). Callers have already verified every element
156
+ # is literal-string-compatible, so a `Constant` element is necessarily a `Constant<String>`.
157
+ def constant_join_reachable?(receiver, args)
158
+ return false unless receiver.elements.all?(Type::Constant)
159
+ return true if args.empty?
156
160
 
157
- Type::Combinator.literal_string
161
+ arg = args.first
162
+ arg.is_a?(Type::Constant) && arg.value.is_a?(String)
158
163
  end
159
164
 
160
165
  # `"foo %s" % "x"` / `"foo %s" % ["x", "y"]` — receiver is the template (already verified
@@ -217,7 +222,8 @@ module Rigor
217
222
  end
218
223
 
219
224
  private_class_method :fold_no_arg, :fold_concat, :fold_repeat, :fold_array_join,
220
- :fold_format, :fold_string_percent, :fold_width_pad,
225
+ :constant_join_reachable?,
226
+ :fold_string_percent, :fold_width_pad,
221
227
  :non_empty_literal_result, :literal_or_constant?,
222
228
  :integer_typed?, :known_negative_integer?,
223
229
  :known_zero_integer?, :known_positive_integer?
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "prism"
4
+
3
5
  require_relative "../../type"
6
+ require_relative "../../source/node_children"
4
7
 
5
8
  module Rigor
6
9
  module Inference
@@ -26,6 +29,36 @@ module Rigor
26
29
  !scope.user_def_for(class_name, method_name).nil?
27
30
  end
28
31
 
32
+ # The bare-local block-form counterpart of {#reader_overridden?}: when a `Data.define`/`Struct.new`
33
+ # block has no resolvable class name to look up in the project def-node table, its member-reader
34
+ # redefinitions are read straight off the block AST instead. True when the block body defines a
35
+ # `def <member>` (a plain instance method, receiver-less) for any member — folding that member's
36
+ # read would run the redefined reader, not return the member. Nested `class`/`module`/`sclass`
37
+ # bodies open a different namespace and are skipped; a `def` anywhere else in the body counts.
38
+ def block_redefines_member_reader?(block, members)
39
+ body = block.body
40
+ return false if body.nil?
41
+
42
+ member_set = members.is_a?(Set) ? members : members.to_set
43
+ reader_def?(body, member_set)
44
+ end
45
+
46
+ def reader_def?(node, member_set)
47
+ return false unless node.is_a?(Prism::Node)
48
+ return true if node.is_a?(Prism::DefNode) && node.receiver.nil? && member_set.include?(node.name)
49
+ # A nested class / module / singleton-class body opens a different namespace — its `def`s do not
50
+ # redefine the value object's reader, so the walk does not descend into it.
51
+ return false if reader_scope_boundary?(node)
52
+
53
+ node.rigor_each_child { |child| return true if reader_def?(child, member_set) }
54
+ false
55
+ end
56
+
57
+ def reader_scope_boundary?(node)
58
+ node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode) ||
59
+ node.is_a?(Prism::SingletonClassNode)
60
+ end
61
+
29
62
  def instance_index(instance, args)
30
63
  return nil unless args.size == 1
31
64
 
@@ -241,6 +241,9 @@ module Rigor
241
241
  # would beat strictly-typed alternatives in pass 2 of the selector.
242
242
  def strictly_typed_params?(method_type, actual_count)
243
243
  fun = method_type.type
244
+ # A `(?)` method type declares no params at all: it is the gradual case by construction, so it
245
+ # must never win the strict pass over a genuinely typed sibling overload.
246
+ return false unless fun.respond_to?(:required_positionals)
244
247
  return false unless arity_compatible?(fun, actual_count)
245
248
 
246
249
  params = positional_params_for(fun, actual_count)
@@ -299,7 +302,11 @@ module Rigor
299
302
  !fun.required_keywords.empty?
300
303
  end
301
304
 
305
+ # `RBS::Types::UntypedFunction` (`(?)`) declares no arity to enforce, so every call site is
306
+ # arity-compatible with it.
302
307
  def arity_compatible?(fun, actual_count)
308
+ return true unless fun.respond_to?(:required_positionals)
309
+
303
310
  min_arity = fun.required_positionals.size + fun.trailing_positionals.size
304
311
  return false if actual_count < min_arity
305
312
 
@@ -314,6 +321,10 @@ module Rigor
314
321
  # Rest_positionals consumes the remainder; we repeat its single declaration for each absorbed
315
322
  # argument.
316
323
  def positional_params_for(fun, actual_count)
324
+ # `(?)` declares no positional params; the caller zips this against the actual args, so an empty
325
+ # list is what "accepts anything, constrains nothing" looks like here.
326
+ return [] unless fun.respond_to?(:required_positionals)
327
+
317
328
  required = fun.required_positionals
318
329
  optional = fun.optional_positionals
319
330
  rest = fun.rest_positionals
@@ -4,6 +4,7 @@ require_relative "../../reflection"
4
4
  require_relative "../../type"
5
5
  require_relative "../../rbs_extended"
6
6
  require_relative "../rbs_type_translator"
7
+ require_relative "../void_origin"
7
8
  require_relative "overload_selector"
8
9
 
9
10
  module Rigor
@@ -96,7 +97,8 @@ module Rigor
96
97
  block_type: context.block_type,
97
98
  self_type_override: context.self_type_override,
98
99
  public_only: context.public_only,
99
- scope: context.scope
100
+ scope: context.scope,
101
+ call_node: context.call_node
100
102
  )
101
103
  end
102
104
 
@@ -135,23 +137,23 @@ module Rigor
135
137
  private
136
138
 
137
139
  def dispatch_for(receiver:, method_name:, args:, environment:, block_type:, self_type_override: nil, # rubocop:disable Metrics/ParameterLists
138
- public_only: false, scope: nil)
140
+ public_only: false, scope: nil, call_node: nil)
139
141
  args ||= []
140
142
  case receiver
141
143
  when Type::Union
142
144
  dispatch_union(receiver, method_name, args, environment, block_type, self_type_override,
143
- public_only: public_only, scope: scope)
145
+ public_only: public_only, scope: scope, call_node: call_node)
144
146
  else
145
147
  dispatch_one(receiver, method_name, args, environment, block_type, self_type_override,
146
- public_only: public_only, scope: scope)
148
+ public_only: public_only, scope: scope, call_node: call_node)
147
149
  end
148
150
  end
149
151
 
150
152
  def dispatch_union(receiver, method_name, args, environment, block_type, self_type_override = nil, # rubocop:disable Metrics/ParameterLists
151
- public_only: false, scope: nil)
153
+ public_only: false, scope: nil, call_node: nil)
152
154
  results = receiver.members.map do |member|
153
155
  dispatch_one(member, method_name, args, environment, block_type, self_type_override,
154
- public_only: public_only, scope: scope)
156
+ public_only: public_only, scope: scope, call_node: call_node)
155
157
  end
156
158
  return nil if results.any?(&:nil?)
157
159
 
@@ -159,7 +161,7 @@ module Rigor
159
161
  end
160
162
 
161
163
  def dispatch_one(receiver, method_name, args, environment, block_type, self_type_override = nil, # rubocop:disable Metrics/ParameterLists
162
- public_only: false, scope: nil)
164
+ public_only: false, scope: nil, call_node: nil)
163
165
  descriptor = receiver_descriptor(receiver)
164
166
  return nil unless descriptor
165
167
 
@@ -173,11 +175,14 @@ module Rigor
173
175
  method_definition,
174
176
  class_name: class_name,
175
177
  kind: kind,
178
+ method_name: method_name,
176
179
  args: args,
177
180
  type_vars: type_vars,
178
181
  block_type: block_type,
179
182
  environment: environment,
180
- self_type_override: self_type_override
183
+ self_type_override: self_type_override,
184
+ scope: scope,
185
+ call_node: call_node
181
186
  )
182
187
  rescue StandardError
183
188
  # Defensive: if RBS' definition builder raises on a broken hierarchy (e.g., partially loaded
@@ -318,7 +323,8 @@ module Rigor
318
323
 
319
324
  # rubocop:disable Metrics/ParameterLists
320
325
  def translate_return_type(method_definition, class_name:, kind:, args:, type_vars:, block_type:,
321
- environment: nil, self_type_override: nil)
326
+ method_name: nil, environment: nil, self_type_override: nil,
327
+ scope: nil, call_node: nil)
322
328
  # rubocop:enable Metrics/ParameterLists
323
329
  # Slice 4b-3 (ADR-7 § "Slice 4-A/4-B") — read the return-type override through the merger so
324
330
  # future plugin / `:rbs_extended` bundles that also assert a `return_type` slot at this call
@@ -348,6 +354,15 @@ module Rigor
348
354
  )
349
355
  return nil unless method_type
350
356
 
357
+ # ADR-100 WD2/WD3 — the return-typing tier is where `void → top` widens, so it is the one place
358
+ # that still knows the RBS return was an author-declared `-> void` before the translator erases
359
+ # it to a plain `top`. Record the recovery on the scope's `void_origins` side-table, keyed by the
360
+ # call node, so the `static.value-use.void` check rule can fire when this `top` is used in value
361
+ # context. Recording is gated on `scope` && `call_node` being present, which naturally scopes it
362
+ # to the *direct*-RBS dispatch (the receiver's own resolvable class): the user-class / Object
363
+ # ancestor fallback nils both out (WD4 defers that, murkier, surface).
364
+ record_void_recovery(method_type, scope, call_node, [class_name, method_name, kind])
365
+
351
366
  full_type_vars = compose_block_type_vars(method_type, type_vars, block_type)
352
367
 
353
368
  RbsTypeTranslator.translate(
@@ -358,6 +373,24 @@ module Rigor
358
373
  )
359
374
  end
360
375
 
376
+ # Record the `void → top` recovery when the selected overload declares `-> void` and both `scope` and
377
+ # `call_node` are present (the direct-dispatch path). `void_site` is the `[class_name, method_name,
378
+ # kind]` triple the {VoidOrigin} carries.
379
+ def record_void_recovery(method_type, scope, call_node, void_site)
380
+ return unless scope && call_node && void_return?(method_type)
381
+
382
+ class_name, method_name, kind = void_site
383
+ scope.record_void_origin(
384
+ call_node,
385
+ VoidOrigin.new(class_name: class_name, method_name: method_name, kind: kind)
386
+ )
387
+ end
388
+
389
+ def void_return?(method_type)
390
+ fun = method_type.type
391
+ fun.respond_to?(:return_type) && fun.return_type.is_a?(RBS::Types::Bases::Void)
392
+ end
393
+
361
394
  # ADR-7 § "Slice 4-A/4-B" — folds the `RBS::Extended` `return:` directive (and any other
362
395
  # `return_type`-bearing contribution future slices add at this call site) through the merger
363
396
  # before consuming. Returns the merged return type or nil when no contribution overrides the
@@ -41,8 +41,13 @@ module Rigor
41
41
  end
42
42
  end
43
43
 
44
+ # `RBS::Types::UntypedFunction` (a `(?)` method type, e.g. core's `Proc#call`) declares no
45
+ # parameters and exposes none of the per-arity accessors. It has no param classes to compare
46
+ # against the receiver, so it carries no affinity either way.
44
47
  def overload_param_classes_in_ancestry?(method_type, self_class_name, environment)
45
48
  fun = method_type.type
49
+ return false unless fun.respond_to?(:required_positionals)
50
+
46
51
  params = fun.required_positionals + fun.optional_positionals + fun.trailing_positionals
47
52
  return false if params.empty?
48
53