rigortype 0.3.5 → 0.3.7

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 (266) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/data/builtins/ruby_core/date.yml +6 -3
  4. data/data/builtins/ruby_core/enumerable.yml +1 -0
  5. data/data/builtins/ruby_core/exception.yml +2 -1
  6. data/data/builtins/ruby_core/file.yml +28 -14
  7. data/data/builtins/ruby_core/hash.yml +5 -2
  8. data/data/builtins/ruby_core/io.yml +33 -16
  9. data/data/builtins/ruby_core/random.yml +2 -1
  10. data/data/builtins/ruby_core/re.yml +6 -3
  11. data/data/builtins/ruby_core/struct.yml +2 -0
  12. data/data/builtins/ruby_core/time.yml +2 -1
  13. data/data/gem_overlay/activesupport/core_ext.rbs +442 -11
  14. data/docs/handbook/02-everyday-types.md +1 -1
  15. data/docs/handbook/04-tuples-and-shapes.md +1 -1
  16. data/docs/handbook/08-understanding-errors.md +1 -1
  17. data/docs/handbook/09-plugins.md +2 -2
  18. data/docs/handbook/10-sorbet.md +1 -1
  19. data/docs/handbook/README.md +2 -2
  20. data/docs/handbook/appendix-go.md +1 -1
  21. data/docs/handbook/appendix-java-csharp.md +2 -2
  22. data/docs/handbook/appendix-mypy.md +1 -1
  23. data/docs/handbook/appendix-protocols-and-structural-typing.md +2 -2
  24. data/docs/handbook/appendix-rust.md +1 -1
  25. data/docs/handbook/appendix-type-theory.md +2 -2
  26. data/docs/handbook/appendix-typescript.md +4 -4
  27. data/docs/manual/02-cli-reference.md +29 -16
  28. data/docs/manual/03-configuration.md +1 -1
  29. data/docs/manual/04-diagnostics.md +12 -0
  30. data/docs/manual/05-inspecting-types.md +20 -8
  31. data/docs/manual/07-plugins.md +4 -0
  32. data/docs/manual/08-skills.md +1 -1
  33. data/docs/manual/11-ci.md +1 -1
  34. data/docs/manual/12-caching.md +1 -1
  35. data/docs/manual/15-type-protection-coverage.md +1 -1
  36. data/docs/manual/18-removing-dead-code.md +13 -4
  37. data/docs/manual/19-effect-labels.md +19 -12
  38. data/docs/manual/README.md +2 -2
  39. data/docs/manual/plugins/rigor-actionmailer.md +4 -4
  40. data/docs/manual/plugins/rigor-actionpack.md +61 -0
  41. data/docs/manual/plugins/rigor-activejob.md +3 -3
  42. data/docs/manual/plugins/rigor-activerecord.md +78 -5
  43. data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
  44. data/docs/manual/plugins/rigor-rails-i18n.md +5 -5
  45. data/docs/manual/plugins/rigor-rspec.md +6 -2
  46. data/docs/manual/plugins/rigor-sidekiq.md +22 -0
  47. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
  48. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
  49. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
  50. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
  51. data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
  52. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
  53. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
  54. data/lib/rigor/analysis/check_rules.rb +383 -48
  55. data/lib/rigor/analysis/crash_signature.rb +116 -0
  56. data/lib/rigor/analysis/dependency_recorder.rb +18 -3
  57. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  58. data/lib/rigor/analysis/diagnostic.rb +17 -6
  59. data/lib/rigor/analysis/effects_cache_probe.rb +132 -0
  60. data/lib/rigor/analysis/incremental.rb +28 -0
  61. data/lib/rigor/analysis/incremental_session.rb +44 -9
  62. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
  63. data/lib/rigor/analysis/reachability/graph.rb +44 -16
  64. data/lib/rigor/analysis/reachability/plugin_roots.rb +11 -8
  65. data/lib/rigor/analysis/reachability/scan.rb +27 -7
  66. data/lib/rigor/analysis/reachability/scan_cache.rb +130 -0
  67. data/lib/rigor/analysis/result.rb +32 -0
  68. data/lib/rigor/analysis/run_cache_key.rb +53 -1
  69. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
  70. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +14 -8
  71. data/lib/rigor/analysis/runner/pool_coordinator.rb +34 -0
  72. data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
  73. data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
  74. data/lib/rigor/analysis/runner.rb +298 -38
  75. data/lib/rigor/analysis/worker_session.rb +10 -1
  76. data/lib/rigor/bleeding_edge.rb +7 -6
  77. data/lib/rigor/builtins/hkt_builtins.rb +45 -6
  78. data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
  79. data/lib/rigor/builtins/static_return_refinements.rb +20 -1
  80. data/lib/rigor/cache/descriptor.rb +42 -2
  81. data/lib/rigor/cache/file_digest.rb +20 -2
  82. data/lib/rigor/cache/incremental_snapshot.rb +40 -3
  83. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +31 -8
  84. data/lib/rigor/cache/store.rb +10 -1
  85. data/lib/rigor/cli/check_command.rb +67 -48
  86. data/lib/rigor/cli/coverage_command.rb +28 -24
  87. data/lib/rigor/cli/coverage_mutation.rb +13 -17
  88. data/lib/rigor/cli/coverage_scan.rb +47 -6
  89. data/lib/rigor/cli/doc_links.rb +100 -0
  90. data/lib/rigor/cli/docs_command.rb +32 -2
  91. data/lib/rigor/cli/effects_command.rb +27 -2
  92. data/lib/rigor/cli/effects_diff_renderer.rb +82 -12
  93. data/lib/rigor/cli/effects_explain_renderer.rb +6 -3
  94. data/lib/rigor/cli/effects_snapshot_command.rb +52 -7
  95. data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
  96. data/lib/rigor/cli/fused_protection_report.rb +23 -2
  97. data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
  98. data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
  99. data/lib/rigor/cli/mutation_protection_report.rb +31 -2
  100. data/lib/rigor/cli/sig_gen_command.rb +18 -0
  101. data/lib/rigor/cli/type_of_command.rb +188 -41
  102. data/lib/rigor/cli/type_of_renderer.rb +72 -3
  103. data/lib/rigor/cli/type_scan_command.rb +10 -1
  104. data/lib/rigor/cli/unused_command.rb +65 -16
  105. data/lib/rigor/cli.rb +22 -9
  106. data/lib/rigor/configuration.rb +7 -4
  107. data/lib/rigor/effects/definition_lines.rb +100 -0
  108. data/lib/rigor/effects/envelope_check.rb +18 -1
  109. data/lib/rigor/effects/liskov_check.rb +17 -8
  110. data/lib/rigor/effects/plugin_facts.rb +1 -1
  111. data/lib/rigor/effects/scanner.rb +2 -3
  112. data/lib/rigor/effects/signature_sources.rb +13 -2
  113. data/lib/rigor/effects/snapshot.rb +53 -21
  114. data/lib/rigor/effects/snapshot_diff.rb +26 -3
  115. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
  116. data/lib/rigor/environment/lockfile_resolver.rb +15 -5
  117. data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
  118. data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
  119. data/lib/rigor/environment/rbs_loader.rb +525 -46
  120. data/lib/rigor/environment.rb +38 -15
  121. data/lib/rigor/inference/acceptance.rb +172 -20
  122. data/lib/rigor/inference/captured_locals.rb +62 -0
  123. data/lib/rigor/inference/content_join.rb +347 -0
  124. data/lib/rigor/inference/def_handle.rb +15 -3
  125. data/lib/rigor/inference/def_node_resolver.rb +62 -3
  126. data/lib/rigor/inference/expression_typer.rb +1020 -141
  127. data/lib/rigor/inference/fork_map.rb +6 -1
  128. data/lib/rigor/inference/hkt_reducer.rb +12 -1
  129. data/lib/rigor/inference/hkt_registry.rb +46 -0
  130. data/lib/rigor/inference/hkt_sugar_translator.rb +93 -0
  131. data/lib/rigor/inference/index_write_widening.rb +48 -0
  132. data/lib/rigor/inference/indexed_narrowing.rb +14 -0
  133. data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
  134. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
  135. data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
  136. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
  137. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
  138. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
  139. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
  140. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
  141. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
  142. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
  143. data/lib/rigor/inference/method_dispatcher.rb +155 -14
  144. data/lib/rigor/inference/method_parameter_binder.rb +3 -3
  145. data/lib/rigor/inference/mutation_widening.rb +244 -174
  146. data/lib/rigor/inference/narrowing.rb +331 -27
  147. data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
  148. data/lib/rigor/inference/pre_eval_constants.rb +15 -0
  149. data/lib/rigor/inference/precision_scanner.rb +5 -2
  150. data/lib/rigor/inference/project_patched_scanner.rb +2 -1
  151. data/lib/rigor/inference/rbs_type_translator.rb +89 -39
  152. data/lib/rigor/inference/scope_indexer.rb +1493 -140
  153. data/lib/rigor/inference/statement_evaluator.rb +276 -91
  154. data/lib/rigor/inference/struct_fold_safety.rb +186 -27
  155. data/lib/rigor/inference/synthetic_method_scanner.rb +7 -0
  156. data/lib/rigor/inference/version_guard.rb +229 -0
  157. data/lib/rigor/language_server/selection_range_provider.rb +1 -1
  158. data/lib/rigor/plugin/base.rb +3 -2
  159. data/lib/rigor/plugin/inflector.rb +14 -5
  160. data/lib/rigor/plugin/io_boundary.rb +107 -4
  161. data/lib/rigor/plugin/loader.rb +14 -0
  162. data/lib/rigor/protection/analysis_guard.rb +60 -0
  163. data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
  164. data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
  165. data/lib/rigor/protection/discovery_seed.rb +1 -1
  166. data/lib/rigor/protection/measurement_integrity.rb +36 -0
  167. data/lib/rigor/protection/mutation_scanner.rb +61 -6
  168. data/lib/rigor/protection/mutator.rb +121 -18
  169. data/lib/rigor/rbs_extended/envelope_scanner.rb +9 -0
  170. data/lib/rigor/reflection.rb +182 -30
  171. data/lib/rigor/scope/discovery_index.rb +39 -1
  172. data/lib/rigor/scope.rb +385 -4
  173. data/lib/rigor/sig_gen/classification.rb +12 -1
  174. data/lib/rigor/sig_gen/generator.rb +254 -5
  175. data/lib/rigor/sig_gen/renderer.rb +1 -6
  176. data/lib/rigor/sig_gen/writer.rb +3 -0
  177. data/lib/rigor/source/constant_path.rb +79 -2
  178. data/lib/rigor/triage/catalogue.rb +1 -1
  179. data/lib/rigor/type/combinator.rb +10 -0
  180. data/lib/rigor/type/maybe.rb +47 -0
  181. data/lib/rigor/type/refined.rb +1 -2
  182. data/lib/rigor/type/result.rb +53 -0
  183. data/lib/rigor/type.rb +2 -0
  184. data/lib/rigor/version.rb +1 -1
  185. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
  186. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
  187. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
  188. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
  189. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
  190. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
  191. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
  192. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
  193. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
  194. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
  195. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
  196. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
  197. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
  198. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
  199. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
  200. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
  201. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
  202. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
  203. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
  204. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
  205. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
  206. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
  207. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
  208. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
  209. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
  210. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
  211. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
  212. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
  213. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
  214. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
  215. data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
  216. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
  217. data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
  218. data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
  219. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
  220. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
  221. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
  222. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
  223. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
  224. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
  225. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
  226. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
  227. data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
  228. data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
  229. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
  230. data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
  231. data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
  232. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
  233. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
  234. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  235. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
  236. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
  237. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
  238. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
  239. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
  240. data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
  241. data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
  242. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
  243. data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
  244. data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
  245. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
  246. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
  247. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
  248. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
  249. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
  250. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
  251. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
  252. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
  253. data/sig/rigor/analysis/baseline.rbs +1 -1
  254. data/sig/rigor/environment.rbs +3 -0
  255. data/sig/rigor/inference.rbs +4 -1
  256. data/sig/rigor/plugin/io_boundary.rbs +2 -0
  257. data/sig/rigor/reflection.rbs +3 -1
  258. data/sig/rigor/scope.rbs +25 -1
  259. data/sig/rigor/type.rbs +95 -1
  260. data/sig/rigor.rbs +16 -0
  261. data/skills/rigor-ci-setup/SKILL.md +2 -2
  262. data/skills/rigor-editor-setup/SKILL.md +2 -2
  263. data/skills/rigor-mcp-setup/SKILL.md +2 -2
  264. data/skills/rigor-monkeypatch-resolve/SKILL.md +1 -1
  265. data/skills/rigor-plugin-review/SKILL.md +3 -3
  266. metadata +73 -3
@@ -41,7 +41,7 @@ module Rigor
41
41
  UnrenderableMethod = Data.define(:path, :class_name, :method_name, :rbs, :error)
42
42
 
43
43
  # @return [Array<UnrenderableMethod>] empty on a healthy run; read after {#run}.
44
- attr_reader :unrenderable
44
+ attr_reader :unrenderable, :unresolvable_superclasses
45
45
 
46
46
  # @param configuration [Rigor::Configuration]
47
47
  # @param paths [Array<String>] files / directories to scan.
@@ -65,6 +65,10 @@ module Rigor
65
65
  @meta_layouts = {}
66
66
  # Whole-run, NOT per-file: a rendering defect is reported once at the end of the run.
67
67
  @unrenderable = []
68
+ # Issue #735 — whole-run too: `{ class name => the superclass token that resolves nowhere }`.
69
+ @unresolvable_superclasses = {}
70
+ # Issue #722 — whole-run: `{ class name => the lexical nesting its header is written in }`.
71
+ @superclass_nestings = {}
68
72
  end
69
73
 
70
74
  # Lifts legacy plain-`Array[Type]` observation entries into {ObservedCall} carriers. Specs from the
@@ -80,11 +84,229 @@ module Rigor
80
84
  def run
81
85
  @environment = build_environment
82
86
  resolved = resolve_paths(@paths)
83
- resolved.flat_map { |path| analyse_file(path, @environment) }
87
+ candidates = resolved.flat_map { |path| analyse_file(path, @environment) }
88
+ demote_overridden_base_methods(
89
+ demote_unresolvable_superclasses(resolve_superclass_spellings(candidates))
90
+ )
84
91
  end
85
92
 
86
93
  private
87
94
 
95
+ # Issue #744 — a base class's method is NOT emitted when a project subclass overrides it and the
96
+ # override is not emitted itself.
97
+ #
98
+ # RBS resolves an undeclared subclass method through its ancestors, so a precise return written for
99
+ # the base becomes the subclass's answer. redmine's `FieldFormat::Base#target_class` honestly returns
100
+ # `nil`; `RecordList#target_class` overrides it with a real lookup that sig-gen could not type, and
101
+ # the emitted `def target_class: () -> nil` then produced four `undefined method … for nil` on the
102
+ # subclass's own working code, plus a `def.return-type-mismatch` telling the user their correct
103
+ # override was wrong.
104
+ #
105
+ # Emitting nothing puts both classes back on source inference, which answers correctly for each. The
106
+ # base keeps its signature whenever nothing overrides the method, and whenever the override IS
107
+ # emitted with a type of its own — the declaration is only dangerous when it is the only one.
108
+ #
109
+ # Whether an inherited declaration SHOULD outrank a class's own source `def` is a separate question
110
+ # (#744 half 2) and is not decided here: this pass only stops sig-gen manufacturing the conflict.
111
+ def demote_overridden_base_methods(candidates)
112
+ superclasses = candidates.each_with_object({}) do |candidate, acc|
113
+ (candidate.class_superclasses || {}).each { |name, sup| acc[name] = sup.sub(/\[.*\]\z/, "") }
114
+ end
115
+ return candidates if superclasses.empty?
116
+
117
+ overridden = unsigned_override_ancestors(candidates, superclasses)
118
+ return candidates if overridden.empty?
119
+
120
+ candidates.map do |candidate|
121
+ next candidate unless Classification::EMITTABLE.include?(candidate.classification)
122
+ next candidate unless overridden.include?([candidate.class_name, candidate.method_name])
123
+
124
+ demoted_candidate(candidate, :overridden_by_unsigned_subclass)
125
+ end
126
+ end
127
+
128
+ # `[class name, method name]` pairs an UNSIGNED override shadows: for every candidate that will not
129
+ # be emitted, every ancestor of its class paired with its method name.
130
+ def unsigned_override_ancestors(candidates, superclasses)
131
+ candidates.each_with_object(Set.new) do |candidate, acc|
132
+ next if Classification::EMITTABLE.include?(candidate.classification)
133
+ next if candidate.class_name.nil?
134
+
135
+ each_ancestor(candidate.class_name, superclasses) { |name| acc << [name, candidate.method_name] }
136
+ end
137
+ end
138
+
139
+ def each_ancestor(class_name, superclasses)
140
+ seen = {}
141
+ current = class_name
142
+ while (parent = superclasses[current]) && !seen[parent]
143
+ seen[parent] = true
144
+ yield parent
145
+ current = parent
146
+ end
147
+ end
148
+
149
+ def demoted_candidate(candidate, reason)
150
+ MethodCandidate.new(
151
+ path: candidate.path, class_name: candidate.class_name, method_name: candidate.method_name,
152
+ kind: candidate.kind, classification: Classification::SKIPPED, skip_reason: reason,
153
+ inferred_return: candidate.inferred_return, declared_return_rbs: candidate.declared_return_rbs,
154
+ namespace_kinds: candidate.namespace_kinds, class_shells: candidate.class_shells,
155
+ class_superclasses: candidate.class_superclasses
156
+ )
157
+ end
158
+
159
+ # Issue #722 — resolves each recorded superclass token to the class Ruby means by it, and rewrites the
160
+ # emitted spelling when the two differ.
161
+ #
162
+ # `record_superclass` kept the source token verbatim on the reasoning that "RBS resolves it relative to
163
+ # the emitted namespace, matching Ruby's lexical scope". That holds only while the emitted namespace
164
+ # equals the source nesting, and sig-gen FLATTENS: `module Admin; class Record2 < Record` is written
165
+ # out as the compact `class Admin::Record2 < Record`, and RBS resolves a compact header's superclass at
166
+ # the TOP level (verified against the rbs gem), so the emitted token silently re-points at `::Record`
167
+ # where the program means `Admin::Record`. The checker resolves the same declaration correctly, so the
168
+ # generated sidecar contradicted the analyzer that wrote it.
169
+ #
170
+ # Candidate order is Ruby's own for the superclass expression: the header's nesting innermost-first,
171
+ # then the bare name — the reading `Scope#ancestor_name_candidates` owns for the analyzer's class
172
+ # graph. The question here is about the EMITTED tree, so the "is this a class" test is this run's
173
+ # declarations plus the RBS environment rather than the discovery tables; keep the two orders in step.
174
+ def resolve_superclass_spellings(candidates)
175
+ return candidates if @superclass_nestings.empty?
176
+
177
+ known = candidates.each_with_object(Set.new) do |candidate, acc|
178
+ acc << candidate.class_name if candidate.class_name
179
+ acc.merge(candidate.class_shells || [])
180
+ end
181
+ rewrites = superclass_rewrites(known)
182
+ return candidates if rewrites.empty?
183
+
184
+ candidates.map do |candidate|
185
+ overlap = rewrites.slice(*candidate.class_superclasses.keys)
186
+ next candidate if overlap.empty?
187
+
188
+ rebuild_with_superclasses(candidate, candidate.class_superclasses.merge(overlap))
189
+ end
190
+ end
191
+
192
+ def superclass_rewrites(known)
193
+ @superclass_nestings.each_with_object({}) do |(full, (nesting, token)), acc|
194
+ next if token.nil? || nesting.empty?
195
+
196
+ resolved = resolve_superclass_token(token, nesting, known)
197
+ acc[full] = resolved if resolved && resolved != token
198
+ end
199
+ end
200
+
201
+ # The first candidate spelling that names a class this run declares or the RBS environment knows, or
202
+ # nil when none does — an unresolvable superclass is #735's business, not this pass's.
203
+ def resolve_superclass_token(token, nesting, known)
204
+ base = token.sub(/\[.*\]\z/, "")
205
+ args = token[base.length..] || ""
206
+ nesting.length.downto(1) do |i|
207
+ candidate = "#{nesting[0, i].join('::')}::#{base}"
208
+ return "#{candidate}#{args}" if known.include?(candidate) ||
209
+ Reflection.rbs_class_known?(candidate, environment: @environment)
210
+ end
211
+ token
212
+ end
213
+
214
+ def rebuild_with_superclasses(candidate, superclasses)
215
+ MethodCandidate.new(
216
+ path: candidate.path, class_name: candidate.class_name, method_name: candidate.method_name,
217
+ kind: candidate.kind, classification: candidate.classification,
218
+ inferred_return: candidate.inferred_return, declared_return_rbs: candidate.declared_return_rbs,
219
+ rbs: candidate.rbs, skip_reason: candidate.skip_reason,
220
+ namespace_kinds: candidate.namespace_kinds, class_shells: candidate.class_shells,
221
+ class_superclasses: superclasses
222
+ )
223
+ end
224
+
225
+ # Issue #735 — a generated declaration whose superclass chain does not terminate in a class the RBS
226
+ # environment knows is DROPPED rather than emitted.
227
+ #
228
+ # `record_superclass` is right that a subclass declaration must carry its superclass: without it the
229
+ # sidecar misrepresents the class and dispatch degrades. But emitting one the environment cannot
230
+ # resolve is worse than either — `RBS::NoSuperclassFoundError` collapses the class, so every call into
231
+ # it reads `Dynamic[top]` and the whole declaration is a liability. On redmine, `class Principal <
232
+ # ApplicationRecord` (nothing declares `ApplicationRecord`, and nothing could: the chain ends at
233
+ # `ActiveRecord::Base`, which no RBS in the environment carries) collapsed 98 classes on the very next
234
+ # run. A chain that LOOPS is dropped for the same reason, and is the shape that made an
235
+ # `AncestorBuilder` walk recurse until `SystemStackError` (#609).
236
+ #
237
+ # Dropping is the conservative end: no declaration leaves the class exactly as it was before sig-gen
238
+ # ran — source discovery and the plugins still type it. Emitting the class with the superclass merely
239
+ # OMITTED would be the harmful middle: the class becomes RBS-known with only the emitted methods, so
240
+ # every inherited call fires `call.undefined-method`.
241
+ def demote_unresolvable_superclasses(candidates)
242
+ superclasses = candidates.each_with_object({}) do |candidate, acc|
243
+ (candidate.class_superclasses || {}).each { |name, sup| acc[name] = sup }
244
+ end
245
+ # Everything this run declares, superclass or not: a chain that reaches `class Record` — emitted
246
+ # here, with no superclass of its own — terminates in a class the next run WILL know.
247
+ emitted = candidates.each_with_object(Set.new) do |candidate, acc|
248
+ acc << candidate.class_name if candidate.class_name
249
+ acc.merge(candidate.class_shells || [])
250
+ end
251
+ unresolvable = superclasses.each_with_object({}) do |(name, _sup), acc|
252
+ root = unresolved_superclass_root(name, superclasses, emitted)
253
+ acc[name] = root if root
254
+ end
255
+ return candidates if unresolvable.empty?
256
+
257
+ candidates.map do |candidate|
258
+ owner = unresolvable_for(candidate.class_name, unresolvable)
259
+ next candidate if owner.nil?
260
+
261
+ record_unresolvable_superclass(candidate, owner)
262
+ end
263
+ end
264
+
265
+ # A nested class or module goes with its wrapper. `WikiPage::Webhookable`'s declaration is emitted
266
+ # INSIDE `class WikiPage < ApplicationRecord`, so keeping the inner one while dropping the outer emits
267
+ # the unresolvable header anyway — the wrapper is not a candidate of its own and carries no methods,
268
+ # so nothing else would have demoted it.
269
+ def unresolvable_for(class_name, unresolvable)
270
+ return nil if class_name.nil?
271
+ return [class_name, unresolvable[class_name]] if unresolvable.key?(class_name)
272
+
273
+ unresolvable.each do |name, root|
274
+ return [name, root] if class_name.start_with?("#{name}::")
275
+ end
276
+ nil
277
+ end
278
+
279
+ # The first superclass token on `name`'s recorded chain that neither the RBS environment knows nor this
280
+ # run emits, or nil when the chain terminates in a known class. A chain that revisits a class returns
281
+ # that class's own superclass token — a cycle resolves nowhere.
282
+ def unresolved_superclass_root(name, superclasses, emitted, seen = {})
283
+ superclass = superclasses[name]
284
+ return nil if superclass.nil?
285
+ return superclass if seen[name]
286
+
287
+ seen[name] = true
288
+ # The recorded spelling may carry the type arguments {#generic_superclass_spelling} added; the
289
+ # resolvability question is about the class, so it is asked of the bare name.
290
+ base = superclass.sub(/\[.*\]\z/, "")
291
+ return nil if Reflection.rbs_class_known?(base, environment: @environment)
292
+ return superclass unless emitted.include?(base)
293
+
294
+ unresolved_superclass_root(base, superclasses, emitted, seen)
295
+ end
296
+
297
+ def record_unresolvable_superclass(candidate, owner)
298
+ class_name, root = owner
299
+ @unresolvable_superclasses[class_name] ||= root
300
+ MethodCandidate.new(
301
+ path: candidate.path, class_name: candidate.class_name, method_name: candidate.method_name,
302
+ kind: candidate.kind, classification: Classification::SKIPPED,
303
+ skip_reason: :unresolvable_superclass, inferred_return: candidate.inferred_return,
304
+ declared_return_rbs: candidate.declared_return_rbs,
305
+ namespace_kinds: candidate.namespace_kinds, class_shells: candidate.class_shells,
306
+ class_superclasses: candidate.class_superclasses
307
+ )
308
+ end
309
+
88
310
  def build_environment
89
311
  Environment.for_project(
90
312
  libraries: @configuration.libraries,
@@ -193,7 +415,7 @@ module Rigor
193
415
 
194
416
  full = (prefix + [name]).join("::")
195
417
  @namespace_kinds[full] = node.is_a?(Prism::ClassNode) ? :class : :module
196
- record_superclass(node, full)
418
+ record_superclass(node, full, prefix)
197
419
  walk_namespace_body(node, prefix + [name], out)
198
420
  true
199
421
  end
@@ -207,11 +429,38 @@ module Rigor
207
429
  # superclass (`Class.new`, any other `CallNode`) is left unrecorded — a `Data.define` / `Struct.new` one is
208
430
  # already carried by {#register_meta_classes}, and the rest are un-representable, where guessing would
209
431
  # misfold.
210
- def record_superclass(node, full)
432
+ def record_superclass(node, full, prefix)
211
433
  return unless node.is_a?(Prism::ClassNode)
212
434
 
213
435
  superclass = qualified_constant_path(node.superclass)
214
- @class_superclasses[full] = superclass if superclass
436
+ return if superclass.nil?
437
+
438
+ # Issue #722 — the nesting the header is WRITTEN in, kept whole-run (not per-file scratch) because
439
+ # the name it resolves to may be a class another file declares. Resolved once every file has been
440
+ # walked, in {#resolve_superclass_spellings}.
441
+ spelling = generic_superclass_spelling(superclass)
442
+ # The token rides along because `@class_superclasses` is per-FILE scratch, reset before the next
443
+ # file, while this table is whole-run.
444
+ @superclass_nestings[full] = [prefix.dup, spelling]
445
+ @class_superclasses[full] = spelling
446
+ end
447
+
448
+ # Issue #735 — a GENERIC superclass has to be written with its arguments. `class Entries < Array`
449
+ # yields `RBS::InvalidTypeApplicationError: ::Array expects parameters [unchecked out E], but given
450
+ # args []`, which fails the build for that class exactly as an unresolvable superclass does — redmine
451
+ # subclasses `Array` five times. The element type is not something this pass can infer, so it is
452
+ # `untyped`: the subclass's OWN methods are what sig-gen is here to record, and `Array[untyped]` keeps
453
+ # the inherited surface resolving instead of collapsing the class.
454
+ def generic_superclass_spelling(superclass)
455
+ loader = @environment&.rbs_loader
456
+ return superclass if loader.nil?
457
+
458
+ arity = loader.class_type_param_names(superclass).size
459
+ return superclass if arity.zero?
460
+
461
+ "#{superclass}[#{(['untyped'] * arity).join(', ')}]"
462
+ rescue StandardError
463
+ superclass
215
464
  end
216
465
 
217
466
  # The ADR-48 member layouts for this file, in one table keyed by qualified class name. Reading the engine's
@@ -40,13 +40,8 @@ module Rigor
40
40
 
41
41
  private
42
42
 
43
- EMITTABLE = [Classification::NEW_FILE,
44
- Classification::NEW_METHOD,
45
- Classification::TIGHTER_RETURN].freeze
46
- private_constant :EMITTABLE
47
-
48
43
  def filter(candidates, selection)
49
- active = selection.empty? ? EMITTABLE : selection
44
+ active = selection.empty? ? Classification::EMITTABLE : selection
50
45
  candidates.select { |c| active.include?(c.classification) }
51
46
  end
52
47
 
@@ -90,6 +90,9 @@ module Rigor
90
90
  target.exist? ? update_existing(source_path, target, candidates) : create_new(source_path, target, candidates)
91
91
  end
92
92
 
93
+ # Deliberately NARROWER than {Classification::EMITTABLE}: the write path has never produced a
94
+ # `:new_file` row (the generator does not classify one), and admitting it here would let a
95
+ # classification with no writer behind it decide that a target file must be created.
93
96
  EMITTABLE = [Classification::NEW_METHOD, Classification::TIGHTER_RETURN].freeze
94
97
  private_constant :EMITTABLE
95
98
 
@@ -14,8 +14,18 @@ module Rigor
14
14
  # * {.qualified_name_or_nil} is STRICT — a dynamic base anywhere in the chain yields `nil`, so a caller
15
15
  # that statically names constants can treat the path as opaque rather than guessing.
16
16
  #
17
- # A leading `::` (absolute root, `::Foo`) renders as `"Foo"` under both policies. A node that is neither
18
- # a `ConstantReadNode` nor a `ConstantPathNode` yields `nil` under both.
17
+ # A leading `::` (absolute root, `::Foo`) renders as `"Foo"` under both policies: the discovered-constant
18
+ # tables are keyed by un-rooted names, so a rendered `"::Foo"` would miss every one of them. The root
19
+ # marker is therefore carried OUT OF BAND by {.rooted?}, which a caller performing Ruby's lexical constant
20
+ # lookup MUST consult — `::Foo` names the top-level `Foo` and never a lexically nearer shadow
21
+ # ([#614](https://github.com/rigortype/rigor/issues/614)). A node that is neither a `ConstantReadNode`
22
+ # nor a `ConstantPathNode` yields `nil` under both policies.
23
+ #
24
+ # A `class` / `module` HEADER is a constant path too, and it has to consult the same marker — which is why
25
+ # {.declaration_prefix} and {.pushed_nesting} live here rather than in each of the two dozen walks that
26
+ # used to append the rendered name to their own prefix. `::` re-anchors a header at the top level exactly
27
+ # as it re-anchors a reference; reading only the rendered name filed `class ::Foo` inside `module MyApp`
28
+ # under `MyApp::Foo` ([#708](https://github.com/rigortype/rigor/issues/708)).
19
29
  module ConstantPath
20
30
  module_function
21
31
 
@@ -53,6 +63,73 @@ module Rigor
53
63
  "#{parent_name}::#{node.name}"
54
64
  end
55
65
  end
66
+
67
+ # True when the reference is written with a leading `::` — `::Foo`, `::Foo::Bar`. Prism spells the
68
+ # root as a `ConstantPathNode` with a nil `parent`, so the answer lives at the LEFTMOST segment of the
69
+ # chain and the walk has to reach it: `::Foo::Bar` is a path node whose parent is itself a path node.
70
+ # A `ConstantReadNode` is a bare name and is never rooted; a dynamic base (`expr::Bar`) is not a root
71
+ # either, so the recursion stops at any non-constant parent.
72
+ def rooted?(node)
73
+ return false unless node.is_a?(Prism::ConstantPathNode)
74
+
75
+ parent = node.parent
76
+ return true if parent.nil?
77
+
78
+ rooted?(parent)
79
+ end
80
+
81
+ # The qualified prefix a `class` / `module` header contributes to the body it opens, given the prefix of
82
+ # the body the header is WRITTEN in. `class Bar` inside `module Outer` is `Outer::Bar`; the compact
83
+ # `class Outer::Bar` written at the top level is the same name through one keyword instead of two.
84
+ #
85
+ # A ROOTED header RESETS the prefix. `class ::Rooted::Bar` names `Rooted::Bar` wherever it is written —
86
+ # Ruby's `::` re-anchors the header at the top level exactly as it does a reference — so the enclosing
87
+ # prefix is DROPPED rather than kept. Keeping it filed the class under `Outer::Rooted::Bar`, a name no
88
+ # caller writes, and the single-segment `class ::Foo` inside `module MyApp` became `MyApp::Foo`: not
89
+ # merely mis-keyed but undiscoverable, so a call on `Foo` typed opaque and reported nothing at all
90
+ # ([#708](https://github.com/rigortype/rigor/issues/708), [#638](https://github.com/rigortype/rigor/issues/638)).
91
+ #
92
+ # nil when the header names no constant — the same refusal {.qualified_name} makes, propagated so a
93
+ # caller keeps its own prefix rather than qualifying under a name it could not render.
94
+ def declaration_prefix(outer_prefix, constant_path)
95
+ name = qualified_name(constant_path)
96
+ return nil if name.nil?
97
+
98
+ rooted?(constant_path) ? [name] : outer_prefix + [name]
99
+ end
100
+
101
+ # Ruby's `Module.nesting` inside the body a `class` / `module` header opens, given the nesting of the
102
+ # body the header is written in. ONE entry per declaration keyword, qualified against the entry already
103
+ # on top, so a compact `class A::B` contributes the single `"A::B"` where `module A; class B` contributes
104
+ # two — the whole difference between the two spellings, and unrecoverable from the `"A::B"` they share
105
+ # afterwards ([#652](https://github.com/rigortype/rigor/issues/652)). This is the ONLY thing that pushes
106
+ # an entry: a `def`, a block, and a `class << expr` body each inherit the chain unchanged, because none
107
+ # of them pushes a cref in Ruby.
108
+ #
109
+ # A ROOTED header pushes its name UNQUALIFIED and leaves the enclosing entries beneath it, which is the
110
+ # one place this differs from {.declaration_prefix}: Ruby's nesting inside `class ::Rooted::Bar` written
111
+ # in `module Outer` is `[Rooted::Bar, Outer]`, so the class's own name resets while `Outer` stays on the
112
+ # ladder as a live rung ([#708](https://github.com/rigortype/rigor/issues/708)).
113
+ #
114
+ # The header is rendered by the LENIENT {.qualified_name}, which is total over every header Ruby parses —
115
+ # `class` and `module` require a constant path, so the only dynamic base the grammar admits is `self`, and
116
+ # `class self::Thing` inside `module Outer` renders `"Thing"` and records `["Outer::Thing", "Outer"]`,
117
+ # which is what Ruby's nesting is there. So there is no unnameable-header case to refuse, and the `nil`
118
+ # guards below are reachable only for a non-constant node (which a `ClassNode` / `ModuleNode` header
119
+ # never is) and for the nil chain a caller may thread in.
120
+ #
121
+ # Both nesting walks call THIS function — `Inference::StatementEvaluator`'s declaration walk and
122
+ # `Inference::ScopeIndexer`'s def-node walk — so a callee re-walk answers what the body's own walk
123
+ # answers by construction rather than by two mirrored implementations staying in step.
124
+ def pushed_nesting(nesting, constant_path)
125
+ return nil if nesting.nil?
126
+
127
+ name = qualified_name(constant_path)
128
+ return nil if name.nil?
129
+
130
+ outer = rooted?(constant_path) ? nil : nesting.first
131
+ [outer ? "#{outer}::#{name}" : name, *nesting].freeze
132
+ end
56
133
  end
57
134
  end
58
135
  end
@@ -327,7 +327,7 @@ module Rigor
327
327
  parser == :toplevel ? d.method_name : parse_undefined_method(d)&.fetch(:method)
328
328
  end
329
329
  names.tally.sort_by { |method, count| [-count, method] }
330
- .first(limit).map { |method, count| "#{method}×#{count}" }.join(" ")
330
+ .first(limit).map { |method, count| "#{method}×#{count}" }.join(" ")
331
331
  end
332
332
 
333
333
  def rule_of(diag)
@@ -369,6 +369,16 @@ module Rigor
369
369
  StructInstance.new(members, class_name)
370
370
  end
371
371
 
372
+ # ADR-3 amendment (ADR-20 slice 4) — generic Result[T, E] carrier constructor.
373
+ def result_of(ok_type, err_type)
374
+ Result.new(ok_type, err_type)
375
+ end
376
+
377
+ # ADR-3 amendment (ADR-20 slice 4) — generic Maybe[T] carrier constructor.
378
+ def maybe_of(value_type)
379
+ Maybe.new(value_type)
380
+ end
381
+
372
382
  # Normalized union. Flattens nested Unions, deduplicates structurally equal members, drops Bot, and
373
383
  # collapses 0/1-member results.
374
384
  def union(*types)
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../trinary"
4
+ require_relative "../value_semantics"
5
+ require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
7
+
8
+ module Rigor
9
+ module Type
10
+ # A generic Maybe carrier representing an optional value that either contains a value of type
11
+ # `value_type` or nothing.
12
+ #
13
+ # Models `Maybe[T]` (e.g. `Dry::Monads::Maybe[T]`).
14
+ #
15
+ # Subtyping is covariant: `Maybe[T1] <: Maybe[T2]` iff `T1 <: T2`.
16
+ class Maybe
17
+ attr_reader :value_type
18
+
19
+ def initialize(value_type)
20
+ raise ArgumentError, "value_type must not be nil" if value_type.nil?
21
+
22
+ @value_type = value_type
23
+ freeze
24
+ end
25
+
26
+ def describe(verbosity = :short)
27
+ "Maybe[#{value_type.describe(verbosity)}]"
28
+ end
29
+
30
+ def erase_to_rbs
31
+ "::Dry::Monads::Maybe[#{value_type.erase_to_rbs}]"
32
+ end
33
+
34
+ include Rigor::Type::PlainLattice
35
+
36
+ include Rigor::Type::AcceptanceRouter
37
+
38
+ include Rigor::ValueSemantics
39
+
40
+ value_fields :value_type
41
+
42
+ def inspect
43
+ "#<Rigor::Type::Maybe #{describe(:short)}>"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -77,14 +77,13 @@ module Rigor
77
77
  # Recognises a Ruby value against this carrier's predicate. The trinary return is intentional:
78
78
  # `true` / `false` when the predicate registry decides, `nil` when the predicate is unknown to the
79
79
  # registry, so callers (today {Inference::Acceptance}) can fall through to gradual-mode `:maybe`.
80
- # rubocop:disable Style/ReturnNilInPredicateMethodDefinition
80
+ # rubocop:disable-next Style/ReturnNilInPredicateMethodDefinition
81
81
  def matches?(value)
82
82
  recogniser = PREDICATES[predicate_id]
83
83
  return nil if recogniser.nil?
84
84
 
85
85
  !!recogniser.call(value)
86
86
  end
87
- # rubocop:enable Style/ReturnNilInPredicateMethodDefinition
88
87
 
89
88
  # `predicate_id => recogniser` table. The recogniser is called with a Ruby value (typically the
90
89
  # inner `value` of a `Constant`) and returns truthy when the value satisfies the predicate. The
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../trinary"
4
+ require_relative "../value_semantics"
5
+ require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
7
+
8
+ module Rigor
9
+ module Type
10
+ # A generic Result carrier representing a computation that either succeeded with a value of type
11
+ # `ok_type` or failed with an error of type `err_type`.
12
+ #
13
+ # Models `Result[T, E]` (e.g. `Dry::Monads::Result[T, E]`).
14
+ #
15
+ # Subtyping is covariant in both positions: `Result[T1, E1] <: Result[T2, E2]` iff `T1 <: T2` and
16
+ # `E1 <: E2`.
17
+ class Result
18
+ attr_reader :ok_type, :err_type
19
+
20
+ alias success_type ok_type
21
+ alias failure_type err_type
22
+
23
+ def initialize(ok_type, err_type)
24
+ raise ArgumentError, "ok_type must not be nil" if ok_type.nil?
25
+ raise ArgumentError, "err_type must not be nil" if err_type.nil?
26
+
27
+ @ok_type = ok_type
28
+ @err_type = err_type
29
+ freeze
30
+ end
31
+
32
+ def describe(verbosity = :short)
33
+ "Result[#{ok_type.describe(verbosity)}, #{err_type.describe(verbosity)}]"
34
+ end
35
+
36
+ def erase_to_rbs
37
+ "::Dry::Monads::Result[#{ok_type.erase_to_rbs}, #{err_type.erase_to_rbs}]"
38
+ end
39
+
40
+ include Rigor::Type::PlainLattice
41
+
42
+ include Rigor::Type::AcceptanceRouter
43
+
44
+ include Rigor::ValueSemantics
45
+
46
+ value_fields :ok_type, :err_type
47
+
48
+ def inspect
49
+ "#<Rigor::Type::Result #{describe(:short)}>"
50
+ end
51
+ end
52
+ end
53
+ end
data/lib/rigor/type.rb CHANGED
@@ -28,6 +28,8 @@ require_relative "type/difference"
28
28
  require_relative "type/refined"
29
29
  require_relative "type/intersection"
30
30
  require_relative "type/app"
31
+ require_relative "type/result"
32
+ require_relative "type/maybe"
31
33
  require_relative "type/bound_method"
32
34
  require_relative "type/accepts_result"
33
35
  require_relative "type/combinator"
data/lib/rigor/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rigor
4
- VERSION = "0.3.5"
4
+ VERSION = "0.3.7"
5
5
  end
@@ -56,7 +56,9 @@ module Rigor
56
56
  # not ActionCable — pass through silently to avoid flagging unrelated `broadcast_to` methods.
57
57
  return [] unless class_name.end_with?("Channel")
58
58
 
59
- entry = channel_index.find(class_name) || channel_index.find("::#{class_name}")
59
+ # `ChannelIndex#find` de-roots the query itself (#621) a `::ChatChannel.broadcast_to` receiver
60
+ # and a plain `ChatChannel.broadcast_to` reach the same entry with no retry arm here.
61
+ entry = channel_index.find(class_name)
60
62
  return [unknown_channel_violation(class_name, channel_index)] if entry.nil?
61
63
 
62
64
  [broadcast_target_info(entry)]