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
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../../configuration/severity_profile"
6
+ require_relative "../../reflection"
7
+ require_relative "../../source/node_walker"
8
+ require_relative "../../scope/resolution_chain"
9
+ require_relative "../../type"
10
+ require_relative "../diagnostic"
11
+ require_relative "rule_ids"
12
+
13
+ module Rigor
14
+ module Analysis
15
+ module CheckRules
16
+ # ADR-128 WD6 / ADR-130 WD3 — `plugin.api-surface`. A call in plugin source on a value typed by a
17
+ # `Rigor::Plugin` interface (`Dynamic[Rigor::Plugin::_Scope]`, ...) to a member that interface does not
18
+ # declare. The method exists on the backing engine class, so the call works today; it is outside what Rigor
19
+ # promises to keep, so `call.undefined-method` would be wrong and this rule says that instead.
20
+ #
21
+ # Authored `:warning`, resolved `:off` by every profile and promoted only by the `plugin-api-surface`
22
+ # bleeding-edge feature (ADR-50 Decision 3, WD2), as `static.value-use.void` is.
23
+ #
24
+ # It fires only where all of these hold:
25
+ #
26
+ # - the call has an explicit receiver, typed `Dynamic[<Rigor::Plugin interface>]`, or that plus `nil` (the
27
+ # facet survives only where ADR-130 WD1 keeps it; a value that lost it at a join reads `Dynamic[top]`);
28
+ # - the interface declares no member of the name, and `Object` (with `Kernel` and `BasicObject`) declares
29
+ # none either: `nil?`, `respond_to?` and `is_a?` are every object's surface, not the engine's;
30
+ # - the call sits lexically inside a class whose discovered superclass chain reaches `Rigor::Plugin::Base`.
31
+ # `Base` itself is not such a class, so its own hooks, which hand interface values to engine helpers
32
+ # (`environment.class_ordering`), are not judged: they are engine code.
33
+ #
34
+ # The plugin-source test reads the class declarations, not a path: a third-party plugin gem keeps its plugin
35
+ # under `lib/` as Rigor's own engine does.
36
+ #
37
+ # The rule runs only when its resolved severity is not `:off` ({.active?}): the runner then adds its
38
+ # {Collector} to the shared {RuleWalk}. A default run builds no collector, so it types no extra receiver and
39
+ # files no ADR-46 dependency for the rule.
40
+ module PluginApiSurface
41
+ PLUGIN_BASE = "Rigor::Plugin::Base"
42
+
43
+ # One call the rule reports: the call and the first interface its receiver carries.
44
+ Result = Data.define(:call_node, :interface)
45
+
46
+ # The {RuleWalk} collector: one {Result} per reported call, in walk order.
47
+ class Collector
48
+ NODE_CLASSES = [Prism::CallNode].freeze
49
+
50
+ def initialize(scope_index)
51
+ @scope_index = scope_index
52
+ @results = []
53
+ end
54
+
55
+ # The legacy single-collector walk, the oracle the ADR-53 equivalence harness compares {RuleWalk} with.
56
+ def collect(root)
57
+ Source::NodeWalker.each(root) { |node| visit(node) if node.is_a?(Prism::CallNode) }
58
+ @results.freeze
59
+ end
60
+
61
+ def visit(node, _context = nil)
62
+ interface = PluginApiSurface.outside_interface(node, @scope_index[node])
63
+ @results << Result.new(call_node: node, interface: interface) if interface
64
+ end
65
+
66
+ def results
67
+ @results.freeze
68
+ end
69
+ end
70
+
71
+ module_function
72
+
73
+ # Whether the rule can report in a run under `configuration`: it is not in `disable:`, and its resolved
74
+ # severity is not `:off`. Default profiles resolve it `:off`; the `plugin-api-surface` bleeding-edge feature
75
+ # or an explicit `severity_overrides:` entry for the id turns it on.
76
+ def active?(configuration)
77
+ disabled = configuration.disabled_rules
78
+ return false if disabled.include?(RULE_PLUGIN_API_SURFACE) || disabled.include?("all")
79
+
80
+ Configuration::SeverityProfile.resolve(
81
+ rule: RULE_PLUGIN_API_SURFACE, authored_severity: :warning,
82
+ profile: configuration.severity_profile, overrides: configuration.severity_overrides,
83
+ bleeding_edge_overrides: configuration.bleeding_edge_severity_overrides
84
+ ) != :off
85
+ end
86
+
87
+ # The interface the call's receiver carries when the call is outside it, or nil.
88
+ def outside_interface(call_node, scope)
89
+ return nil if call_node.receiver.nil? || scope.nil?
90
+
91
+ interfaces = receiver_interfaces(scope.type_of(call_node.receiver))
92
+ return nil if interfaces.nil?
93
+
94
+ environment = scope.environment
95
+ method_name = call_node.name
96
+ return nil if interfaces.any? { |interface| declares?(environment, interface, method_name) }
97
+ return nil if object_member?(environment, method_name)
98
+ return nil unless plugin_source?(scope)
99
+
100
+ interfaces.first
101
+ end
102
+
103
+ def diagnostics(path, results)
104
+ results.map { |result| build(path, result.call_node, result.interface, result.call_node.name) }
105
+ end
106
+
107
+ # The interface facets the receiver carries, or nil when it is anything but `Dynamic[Interface]` arms and
108
+ # `nil`. A union whose non-nil arms include another type has a member the call may resolve on.
109
+ def receiver_interfaces(type)
110
+ case type
111
+ when Type::Dynamic
112
+ facet = type.static_facet
113
+ [facet] if facet.is_a?(Type::Interface)
114
+ when Type::Union
115
+ union_interfaces(type)
116
+ end
117
+ end
118
+
119
+ def union_interfaces(union)
120
+ interfaces = []
121
+ union.members.each do |member|
122
+ next if nil_type?(member)
123
+ return nil unless member.is_a?(Type::Dynamic) && member.static_facet.is_a?(Type::Interface)
124
+
125
+ interfaces << member.static_facet
126
+ end
127
+ interfaces.empty? ? nil : interfaces
128
+ end
129
+
130
+ def nil_type?(type)
131
+ (type.is_a?(Type::Constant) && type.value.nil?) ||
132
+ (type.is_a?(Type::Nominal) && type.class_name == "NilClass")
133
+ end
134
+
135
+ # An interface that does not resolve declares nothing the rule can read, so it is treated as declaring
136
+ # every member. A guard: the carrier exists only for an interface the loader translated, so no fixture
137
+ # reaches it.
138
+ def declares?(environment, interface, method_name)
139
+ definition = environment.rbs_loader.interface_definition(interface.name)
140
+ return true if definition.nil?
141
+
142
+ definition.methods.key?(method_name.to_sym)
143
+ end
144
+
145
+ def object_member?(environment, method_name)
146
+ !Rigor::Reflection.instance_method_definition("Object", method_name, environment: environment).nil?
147
+ end
148
+
149
+ # Whether the call is plugin source: lexically inside a class that inherits `Rigor::Plugin::Base`. A
150
+ # `Rigor::Plugin::Base` body is not: its chain holds `Base` only as itself. An unrecorded nesting (a scope
151
+ # built outside a declaration walk) is not either.
152
+ def plugin_source?(scope)
153
+ nesting = scope.lexical_nesting
154
+ return false if nesting.nil? || nesting.empty?
155
+
156
+ nesting.any? { |name| inherits_plugin_base?(scope, name) }
157
+ end
158
+
159
+ # Whether `class_name`'s instance chain ({Scope::ResolutionChain}, the one ancestry reader) holds
160
+ # `Rigor::Plugin::Base` below the class itself. Reading it files the ADR-46 class dependencies, so
161
+ # an incremental recheck re-judges the call when a declaration on the chain changes in another file. A
162
+ # truncated chain reads as no plugin: the rule withholds where it cannot see the whole chain.
163
+ def inherits_plugin_base?(scope, class_name)
164
+ root = class_name.to_s
165
+ chain = Scope::ResolutionChain.for(scope, root, :instance, :methods)
166
+ chain.record(scope)
167
+ return false if chain.truncated?
168
+
169
+ chain.entries.any? { |entry| entry.name != root && plugin_base_entry?(entry) }
170
+ end
171
+
172
+ # `Rigor::Plugin::Base` is external where only its RBS and the gem declare it, and a project entry where the
173
+ # analysed tree holds its source (Rigor's own `lib`, or a reopen).
174
+ def plugin_base_entry?(entry)
175
+ entry.external? ? entry.candidates.include?(PLUGIN_BASE) : entry.name == PLUGIN_BASE
176
+ end
177
+
178
+ def build(path, call_node, interface, method_name)
179
+ Diagnostic.from_message_loc(
180
+ call_node,
181
+ rule: RULE_PLUGIN_API_SURFACE,
182
+ path: path,
183
+ message: "`#{method_name}' is not part of the plugin API: `#{interface.describe}' does not declare it. " \
184
+ "The engine object answers it today, but Rigor does not promise to keep it; call a member the " \
185
+ "interface declares (sig/rigor/plugin/interfaces.rbs) or a public namespace " \
186
+ "(docs/internal-spec/public-api.md)",
187
+ severity: :warning,
188
+ receiver_type: nil,
189
+ method_name: method_name
190
+ )
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
@@ -115,15 +115,19 @@ module Rigor
115
115
  # exemption exactly rather than on the last segment. A path that renders no qualified name falls back
116
116
  # to its own last segment, which is all there is to ask about.
117
117
  def published_path?(node, scope)
118
- rendered = Source::ConstantPath.qualified_name_or_nil(node) || node.name&.to_s
118
+ rendered = constant_spelling(node)
119
119
  !rendered.nil? && scope.published_constant?(rendered)
120
120
  end
121
121
 
122
122
  # The one hop. Resolves the callee through the ordinary `Scope` accessors (so the ADR-46 recorder sees
123
123
  # the read and the caller gains an edge to the callee — the conservative direction) and scans its
124
124
  # body. Fails soft to false: a resolution this cannot make is not a reason to withhold.
125
+ #
126
+ # Issue #3199 — the empty-table shortcut is taken only when no ADR-46 recorder is watching: the scan's
127
+ # `Scope#published_constant?` questions are what record the `constant:` edges, and a reader that skipped
128
+ # them would not be re-checked when another file publishes a constant the callee's body names.
125
129
  def callee_reads_published_constant?(node, scope)
126
- return false if scope.published_constant_names.empty?
130
+ return false if scope.published_constant_names.empty? && !Analysis::DependencyRecorder.recording?
127
131
 
128
132
  def_node = resolve_callee(node, scope)
129
133
  return false if def_node.nil?
@@ -172,10 +176,30 @@ module Rigor
172
176
  end
173
177
  end
174
178
 
175
- # An explicit worklist rather than recursion, so the node budget is one loop-carried local instead of
176
- # a shared mutable cell (the engine value-pins a one-element Array counter and then self-flags the
177
- # guard's own `negative?` as an always-falsey condition — this rule's own medicine).
178
179
  def body_reads_published_constant?(root, scope)
180
+ scan_body(root) { |node| return true if published_constant_node?(node, scope) }
181
+ false
182
+ end
183
+
184
+ # Issue #3186 — the constant spellings the one hop's body scan would ask `Scope#published_constant?` about,
185
+ # sorted and de-duplicated: every node {#body_reads_published_constant?} visits within its budget, in its
186
+ # order. The hop's answer for any reader is a function of this list and of that reader's publication
187
+ # tables, so the ADR-89 WD2 gate stores it beside a def's return summary and compares it on an edit: a
188
+ # reader the gate skips would otherwise keep a withholding (or a firing) the edited body no longer earns.
189
+ def body_constant_spellings(root)
190
+ spellings = []
191
+ scan_body(root) do |node|
192
+ spelling = constant_spelling(node)
193
+ spellings << spelling unless spelling.nil?
194
+ end
195
+ spellings.uniq.sort
196
+ end
197
+
198
+ # Yields each node of `root` the hop's scan visits, stopping once {BODY_SCAN_BUDGET} is spent. An explicit
199
+ # worklist rather than recursion, so the node budget is one loop-carried local instead of a shared mutable
200
+ # cell (the engine value-pins a one-element Array counter and then self-flags the guard's own `negative?`
201
+ # as an always-falsey condition — this rule's own medicine).
202
+ def scan_body(root)
179
203
  stack = [root]
180
204
  budget = BODY_SCAN_BUDGET
181
205
  until stack.empty?
@@ -183,19 +207,24 @@ module Rigor
183
207
  next unless node.is_a?(Prism::Node)
184
208
 
185
209
  budget -= 1
186
- return false if budget.negative?
187
- return true if published_constant_node?(node, scope)
210
+ return if budget.negative?
188
211
 
212
+ yield node
189
213
  node.rigor_each_child { |child| stack << child }
190
214
  end
191
- false
192
215
  end
193
216
 
194
217
  def published_constant_node?(node, scope)
218
+ spelling = constant_spelling(node)
219
+ !spelling.nil? && scope.published_constant?(spelling)
220
+ end
221
+
222
+ # The name a constant reference asks the publication tables about ({#published_path?}'s rendering for a
223
+ # path), or nil for any other node and for a path that renders no name.
224
+ def constant_spelling(node)
195
225
  case node
196
- when Prism::ConstantReadNode then scope.published_constant?(node.name.to_s)
197
- when Prism::ConstantPathNode then published_path?(node, scope)
198
- else false
226
+ when Prism::ConstantReadNode then node.name.to_s
227
+ when Prism::ConstantPathNode then Source::ConstantPath.qualified_name_or_nil(node) || node.name&.to_s
199
228
  end
200
229
  end
201
230
 
@@ -36,10 +36,36 @@ module Rigor
36
36
  MISSING_HOOK = :method_missing
37
37
  # The roots whose project mixins `Inference::ObjectMixins` answers for.
38
38
  OBJECT_ROOTS = %w[Object Kernel BasicObject].to_set.freeze
39
- private_constant :MISSING_HOOK, :OBJECT_ROOTS
39
+ # Issue #2792 — the owners of a `method_missing` every object (and every class object) reaches. Core's
40
+ # `BasicObject#method_missing` raises `NoMethodError`, so it opens nothing; core declares none on the others,
41
+ # which are here so that a project or gem RBS reopening `Object`, `Kernel`, `Module` or `Class` with one cannot
42
+ # silence the rule for every receiver.
43
+ UNIVERSAL_HOOK_OWNERS = {
44
+ instance: OBJECT_ROOTS,
45
+ singleton: (OBJECT_ROOTS | %w[Module Class]).freeze
46
+ }.freeze
47
+ private_constant :MISSING_HOOK, :OBJECT_ROOTS, :UNIVERSAL_HOOK_OWNERS
40
48
 
41
49
  module_function
42
50
 
51
+ # Issue #2792 — whether RBS gives `class_name`'s `side` a `method_missing` from somewhere other than the
52
+ # universal roots: the class itself, an RBS ancestor or an included module (`Delegator` for a
53
+ # `SimpleDelegator`, `class W < SimpleDelegator`, `OpenStruct`). Ruby sends any name the receiver lacks there,
54
+ # private or public, so no name is provably missing. Only the absence verdict reads it: the call's type stays
55
+ # what an unresolved call answers.
56
+ def rbs_missing_hook?(scope, class_name, side)
57
+ definition =
58
+ if side == :singleton
59
+ Rigor::Reflection.singleton_method_definition(class_name, MISSING_HOOK, scope: scope)
60
+ else
61
+ Rigor::Reflection.instance_method_definition(class_name, MISSING_HOOK, scope: scope)
62
+ end
63
+ owner = definition&.defined_in
64
+ return false if owner.nil?
65
+
66
+ !UNIVERSAL_HOOK_OWNERS.fetch(side).include?(owner.to_s.delete_prefix("::"))
67
+ end
68
+
43
69
  # Whether the project may answer `method_name` on an instance of `class_name` through one of its RBS
44
70
  # ancestors.
45
71
  def ancestor_may_answer?(scope, class_name, method_name)
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../../inference/scope_indexer"
6
+
7
+ module Rigor
8
+ module Analysis
9
+ module CheckRules
10
+ # Issue #3155 — the singleton defs of one file that a later def of the same name certainly replaces, which
11
+ # `def.return-type-mismatch` does not check against the class's signature: Ruby keeps the later body.
12
+ #
13
+ # Both defs must be certain singleton defs of the same class or module body: a direct statement of that body
14
+ # (`def self.x`, or `def K.x` where `K` names the class), or a direct statement of a `class << self` there
15
+ # (`def x`, or `def K.x`). A def in a block (`Class.new do … end`, `Struct.new(…) do … end`), under a condition,
16
+ # loop or `begin`/`rescue`, or in a method body may not run, or runs on another object, so it replaces nothing,
17
+ # and a def written there is never read as replaced either. A reopening in another file is not read.
18
+ #
19
+ # The walk groups defs by the class or module body node only; whether a constant receiver names the class is
20
+ # read from the def's own Scope (`self_type`, the indexer's declaration context), so it derives none itself.
21
+ class ReplacedSingletonDefs
22
+ def initialize(root, scope_index)
23
+ @root = root
24
+ @scope_index = scope_index
25
+ end
26
+
27
+ def replaced?(def_node) = offsets.include?(def_node.location.start_offset)
28
+
29
+ # `def (self).x` and `def (K).x` name the receiver inside parentheses.
30
+ def self.unparenthesized(node)
31
+ while node.is_a?(Prism::ParenthesesNode)
32
+ body = node.body
33
+ node = body.is_a?(Prism::StatementsNode) && body.body.size == 1 ? body.body.first : body
34
+ end
35
+ node
36
+ end
37
+
38
+ private
39
+
40
+ def offsets
41
+ @offsets ||= collect
42
+ end
43
+
44
+ def collect
45
+ replaced = Set.new
46
+ each_class_body(@root) do |body|
47
+ last = {}
48
+ certain_singleton_defs(body).each do |def_node|
49
+ replaced << last[def_node.name].location.start_offset if last.key?(def_node.name)
50
+ last[def_node.name] = def_node
51
+ end
52
+ end
53
+ replaced
54
+ end
55
+
56
+ def each_class_body(node, &)
57
+ return if node.nil?
58
+
59
+ yield node.body if node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
60
+ node.rigor_each_child { |child| each_class_body(child, &) }
61
+ end
62
+
63
+ def certain_singleton_defs(body)
64
+ statements(body).flat_map do |stmt|
65
+ case stmt
66
+ when Prism::DefNode
67
+ lexical_self?(stmt.receiver, stmt, self_allowed: true) ? [stmt] : []
68
+ when Prism::SingletonClassNode
69
+ next [] unless lexical_self?(stmt.expression, stmt, self_allowed: true)
70
+
71
+ statements(stmt.body).select do |inner|
72
+ inner.is_a?(Prism::DefNode) &&
73
+ (inner.receiver.nil? || lexical_self?(inner.receiver, inner, self_allowed: false))
74
+ end
75
+ else
76
+ []
77
+ end
78
+ end
79
+ end
80
+
81
+ # `at` is the node whose Scope names the class: its `self_type` is the class or its singleton.
82
+ def lexical_self?(node, at, self_allowed:)
83
+ node = self.class.unparenthesized(node)
84
+ return self_allowed if node.is_a?(Prism::SelfNode)
85
+
86
+ self_type = @scope_index[at]&.self_type
87
+ return false unless self_type.respond_to?(:class_name)
88
+
89
+ Inference::ScopeIndexer.def_receiver_targets_lexical_self?(node, self_type.class_name.split("::"))
90
+ end
91
+
92
+ def statements(body) = body.is_a?(Prism::StatementsNode) ? body.body : []
93
+ end
94
+ end
95
+ end
96
+ end
@@ -81,6 +81,12 @@ module Rigor
81
81
  # Issue #1692 (the ADR-47 WD5 amendment) — a call to the `ruby2_keywords` family Ruby 4.1 deprecates. Runs only
82
82
  # under an explicit `target_ruby` of 4.1 or later; see {RubyDeprecations}.
83
83
  RULE_DEPRECATED_RUBY2_KEYWORDS = "call.deprecated-ruby2-keywords"
84
+ # ADR-128 WD6 / ADR-130 slice 2 (#2686) — a call in plugin source on a `Rigor::Plugin` interface value to a
85
+ # member the interface does not declare. Authored `:warning`, resolved `:off` by every profile and promoted
86
+ # only by the `plugin-api-surface` bleeding-edge feature. The one built-in id under `plugin.`, the prefix a
87
+ # plugin's own rows carry as `plugin.<id>.<rule>`: it is in {ALL_RULES}, so `disable:`, markers and
88
+ # `severity_overrides:` resolve it as a catalogue id before any plugin-id reading.
89
+ RULE_PLUGIN_API_SURFACE = "plugin.api-surface"
84
90
 
85
91
  ALL_RULES = [
86
92
  RULE_UNDEFINED_METHOD,
@@ -117,7 +123,8 @@ module Rigor
117
123
  RULE_EFFECT_LISKOV_WIDENED,
118
124
  RULE_EFFECT_UNKNOWN_LABEL,
119
125
  RULE_EFFECT_ANNOTATIONS_UNCHECKED,
120
- RULE_CONTRADICTING_SIGNATURE
126
+ RULE_CONTRADICTING_SIGNATURE,
127
+ RULE_PLUGIN_API_SURFACE
121
128
  ].freeze
122
129
 
123
130
  # Backward-compat alias table (ADR-8 § "Backward compatibility"). Existing user code with
@@ -100,7 +100,7 @@ module Rigor
100
100
  return unless value_node.is_a?(Prism::CallNode)
101
101
 
102
102
  scope = @scope_index[value_node]
103
- origin = scope&.void_origins&.[](value_node)
103
+ origin = scope&.void_origin(value_node)
104
104
  return if origin.nil?
105
105
 
106
106
  @results << Result.new(void_node: value_node, origin: origin)