rigortype 0.4.7 → 0.4.8

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 (268) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/docs/handbook/01-getting-started.md +1 -1
  8. data/docs/handbook/11-sig-gen.md +35 -0
  9. data/docs/install.md +20 -0
  10. data/docs/manual/01-installation.md +67 -26
  11. data/docs/manual/02-cli-reference.md +307 -91
  12. data/docs/manual/03-configuration.md +41 -3
  13. data/docs/manual/04-diagnostics.md +5 -4
  14. data/docs/manual/08-skills.md +11 -0
  15. data/docs/manual/09-editor-integration.md +8 -0
  16. data/docs/manual/14-rails-quickstart.md +1 -1
  17. data/docs/manual/19-effect-labels.md +3 -4
  18. data/docs/manual/plugins/rigor-actionpack.md +15 -6
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
  20. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  21. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  22. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  23. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
  24. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
  25. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
  26. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  27. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
  28. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  29. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  30. data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
  31. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  32. data/lib/rigor/analysis/check_rules.rb +598 -99
  33. data/lib/rigor/analysis/crash_signature.rb +43 -1
  34. data/lib/rigor/analysis/dependency_recorder.rb +233 -76
  35. data/lib/rigor/analysis/incremental.rb +11 -1
  36. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
  37. data/lib/rigor/analysis/incremental_session.rb +279 -37
  38. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  39. data/lib/rigor/analysis/rule_catalog.rb +37 -1
  40. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  41. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  42. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
  43. data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
  44. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
  45. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  48. data/lib/rigor/analysis/runner.rb +222 -39
  49. data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
  50. data/lib/rigor/analysis/worker_session.rb +42 -41
  51. data/lib/rigor/bleeding_edge.rb +19 -4
  52. data/lib/rigor/cache/file_digest.rb +39 -31
  53. data/lib/rigor/cache/incremental_snapshot.rb +66 -7
  54. data/lib/rigor/cache/rbs_descriptor.rb +37 -2
  55. data/lib/rigor/cache/rbs_environment.rb +5 -1
  56. data/lib/rigor/cli/baseline_command.rb +15 -2
  57. data/lib/rigor/cli/check_command.rb +4 -2
  58. data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
  59. data/lib/rigor/cli/lens_command.rb +17 -2
  60. data/lib/rigor/cli/sig_gen_command.rb +8 -0
  61. data/lib/rigor/cli/skill_describe.rb +1 -0
  62. data/lib/rigor/cli.rb +5 -2
  63. data/lib/rigor/config_audit.rb +32 -18
  64. data/lib/rigor/configuration/severity_profile.rb +9 -0
  65. data/lib/rigor/configuration.rb +32 -5
  66. data/lib/rigor/effects/collector.rb +53 -21
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
  68. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  69. data/lib/rigor/environment/default_libraries.rb +17 -0
  70. data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
  71. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  72. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  73. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  74. data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
  75. data/lib/rigor/environment/rbs_loader.rb +312 -63
  76. data/lib/rigor/environment.rb +131 -35
  77. data/lib/rigor/inference/acceptance.rb +50 -15
  78. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  79. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  80. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  81. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  82. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  83. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  84. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  85. data/lib/rigor/inference/block_run_count.rb +9 -2
  86. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  87. data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
  88. data/lib/rigor/inference/captured_locals.rb +2 -0
  89. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  90. data/lib/rigor/inference/clause_widening.rb +1 -1
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  92. data/lib/rigor/inference/construction_escape.rb +56 -15
  93. data/lib/rigor/inference/declaration_walk.rb +4 -0
  94. data/lib/rigor/inference/def_node_resolver.rb +32 -14
  95. data/lib/rigor/inference/def_return_typer.rb +1 -1
  96. data/lib/rigor/inference/definer_resolution.rb +15 -7
  97. data/lib/rigor/inference/definers_index.rb +72 -16
  98. data/lib/rigor/inference/error_info.rb +16 -0
  99. data/lib/rigor/inference/expression_typer.rb +1459 -287
  100. data/lib/rigor/inference/flow_tracer.rb +31 -14
  101. data/lib/rigor/inference/fork_map.rb +3 -0
  102. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  103. data/lib/rigor/inference/global_write_census.rb +8 -6
  104. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  105. data/lib/rigor/inference/indexed_narrowing.rb +57 -8
  106. data/lib/rigor/inference/ivar_writer_census.rb +106 -11
  107. data/lib/rigor/inference/key_presence_guard.rb +51 -25
  108. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  109. data/lib/rigor/inference/last_line.rb +5 -2
  110. data/lib/rigor/inference/loop_value_record.rb +63 -15
  111. data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
  112. data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
  113. data/lib/rigor/inference/match_rebinding.rb +21 -8
  114. data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
  115. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  116. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  117. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  118. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  119. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  120. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  121. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
  122. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
  123. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
  124. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
  125. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  126. data/lib/rigor/inference/method_dispatcher.rb +66 -18
  127. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  128. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  129. data/lib/rigor/inference/module_function_state.rb +13 -2
  130. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  131. data/lib/rigor/inference/mutation_widening.rb +44 -4
  132. data/lib/rigor/inference/narrowing.rb +201 -27
  133. data/lib/rigor/inference/operand_effects.rb +43 -2
  134. data/lib/rigor/inference/optimistic_origin.rb +133 -8
  135. data/lib/rigor/inference/origin_lookup.rb +1 -1
  136. data/lib/rigor/inference/precision_scanner.rb +1 -1
  137. data/lib/rigor/inference/project_method_ownership.rb +28 -10
  138. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  139. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  140. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  141. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  142. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  143. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  144. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
  145. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
  146. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  147. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  148. data/lib/rigor/inference/remover_holder_scan.rb +76 -22
  149. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  150. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
  151. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
  152. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  153. data/lib/rigor/inference/scope_indexer.rb +2079 -183
  154. data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
  155. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  156. data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
  157. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  158. data/lib/rigor/inference/statement_evaluator.rb +970 -250
  159. data/lib/rigor/inference/string_mutation.rb +14 -0
  160. data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
  161. data/lib/rigor/inference/toplevel_def_census.rb +14 -6
  162. data/lib/rigor/inference/unknown_store_widening.rb +1 -1
  163. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  164. data/lib/rigor/inference/void_tail_summary.rb +29 -10
  165. data/lib/rigor/lens/claims/comparison.rb +78 -0
  166. data/lib/rigor/lens/claims/request.rb +131 -0
  167. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  168. data/lib/rigor/lens/claims.rb +208 -0
  169. data/lib/rigor/lens/contract_reader.rb +5 -0
  170. data/lib/rigor/lens/document.rb +1 -1
  171. data/lib/rigor/lens/file_reader.rb +21 -8
  172. data/lib/rigor/lens/run.rb +8 -0
  173. data/lib/rigor/lens.rb +1 -1
  174. data/lib/rigor/library_probe.rb +68 -0
  175. data/lib/rigor/memo_audit.rb +282 -0
  176. data/lib/rigor/plugin/base.rb +15 -2
  177. data/lib/rigor/plugin/io_boundary.rb +25 -3
  178. data/lib/rigor/plugin/manifest.rb +7 -3
  179. data/lib/rigor/plugin/registry.rb +13 -2
  180. data/lib/rigor/plugin/trust_policy.rb +46 -0
  181. data/lib/rigor/protection/mutation_cache.rb +4 -1
  182. data/lib/rigor/reflection/constant_ancestors.rb +79 -22
  183. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  184. data/lib/rigor/reflection/declarations.rb +58 -0
  185. data/lib/rigor/reflection/project_method.rb +177 -0
  186. data/lib/rigor/reflection.rb +2 -0
  187. data/lib/rigor/rooted_glob.rb +18 -1
  188. data/lib/rigor/run_scoped_slots.rb +20 -20
  189. data/lib/rigor/scope/discovery_index.rb +149 -2
  190. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  191. data/lib/rigor/scope/resolution_chain.rb +900 -55
  192. data/lib/rigor/scope.rb +307 -34
  193. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  194. data/lib/rigor/sig_gen/classification.rb +14 -1
  195. data/lib/rigor/sig_gen/generator.rb +222 -13
  196. data/lib/rigor/sig_gen/method_candidate.rb +14 -0
  197. data/lib/rigor/sig_gen/observation_collector.rb +11 -2
  198. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  199. data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
  200. data/lib/rigor/sig_gen/writer.rb +21 -0
  201. data/lib/rigor/sig_gen.rb +1 -0
  202. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  203. data/lib/rigor/source/node_children.rb +13 -0
  204. data/lib/rigor/type/combinator.rb +164 -8
  205. data/lib/rigor/type/interface.rb +60 -0
  206. data/lib/rigor/type.rb +1 -0
  207. data/lib/rigor/type_of_memo/serving.rb +99 -0
  208. data/lib/rigor/type_of_memo/stats.rb +43 -0
  209. data/lib/rigor/type_of_memo.rb +337 -0
  210. data/lib/rigor/typing_state.rb +1094 -0
  211. data/lib/rigor/version.rb +1 -1
  212. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
  213. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
  214. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  215. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  216. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  217. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  218. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  219. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
  220. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
  221. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  222. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
  223. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  224. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
  225. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
  226. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
  227. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  228. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  229. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
  230. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  231. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
  232. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
  233. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
  234. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
  235. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
  236. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  237. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  238. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
  239. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  240. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
  241. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  242. data/sig/rigor/analysis/fact_store.rbs +2 -3
  243. data/sig/rigor/cache/descriptor.rbs +14 -0
  244. data/sig/rigor/environment.rbs +4 -1
  245. data/sig/rigor/plugin/base.rbs +53 -19
  246. data/sig/rigor/plugin/interfaces.rbs +51 -0
  247. data/sig/rigor/plugin/io_boundary.rbs +2 -3
  248. data/sig/rigor/plugin/node_context.rbs +21 -0
  249. data/sig/rigor/reflection.rbs +37 -0
  250. data/sig/rigor/scope.rbs +22 -3
  251. data/sig/rigor/trinary.rbs +1 -0
  252. data/sig/rigor/type.rbs +9 -3
  253. data/sig/rigor.rbs +3 -2
  254. data/skills/rigor-ask/SKILL.md +1 -0
  255. data/skills/rigor-project-init/references/01-detect.md +1 -0
  256. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
  257. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  258. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  259. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  260. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  261. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  262. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  263. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  264. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  265. data/skills/rigor-type-oracle/SKILL.md +26 -1
  266. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  267. metadata +48 -2
  268. data/lib/rigor/inference/memo_audit.rb +0 -55
@@ -67,7 +67,7 @@ carries no `documentation_url`.
67
67
  | <a id="rule-flow-return-in-ensure"></a>`flow.return-in-ensure` | An explicit `return` inside an `ensure` clause — it overrides the method's in-flight return value and silently swallows any in-flight exception. A `return` in a nested `def`, lambda, or `define_method` block inside the `ensure` does not fire (it exits that inner frame). | high |
68
68
  | <a id="rule-flow-shadowed-rescue-clause"></a>`flow.shadowed-rescue-clause` | A `rescue` clause can never run because an earlier clause of the same chain already catches a superclass (or the same class) of every exception class it names. | high |
69
69
  | <a id="rule-def-return-type-mismatch"></a>`def.return-type-mismatch` | The method body's result violates its declared RBS return type. | medium |
70
- | <a id="rule-def-ivar-write-mismatch"></a>`def.ivar-write-mismatch` | An instance variable is written with a type disagreeing with its first write, unless its RBS declaration (`@x: T` or an `attr_*` member) admits both writes. With `rigor-actionpack` or `rigor-actionmailer` loaded, writes in two different actions of a controller or mailer are not compared, since Rails runs one action per instance. A write in a private or protected helper, `initialize` or the same action still is, and so is one in a callback target or `helper_method` that a macro in the same file names. A public method named only by a macro in another file or a concern, or reached through a lambda callback, counts as an action, and so does an action another action calls. | high |
70
+ | <a id="rule-def-ivar-write-mismatch"></a>`def.ivar-write-mismatch` | An instance variable is written with a type disagreeing with its first write, unless its RBS declaration (`@x: T` or an `attr_*` member) admits both writes. A write in a block whose `self` is provably another object (`Other.new.instance_eval { @x = 1 }`, `Class.new { … }`, `X.class_eval { … }`, a `class << obj` body) sets that object's variable and is not compared. With `rigor-actionpack` or `rigor-actionmailer` loaded, writes in two different actions of a controller or mailer are not compared, since Rails runs one action per instance. A write in a private or protected helper, `initialize` or the same action still is, and so is one in a callback target or `helper_method` that a macro in the same file names. A public method named only by a macro in another file or a concern, or reached through a lambda callback, counts as an action, and so does an action another action calls. | high |
71
71
  | <a id="rule-def-method-visibility-mismatch"></a>`def.method-visibility-mismatch` | An explicit-receiver call reaches a private method. | high |
72
72
  | <a id="rule-def-override-visibility-reduced"></a>`def.override-visibility-reduced` | An override reduces the visibility it inherits from a project-defined ancestor. | high |
73
73
  | <a id="rule-def-override-return-widened"></a>`def.override-return-widened` | An override's declared return type widens the inherited return (covariance). | high |
@@ -75,9 +75,10 @@ carries no `documentation_url`.
75
75
  | <a id="rule-global-write-type-mismatch"></a>`global.write-type-mismatch` | A special global is assigned a literal its setter rejects, so the write raises `TypeError` every time it runs. `$/`, `$-0`, `$,` and `$\` take only a String or nil (`$/ = 1` and `$/ = /x/` report). `$;` and `$-F` take a String, a Regexp, nil, or an object with `to_str`. `$~` takes a MatchData or nil. `$0` and `$PROGRAM_NAME` take a String or an object with `to_str`, so `$0 = nil` and `$0 = :name` report. `$.` takes an Integer, a Float, or an object with `to_int`, so `$. = "3"` reports and `$. = 3r` does not. `$-i` takes a String, nil, false, or an object with `to_str`. `$stdout`, `$>` and `$stderr` take anything that responds to `write`: `$stdout = 1` reports, `$stdout = StringIO.new` does not. Ruby's setter decides, not the global's RBS type. Only a value written as a literal is judged — a number, a string, a symbol, an array, a hash or a regexp literal (interpolated or not), or `nil`, `true` or `false` — so a variable, a method call or a constant never reports, whatever its type. A literal stays silent when your program defines the method the setter asks for (`write`, `to_str`, `to_int`) or a `method_missing` / `respond_to_missing?` / `respond_to?` anywhere, in any spelling and on any class — Rigor cannot always tell which objects such a definition reaches, so it does not try — or when a top-level `include` / `extend` or an ancestor's `include` names a module Rigor has no RBS for. For `$stdout`, `$>` and `$stderr` it also stays silent where a `using` is in effect for a refinement that may add `write`. `$/`, `$-0`, `$,`, `$\` and `$~` accept only their classes, so none of that silences them. A write to `$stdin` is never checked, and a special any file aliases (`alias $stdout $out`, including a `pre_eval:` file) is exempt. `warning` under `lenient`. | high |
76
76
  | <a id="rule-global-readonly-write"></a>`global.readonly-write` | A read-only special global is written — `$!`, `$$`, `$?`, `$<`, `$FILENAME`, `$*`, `$:` / `$LOAD_PATH` / `$-I`, `$"` / `$LOADED_FEATURES`, `$-W`, `$-p`, `$-l` or `$-a` — which raises `NameError` whatever the value. Covers `$g = value`, `$g += value` and a multiple-assignment target. `$LOAD_PATH ||= []` never writes and `$LOAD_PATH << dir` is a method call, so neither fires. A special any file aliases (`alias $! $err`) is exempt. An error in every profile. | high |
77
77
  | <a id="rule-static-value-use-void"></a>`static.value-use.void` | A value recovered from an author-declared `-> void` return is used in value context (an assignment right-hand side, a call receiver, or a call argument). Off by default; reaches a run only through the `use-of-void-value` bleeding-edge feature (ADR-100). A bare-statement `void` call and a legitimate `top` value both stay silent. | high |
78
+ | <a id="rule-plugin-api-surface"></a>`plugin.api-surface` | Plugin code (inside a class that inherits `Rigor::Plugin::Base`) calls a member that a `Rigor::Plugin` interface (`_Scope`, `_Environment`, `_Services`, `_FactStore`, `_Configuration`) does not declare, on a value that interface types: a hook's `scope`, `scope.environment`, and so on. The engine object answers the call today, but it is outside the plugin API Rigor promises to keep, so call a member the interface declares (`sig/rigor/plugin/interfaces.rbs`) or a public namespace instead. Off by default; reaches a run through the `plugin-api-surface` bleeding-edge feature (ADR-128), or a `severity_overrides:` key naming `plugin.api-surface` itself (a bare `plugin` key does not reach it). Calls of members every object has (`nil?`, `respond_to?`) and values that lost the interface type (a helper parameter with no signature, a join with an untyped value) stay silent; a call a `respond_to?` guard protects still reports. | high |
78
79
  | <a id="rule-effect-envelope-exceeded"></a>`effect.envelope-exceeded` | A method performs an effect its declared envelope does not admit — its proven effect labels (its own body plus everything it calls) are not covered by the `%a{pure}` or `%a{rigor:v1:effect …}` bound written on it or on its class. Opt-in twice over: it needs an `effects:` block in `.rigor.yml` and an envelope you wrote. Positioned at the Ruby `def`. Unproven ("and possibly more") effects never fire, and `mutate.local` is tolerated by every envelope. | high |
79
80
  | <a id="rule-effect-liskov-widened"></a>`effect.liskov-widened` | An override escapes the envelope written on the method it overrides. A `PgRepo` is usable wherever a `Repo` is, so a `%a{rigor:v1:effect io.db}` on `Repo#find` binds `PgRepo#find` too: an implementation may be purer than the bound it inherits, never less pure. Either what the override *does* exceeds the inherited bound, or the envelope the override *declares for itself* is wider than it. Both sides must be authored — nothing fires unless someone wrote an envelope on the ancestor — and only subclassing counts, not `include`. Positioned at the override's `def`. Needs an `effects:` block. | high |
80
- | <a id="rule-effect-unknown-label"></a>`effect.unknown-label` | An effect declaration names a label the registry does not know — a typo in an envelope (`%a{rigor:v1:effect io.bd}`), or a member of `effects.tolerated:`. The whole tag then reads as unbounded, so the declaration quietly stops doing anything; this says so. Positioned at the declaration: the `.rbs` line, the `.rb` line for an rbs-inline annotation, or `.rigor.yml` for a config value. `# rigor:disable` comments are not read out of `.rbs` or `.rigor.yml`, so use `disable:` or the baseline there. Only fires where the spelling is evidently meant to be a label (close to a known one, next to a known one, dotted, or retired) — a word nothing resembles stays silent, because you may be opening your own vocabulary. Needs an `effects:` block. | high |
81
+ | <a id="rule-effect-unknown-label"></a>`effect.unknown-label` | An effect declaration names a label the registry does not know — a typo in an envelope (`%a{rigor:v1:effect io.bd}`), or a member of `effects.tolerated:`. The whole tag then reads as unbounded, so the declaration quietly stops doing anything; this says so. Positioned at the declaration: the `.rbs` line, the `.rb` line for an rbs-inline annotation, or the loaded configuration file (`.rigor.yml` when none was loaded) for a config value. `# rigor:disable` comments are not read out of `.rbs` or `.rigor.yml`, so use `disable:` or the baseline there. Only fires where the spelling is evidently meant to be a label (close to a known one, next to a known one, dotted, or retired) — a word nothing resembles stays silent, because you may be opening your own vocabulary. Needs an `effects:` block. | high |
81
82
  | <a id="rule-effect-annotations-unchecked"></a>`effect.annotations-unchecked` | Your signatures carry `%a{pure}` / `%a{rigor:v1:effect …}` but `.rigor.yml` has no `effects:` block, so nothing checks them. One `:info` per run, positioned at the first annotation. An annotation never turns effect collection on by itself — that would make one line in one file more expensive for every run — so this is how it tells you instead. Add `effects: {}` to opt in, or `disable:` it to keep the annotations documentary. Reads both annotation lanes — `sig/*.rbs` and rbs-inline comments — on every run, a warm `--incremental` with nothing changed included. | — |
82
83
  | <a id="rule-rbs-contradicting-signature"></a>`rbs.contradicting-signature` | A method is declared both in your `sig/` and by an inline `# @rbs` / `#:` annotation, and the two provably contradict — no value or call satisfies both: in the return or a parameter every call must pass, the two types share no value (`::String` against `::Integer`). Only absolutely written Ruby core or stdlib classes count: a module such as `Comparable`, a class your project declares, a gem's class, or a relative name like plain `String` never does, or two keyword-free declarations accept positional counts that cannot meet, or one requires a keyword the other cannot take in any form. Overloads are paired by correspondence, not order. Also fires when a member-level `%a{rigor:v1:return: …}` / `%a{rigor:v1:param: …}` refinement shares no value with the type its own member declares. Positioned at the `.rbs` member; Rigor reads that declaration. A stale generated signature is the usual cause: regenerate it, or fix the annotation. Two declarations where one refines the other merge to the more precise one without a word, and a pair Rigor cannot rank drops the inline side with a `source-rbs-annotation-not-honoured` `:info` instead ([Precedence](plugins/rigor-rbs-inline.md#precedence)). An error in every profile. | high |
83
84
  | <a id="rule-suppression-unknown-rule"></a>`suppression.unknown-rule` | A `# rigor:disable[-file]` comment names a rule that does not exist (typically a typo), so the suppression silently does nothing. `plugin.`-prefixed tokens are never flagged. | high |
@@ -130,7 +131,7 @@ re-stamps it for the run. Three profiles, set with the
130
131
  | --- | --- |
131
132
  | `lenient` | Only proven diagnostics are errors; uncertain ones drop to `warning` / `info`. For incremental adoption on legacy code. |
132
133
  | `balanced` *(default)* | Most rules `error`; `dump.type` `info`; uncertain rules `warning`. |
133
- | `strict` | Nearly every rule is an `error`. The exceptions: `call.self-undefined-method` and `static.value-use.void` stay `off` (both opt-in only), `flow.unreachable-clause` stays `warning` pending its false-positive gate, and the three `suppression.*` rules stay `warning` — a stale suppression comment is worth telling you about, but it is not a reason to fail a build. CI-friendly. |
134
+ | `strict` | Nearly every rule is an `error`. The exceptions: `call.self-undefined-method`, `static.value-use.void` and `plugin.api-surface` stay `off` (all three opt-in only), `flow.unreachable-clause` stays `warning` pending its false-positive gate, and the three `suppression.*` rules stay `warning` — a stale suppression comment is worth telling you about, but it is not a reason to fail a build. CI-friendly. |
134
135
 
135
136
  Under `balanced`, the rules that do **not** emit as `error`
136
137
  are:
@@ -139,7 +140,7 @@ are:
139
140
  | --- | --- |
140
141
  | `warning` | `call.unresolved-toplevel`, `def.ivar-write-mismatch`, `def.return-type-mismatch`, `def.override-visibility-reduced`, `def.override-return-widened`, `def.override-param-narrowed`, `flow.unreachable-branch`, `flow.always-truthy-condition`, `flow.dead-assignment`, `flow.duplicate-hash-key`, `flow.return-in-ensure`, `flow.shadowed-rescue-clause`, `suppression.unknown-rule`, `suppression.empty`, `suppression.unknown-marker`, `effect.envelope-exceeded`, `effect.liskov-widened` |
141
142
  | `info` | `flow.unreachable-clause`, `dump.type`, `effect.unknown-label`, `effect.annotations-unchecked` |
142
- | `off` | `call.self-undefined-method`, `static.value-use.void` |
143
+ | `off` | `call.self-undefined-method`, `static.value-use.void`, `plugin.api-surface` |
143
144
 
144
145
  Everything else emits as `error`. For one rule under all three
145
146
  profiles, `rigor explain <rule>` prints `Authored severity:` and
@@ -80,6 +80,17 @@ is not repeated here.)
80
80
  minimal hand-RBS residual, under a double gate (the site becomes
81
81
  protected *and* `rigor check` gains no new diagnostic). See
82
82
  [Type-protection coverage](15-type-protection-coverage.md).
83
+ - **`rigor-rbs-contract`** — audits, repairs, and extends RBS
84
+ declarations as *contracts*: a gem's own `sig/`, an application's
85
+ `sig/`, or an entry contributed to `ruby/gem_rbs_collection`. It finds
86
+ where a declaration and the code disagree (`rigor check`'s
87
+ `def.return-type-mismatch`, `rigor sig-gen --diff`, use-site checks,
88
+ the library's documentation), changes a declaration only on cited
89
+ evidence, keeps a declared `void` unless the library documents a
90
+ value, and proves each change with a use-site test. On your own code
91
+ it asks you the contract questions the evidence leaves open; on a gem
92
+ you do not own, the gem's documentation decides. See
93
+ [ADR-131](../adr/131-declarations-are-contracts.md).
83
94
  - **`rigor-baseline-reduce`** — works an existing `.rigor-baseline.yml`
84
95
  down rule by rule. It prioritises with `rigor triage`, classifies each
85
96
  site as a real bug / safe stylistic finding / false positive, and
@@ -35,6 +35,14 @@ install channels in [Installing Rigor](01-installation.md) provide it;
35
35
  `mise` is the recommended one because its shims make `rigor` available
36
36
  to GUI-launched editors that do not inherit your shell environment.
37
37
 
38
+ The [VS Code extension](../../editors/vscode/README.md) also finds a
39
+ mise or asdf shim when `rigor` is not on `PATH`. When it finds no
40
+ `rigor` at all, it offers to install a **managed Rigor** into its own
41
+ storage with a Ruby 4.0 already on the machine, and uses it as its
42
+ last resort; a project that pins `rigortype` in `Gemfile.lock`,
43
+ `mise.toml` or `.tool-versions` is never served by it. See the
44
+ extension README's *Server discovery*.
45
+
38
46
  Do **not** add `rigortype` to your project's `Gemfile`. Rigor is a
39
47
  tool, not a library — installing it standalone keeps its Ruby version
40
48
  and its dependencies out of your application's resolution. See
@@ -61,7 +61,7 @@ mise use gem:rigortype
61
61
  ```
62
62
 
63
63
  `mise use` records the chosen versions in a `mise.toml` in the
64
- current directory and installs them in one step. Verify:
64
+ current directory and installs them in one step. Rails projects with Steep or `rbs_rails` list `rbs` in the bundle, and the `rbs` that `gem:rigortype` puts on `PATH` can shadow it; see the [install guide's note on `rbs` shadowing](../install.md#case-a--mise-is-available-recommended) before running `bundle exec rbs`. Verify:
65
65
 
66
66
  ```sh
67
67
  rigor --version
@@ -108,10 +108,9 @@ annotations and no block gets told so, once per run:
108
108
  sig/slug.rbs:2:1: info: Effect annotations (`%a{pure}` / `%a{rigor:v1:effect …}`) are present in your project's signatures, but `.rigor.yml` carries no `effects:` block, so effect collection never runs and nothing checks them — they are documentation, not a contract. Add `effects: {}` to have Rigor prove what your methods do and check these bounds against it (ADR-103); an annotation alone never turns collection on, because that would make one line in one signature file more expensive for every run of the project.
109
109
  ```
110
110
 
111
- From **v0.4.0** collection is the default: a config with no `effects:` key at
112
- all behaves as `effects: {}`, and `effects: false` is how you opt out. On the
113
- `0.3.x` line you can preview that with
114
- `bleeding_edge: [effects-on-by-default]`.
111
+ From **v0.5.0** collection is the default: a config with no `effects:` key at
112
+ all behaves as `effects: {}`, and `effects: false` is how you opt out. Until
113
+ then you can preview that with `bleeding_edge: [effects-on-by-default]`.
115
114
 
116
115
  One exception to all of this: `rigor effects` — the bare report — runs under an
117
116
  implicit empty block whether or not you configured one, so you can look before
@@ -399,12 +399,21 @@ diff — not a policy scoped to `app/views/**/*`. See
399
399
  those producers widens what this plugin can check.
400
400
  - **`params[:key]` is a gradual carrier.** Inside a controller,
401
401
  `params` types as `ActionController::Parameters`, and so does the
402
- result of every builder method that always returns one — `require`,
403
- `permit`, `permit!`, `expect`, `slice`, `slice!`, `except`,
404
- `without`, `extract!`, `merge`, `merge!`, `reverse_merge`,
405
- `reverse_merge!`, `with_defaults`, `with_defaults!`, `compact`,
406
- `compact_blank`, `deep_dup` — so a chain built from them keeps a
407
- concrete receiver throughout. A subscript read answers
402
+ result of every builder method that always returns one — `permit`,
403
+ `permit!`, `slice`, `slice!`, `except`, `without`, `extract!`,
404
+ `merge`, `merge!`, `reverse_merge`, `reverse_merge!`,
405
+ `with_defaults`, `with_defaults!`, `compact`, `compact_blank`,
406
+ `deep_dup` — when the receiver is `params` itself. A carrier
407
+ (`params[:tags]`) may hold an Array or a String at runtime, so a
408
+ builder call on one answers the carrier too, except `permit` and
409
+ `permit!`, which no core class defines and which answer `Parameters`.
410
+ `require`
411
+ and `expect` are different: Rails returns the value itself, which
412
+ can be a scalar (`params.require(:id).to_sym`), so they answer the
413
+ same `Dynamic[ActionController::Parameters]` carrier (#2947). A
414
+ `permit` or `permit!` after one (`params.require(:user).permit(:name)`)
415
+ still types as `Parameters`; other chain methods off a carrier stay
416
+ the carrier. A subscript read answers
408
417
  `Dynamic[ActionController::Parameters | nil]` (#2430): the static
409
418
  facet is the nilable `Parameters`, so `params[:user][:name]` and
410
419
  `params[:user].permit(:name)` keep dispatching on that facet, and
@@ -53,6 +53,9 @@ Roughly the top ~40 selectors plus their close neighbours, across:
53
53
  `TrueClass` / `FalseClass`).
54
54
  - **Integer / Float** — Duration multipliers (`#days`, `#hours`,
55
55
  `#minutes`, …) and Bytes multipliers (`#megabytes`, `#gigabytes`, …).
56
+ - **Integer / Float / BigDecimal** — `#to_fs` / `#to_formatted_s`
57
+ (`NumericWithFormat`: a Symbol format, an Integer radix or a String
58
+ format, and the no-argument `to_s` form).
56
59
  - **String** — inflections (`#underscore`, `#camelize`, `#classify`,
57
60
  `#constantize`, `#pluralize`, …), filters (`#squish`, `#truncate`),
58
61
  `#html_safe`, `#starts_with?` / `#ends_with?` (also on **Symbol**),
@@ -61,9 +61,23 @@ The plugin also types the annotation DSL's own expressions
61
61
  `T::Struct` subclass still needs the subclass's own `extend T::Sig`,
62
62
  matching the runtime) through a bundled
63
63
  `sorbet-runtime` RBS surface — so the sig DSL no longer reads as
64
- `Dynamic[top]` on a Sorbet codebase. What it does not model:
65
- `T::Struct` does not synthesise `prop`-declared readers (a
66
- `Doc#name` accessor still reads opaque), `include T::Props` does
64
+ `Dynamic[top]` on a Sorbet codebase.
65
+
66
+ On a `T::Struct`, `T::InexactStruct` or `T::ImmutableStruct`
67
+ subclass, `prop :name, Type` and `const :name, Type` at the top
68
+ level of the class body give `obj.name` the translated `Type`
69
+ (`T.nilable(String)` reads `String?`; a form outside the table
70
+ reads `Dynamic[top]`), and `prop` also answers `obj.name = v`.
71
+ A relative constant in `Type` resolves from the class body's
72
+ namespaces outward, and one that names no known class reads
73
+ `Dynamic[top]`.
74
+ A `const` has no writer, so `obj.name = v` still reports.
75
+ Subclasses inherit the accessors, a `def` of the same name in
76
+ the class body wins over the macro, and a `prop` nested under an
77
+ `if` or a block reads `Dynamic[top]`. A `prop` call in a class
78
+ outside the struct family defines nothing.
79
+
80
+ What it does not model: `include T::Props` does
67
81
  not confer the `ClassMethods` surface (only the `< T::Struct`
68
82
  subclass path does), and the deeper type-member machinery
69
83
  (`T.attached_class` variance, `type_member` bounds) is not
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../../reflection"
6
+ require_relative "../../source/node_children"
7
+ require_relative "../../type"
8
+
9
+ module Rigor
10
+ module Analysis
11
+ module CheckRules
12
+ # Issue #3036 — answers, for a call node `flow.always-raises` proved raises, whether an enclosing
13
+ # `begin` (or `def … rescue … end`, which Prism wraps in the same `BeginNode`) catches the raise
14
+ # before it escapes. When a rescue clause may catch the raised class the exception flows to the
15
+ # handler, so the rule stays silent.
16
+ #
17
+ # The check is deliberately conservative in BOTH directions:
18
+ #
19
+ # - it suppresses only when a clause NAMES a class the raised class orders at-or-below
20
+ # (`rescue ZeroDivisionError`, `rescue StandardError`, `rescue Exception`, or a bare `rescue`,
21
+ # which is `StandardError`), resolved through the typer so lexical constant lookup applies. An
22
+ # exception reference that is not a constant (`rescue *ERRORS`, `rescue klass`) contributes no
23
+ # coverage, and an unresolvable / `:unknown` ordering keeps the diagnostic — a wrong silence is
24
+ # worse than a kept finding.
25
+ # - it never crosses a body boundary ({BODY_BOUNDARIES}): a `def`, `->`, or block body runs when
26
+ # called, not while the `begin` body evaluates, and `BEGIN` / `END` blocks run on their own
27
+ # schedule, so an outer rescue cannot be proven to see their raise. Class / module bodies are
28
+ # NOT boundaries — they evaluate inline, so their raises DO reach the enclosing rescue.
29
+ #
30
+ # Prism nodes carry no parent pointers, so ancestry is answered from a `node => parent` table
31
+ # built lazily on the first query: a file with no zero-division candidate never pays for it.
32
+ class AlwaysRaisesShield
33
+ # The exception class the rule proves today — Integer `/# % div modulo divmod` by a literal
34
+ # zero divisor. A rescue catches it when the clause names the class itself or an ancestor.
35
+ RAISED_CLASS = "ZeroDivisionError"
36
+
37
+ # The implicit class of a bare `rescue` / `rescue => e`.
38
+ BARE_RESCUE_CLASS = "StandardError"
39
+
40
+ # Node classes whose bodies do not execute inside the enclosing `begin` — see the class
41
+ # comment. Mirrors the deferred-body set `Inference::ScopeIndexer` already tracks.
42
+ BODY_BOUNDARIES = [
43
+ Prism::DefNode, Prism::LambdaNode, Prism::BlockNode,
44
+ Prism::PreExecutionNode, Prism::PostExecutionNode
45
+ ].freeze
46
+
47
+ def initialize(root)
48
+ @root = root
49
+ @parent_map = nil
50
+ end
51
+
52
+ # True when `node` sits in the protected body of a `begin` whose rescue chain may catch
53
+ # {RAISED_CLASS}. `scope` is the scope recorded for `node`; it resolves the clause's exception
54
+ # constants lexically. A nil scope or a node outside `root`'s subtree answers false.
55
+ def rescued?(node, scope)
56
+ return false if scope.nil?
57
+
58
+ parents = parent_map
59
+ child = node
60
+ current = parents[node]
61
+ while current
62
+ return true if protected_body?(current, child, scope)
63
+ return false if BODY_BOUNDARIES.any? { |klass| current.is_a?(klass) }
64
+
65
+ child = current
66
+ current = parents[current]
67
+ end
68
+ false
69
+ end
70
+
71
+ private
72
+
73
+ # True when `node` is a `begin` whose protected body (`statements`) contains `child` — the
74
+ # ascent arrived through the body rather than through a rescue / else / ensure clause, whose
75
+ # raises this `begin` does not catch — and whose rescue chain may catch {RAISED_CLASS}. When
76
+ # the chain does NOT catch, the ascent continues: an outer `begin` may still.
77
+ def protected_body?(node, child, scope)
78
+ return false unless node.is_a?(Prism::BeginNode)
79
+ return false unless child.equal?(node.statements)
80
+ return false if node.rescue_clause.nil?
81
+
82
+ rescue_chain_catches?(node.rescue_clause, scope)
83
+ end
84
+
85
+ # Any clause of the `rescue … rescue …` chain may catch — clauses are tried in order and the
86
+ # first match wins, so one catching clause is enough.
87
+ def rescue_chain_catches?(clause, scope)
88
+ while clause
89
+ return true if rescue_clause_catches?(clause, scope)
90
+
91
+ clause = clause.subsequent
92
+ end
93
+ false
94
+ end
95
+
96
+ # One `rescue` clause catches {RAISED_CLASS} when any class it names orders at-or-above the
97
+ # raised class. A bare `rescue` names {BARE_RESCUE_CLASS}.
98
+ def rescue_clause_catches?(clause, scope)
99
+ exceptions = clause.exceptions
100
+ return class_catches?(BARE_RESCUE_CLASS, scope) if exceptions.empty?
101
+
102
+ exceptions.any? do |exception|
103
+ class_name = exception_class_name(exception, scope)
104
+ class_name && class_catches?(class_name, scope)
105
+ end
106
+ end
107
+
108
+ # The class a rescue exception reference names, or nil when it is not a constant path (a splat,
109
+ # a dynamic expression) or does not resolve to a class object. Only `Type::Singleton` counts:
110
+ # the rescue operand is the class itself, and an instance-typed constant cannot be proven to
111
+ # match.
112
+ def exception_class_name(node, scope)
113
+ return nil unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
114
+
115
+ type = scope.type_of(node)
116
+ type.class_name if type.is_a?(Type::Singleton)
117
+ end
118
+
119
+ # `class_ordering(raised, rescued)`: `:equal` / `:subclass` mean the rescued class IS the raised
120
+ # class or an ancestor of it — the clause catches. `:superclass`, `:disjoint`, and `:unknown`
121
+ # all keep the diagnostic.
122
+ def class_catches?(rescued_class, scope)
123
+ %i[equal subclass].include?(
124
+ Rigor::Reflection.class_ordering(RAISED_CLASS, rescued_class, scope: scope)
125
+ )
126
+ end
127
+
128
+ # `node => parent` for every descendant of `root`, built on first use. Iterative so a deeply
129
+ # nested file cannot overflow the stack the way a recursive walk could.
130
+ def parent_map
131
+ @parent_map ||= begin
132
+ parents = {}
133
+ stack = [@root]
134
+ until stack.empty?
135
+ node = stack.pop
136
+ node.rigor_each_child do |child|
137
+ parents[child] = node
138
+ stack << child
139
+ end
140
+ end
141
+ parents
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Analysis
5
+ module CheckRules
6
+ # Issue #2786 — what the per-call-node rules ({CheckRules.call_node_diagnostics}) each derived on their own:
7
+ # the call node's index scope, the type of its receiver under that scope, and the RBS signature the call
8
+ # resolves to on that receiver. One object per call node is built before the rules run and handed to every
9
+ # rule that reads them.
10
+ #
11
+ # The receiver type is computed lazily, on the first rule that asks, from the same `(scope, node)` pair each
12
+ # rule typed before: `scope_index[call_node]` and `call_node.receiver`. Every later rule reads that value
13
+ # instead of re-typing the receiver chain. This is not a memo over `type_of`: nothing is served across call
14
+ # nodes or across scopes, and no fact is computed anywhere but at the first point a rule computed it before.
15
+ # It rests on the `Scope#type_of` contract that one `(scope, node)` pair types structurally equal within a run
16
+ # *under the same ambient inputs* (`docs/internal-spec/inference-engine.md`), which holds here because of
17
+ # an invariant of the rule list: between the first rule that types the receiver and the last that reads it, no
18
+ # frame is installed and nothing types a node inside the receiver's subtree under another state or adds an
19
+ # `optimistic_origins` mark there. The other typings in that window are the call's arguments (outside the
20
+ # subtree), `receiver.receiver` under the same scope (which rewrites what the receiver's typing already
21
+ # wrote), and the key-presence re-walk, which runs on `with_isolated_side_tables`. A rule inserted between
22
+ # two readers that breaks this must type the receiver itself.
23
+ #
24
+ # Of the typing, only the receiver is shared. A rule that types a sub-expression (`receiver.receiver`), an
25
+ # argument, or any node under a scope other than the call's keeps its own `type_of` call: arguments are typed
26
+ # from their own index entry by `call.argument-type-mismatch` and from the call's by `flow.always-raises`, and
27
+ # the two meet only on a divisor.
28
+ class CallFacts
29
+ attr_reader :call_node, :scope_index, :scope
30
+
31
+ def initialize(call_node, scope_index)
32
+ @call_node = call_node
33
+ @scope_index = scope_index
34
+ @scope = scope_index[call_node]
35
+ @receiver_type = nil
36
+ @receiver_signature = nil
37
+ @receiver_signature_resolved = false
38
+ end
39
+
40
+ # `scope.type_of(call_node.receiver)`, computed once. Callers check that the call has a receiver and that
41
+ # {#scope} is not nil first, as they did before sharing it.
42
+ def receiver_type
43
+ @receiver_type ||= @scope.type_of(@call_node.receiver)
44
+ end
45
+
46
+ # The signature `CheckRules.receiver_signature` resolves, its block run on the first ask only. A nil answer
47
+ # is remembered too. A block that raises leaves it unresolved, so the next ask raises as it did before.
48
+ def receiver_signature
49
+ return @receiver_signature if @receiver_signature_resolved
50
+
51
+ signature = yield
52
+ @receiver_signature_resolved = true
53
+ @receiver_signature = signature
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -59,6 +59,14 @@ module Rigor
59
59
  end
60
60
  end
61
61
 
62
+ # Issue #2656 — true when `node` reads a local a class guard narrowed from a `Dynamic` value
63
+ # ({Scope#with_gradual_narrowing_mark}). `call.argument-type-mismatch` judges such an argument by its class
64
+ # rather than against the parameter's literals (`CheckRules#gradual_narrowing_accepted?`); the other rules
65
+ # read the narrowed `Nominal` as before.
66
+ def gradual_narrowed?(node, scope)
67
+ node.is_a?(Prism::LocalVariableReadNode) && scope.gradual_narrowing?(node.name)
68
+ end
69
+
62
70
  # The globals whose declared seeds `node` still reads, empty for none: a read of a marked global, or of a
63
71
  # local copied from one or more (`sep = $/`, `Scope#declaration_sourced_global_copies`), bare or in
64
72
  # parentheses (`($stdout)`).
@@ -6,6 +6,7 @@ require_relative "rule_walk"
6
6
  require_relative "../../scope/resolution_chain"
7
7
  require_relative "../../source/constant_path"
8
8
  require_relative "../../source/node_children"
9
+ require_relative "../../inference/census_foreign_block"
9
10
 
10
11
  module Rigor
11
12
  module Analysis
@@ -28,6 +29,9 @@ module Rigor
28
29
  # doesn't yet model.
29
30
  # - Nested classes / modules / defs inside a method body are barriers, mirroring the indexer's
30
31
  # `IVAR_BARRIER_NODES` policy.
32
+ # - Issue #2943: a write directly in a block or `class << obj` body whose `self` is provably not an instance
33
+ # of the class (`Other.new.instance_eval { @r = 1 }`), by the class-ivar census's rule
34
+ # ({Inference::CensusForeignBlock}).
31
35
  class IvarWriteCollector
32
36
  BARRIER_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode].freeze
33
37
  private_constant :BARRIER_NODES
@@ -156,7 +160,8 @@ module Rigor
156
160
 
157
161
  def collect_def_writes(def_node, qualified_prefix)
158
162
  return if def_node.body.nil? || qualified_prefix.empty?
159
- return if def_node.receiver.is_a?(Prism::SelfNode)
163
+ # A def with a receiver writes the ivars of another object than an instance (#3155).
164
+ return if def_node.receiver
160
165
 
161
166
  class_name = qualified_prefix.join("::")
162
167
  gather_writes(def_node.body, class_name, def_node.name)
@@ -168,7 +173,13 @@ module Rigor
168
173
  record_write(node, class_name, method_name) if node.is_a?(Prism::InstanceVariableWriteNode)
169
174
  return if BARRIER_NODES.any? { |klass| node.is_a?(klass) }
170
175
 
171
- node.rigor_each_child { |child| gather_writes(child, class_name, method_name) }
176
+ # #2943 — a block or `class <<` body whose `self` is provably another object writes that object's ivars, as
177
+ # the class-ivar census reads it ({Inference::CensusForeignBlock}); a `define_method` block nested in it that
178
+ # runs on an instance again still counts. A `def` it reenters stays a barrier here.
179
+ children = Inference::CensusForeignBlock.census_children(node, class_name)
180
+ return node.rigor_each_child { |child| gather_writes(child, class_name, method_name) } unless children
181
+
182
+ children.each { |child| gather_writes(child, class_name, method_name) }
172
183
  end
173
184
 
174
185
  def record_write(node, class_name, method_name)
@@ -5,6 +5,7 @@ require "prism"
5
5
  require_relative "../../inference/scope_indexer"
6
6
  require_relative "../../source/node_children"
7
7
  require_relative "rebinding_block_callees"
8
+ require_relative "replaced_singleton_defs"
8
9
 
9
10
  module Rigor
10
11
  module Analysis
@@ -42,6 +43,11 @@ module Rigor
42
43
 
43
44
  def refinements = (@refinements ||= Inference::InEffectRefinements.new(@root))
44
45
 
46
+ # Issue #3155 — the file's singleton defs a later certain def replaces, for `def.return-type-mismatch`.
47
+ def replaced_singleton_defs(scope_index)
48
+ @replaced_singleton_defs ||= ReplacedSingletonDefs.new(@root, scope_index)
49
+ end
50
+
45
51
  # Issue #1817 — the file's block-passing calls, for `call.unresolved-toplevel`'s rebinding-callee decline.
46
52
  def block_calls = (@block_calls ||= RebindingBlockCallees::Sites.new(@root))
47
53