rigortype 0.4.0 → 0.4.2

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 (237) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/08-understanding-errors.md +3 -2
  25. data/docs/handbook/11-sig-gen.md +6 -0
  26. data/docs/handbook/appendix-liskov.md +5 -0
  27. data/docs/install.md +87 -15
  28. data/docs/manual/02-cli-reference.md +2 -2
  29. data/docs/manual/03-configuration.md +6 -1
  30. data/docs/manual/04-diagnostics.md +2 -1
  31. data/docs/manual/06-baseline.md +2 -0
  32. data/docs/manual/07-plugins.md +1 -1
  33. data/docs/manual/08-skills.md +4 -3
  34. data/docs/manual/12-caching.md +43 -4
  35. data/docs/manual/14-rails-quickstart.md +19 -11
  36. data/docs/manual/18-removing-dead-code.md +32 -2
  37. data/docs/manual/plugins/README.md +6 -1
  38. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  39. data/docs/manual/plugins/rigor-alba.md +60 -0
  40. data/docs/manual/plugins/rigor-minitest.md +22 -4
  41. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  42. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  43. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  44. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  45. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
  47. data/lib/rigor/analysis/check_rules.rb +407 -144
  48. data/lib/rigor/analysis/dependency_recorder.rb +215 -18
  49. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  50. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  51. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  52. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  53. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  54. data/lib/rigor/analysis/incremental.rb +29 -9
  55. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
  56. data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
  57. data/lib/rigor/analysis/incremental_session.rb +287 -50
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  59. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  60. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +43 -9
  64. data/lib/rigor/analysis/run_stats.rb +2 -2
  65. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
  68. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  69. data/lib/rigor/analysis/runner.rb +371 -49
  70. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  71. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  72. data/lib/rigor/analysis/template_unit_paths.rb +6 -4
  73. data/lib/rigor/analysis/template_units.rb +15 -5
  74. data/lib/rigor/analysis/worker_session.rb +11 -7
  75. data/lib/rigor/cache/descriptor.rb +40 -1
  76. data/lib/rigor/cache/file_digest.rb +28 -0
  77. data/lib/rigor/cache/incremental_snapshot.rb +92 -8
  78. data/lib/rigor/cache/store.rb +63 -3
  79. data/lib/rigor/cache/value_digest.rb +325 -0
  80. data/lib/rigor/cli/annotate_command.rb +5 -0
  81. data/lib/rigor/cli/baseline_command.rb +7 -18
  82. data/lib/rigor/cli/check_command.rb +85 -14
  83. data/lib/rigor/cli/doctor_command.rb +1 -1
  84. data/lib/rigor/cli/effects_report.rb +2 -2
  85. data/lib/rigor/cli/trace_command.rb +5 -0
  86. data/lib/rigor/cli/type_of_command.rb +5 -3
  87. data/lib/rigor/cli/unused_command.rb +16 -10
  88. data/lib/rigor/cli.rb +11 -3
  89. data/lib/rigor/config_audit.rb +3 -2
  90. data/lib/rigor/config_path.rb +29 -0
  91. data/lib/rigor/configuration/severity_profile.rb +11 -0
  92. data/lib/rigor/configuration.rb +52 -7
  93. data/lib/rigor/effects/plugin_facts.rb +6 -6
  94. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  95. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  96. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  97. data/lib/rigor/environment/lockfile_resolver.rb +10 -1
  98. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  99. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  100. data/lib/rigor/environment/rbs_loader.rb +284 -15
  101. data/lib/rigor/environment/required_features.rb +206 -0
  102. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  103. data/lib/rigor/environment.rb +34 -73
  104. data/lib/rigor/inference/acceptance.rb +28 -3
  105. data/lib/rigor/inference/block_parameter_binder.rb +29 -13
  106. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  107. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  108. data/lib/rigor/inference/captured_locals.rb +7 -1
  109. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  110. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  111. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  112. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  113. data/lib/rigor/inference/declaration_walk.rb +7 -0
  114. data/lib/rigor/inference/def_node_resolver.rb +26 -3
  115. data/lib/rigor/inference/definer_resolution.rb +266 -0
  116. data/lib/rigor/inference/expression_typer.rb +351 -61
  117. data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
  118. data/lib/rigor/inference/global_write_census.rb +7 -4
  119. data/lib/rigor/inference/guard_rebinding.rb +54 -13
  120. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  121. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  122. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  123. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  124. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  125. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  126. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  127. data/lib/rigor/inference/macro_block_self_type.rb +73 -14
  128. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  129. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  130. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  131. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  132. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  133. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  134. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  135. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  136. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
  137. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  138. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  139. data/lib/rigor/inference/module_function_state.rb +214 -0
  140. data/lib/rigor/inference/multi_target_binder.rb +21 -16
  141. data/lib/rigor/inference/narrowing.rb +166 -22
  142. data/lib/rigor/inference/object_mixins.rb +180 -0
  143. data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
  144. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  145. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  146. data/lib/rigor/inference/refine_census.rb +239 -0
  147. data/lib/rigor/inference/refine_self.rb +317 -0
  148. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  149. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  150. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  151. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  152. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  153. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  154. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  155. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
  156. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  157. data/lib/rigor/inference/scope_indexer.rb +2129 -354
  158. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  159. data/lib/rigor/inference/statement_evaluator.rb +149 -32
  160. data/lib/rigor/inference/string_mutation.rb +9 -3
  161. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  162. data/lib/rigor/inference/version_guard.rb +19 -10
  163. data/lib/rigor/language_server/project_context.rb +31 -0
  164. data/lib/rigor/language_server/server.rb +18 -0
  165. data/lib/rigor/plugin/base.rb +28 -8
  166. data/lib/rigor/plugin/inflector.rb +1 -1
  167. data/lib/rigor/plugin/io_boundary.rb +124 -7
  168. data/lib/rigor/plugin/isolation.rb +1 -1
  169. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  170. data/lib/rigor/plugin/trust_policy.rb +3 -3
  171. data/lib/rigor/project_environment.rb +29 -6
  172. data/lib/rigor/protection/discovery_seed.rb +18 -9
  173. data/lib/rigor/protection/mutation_cache.rb +4 -4
  174. data/lib/rigor/protection/mutator.rb +1 -1
  175. data/lib/rigor/reflection/constant_ancestors.rb +211 -46
  176. data/lib/rigor/reflection/constant_path.rb +33 -7
  177. data/lib/rigor/reflection.rb +41 -10
  178. data/lib/rigor/scope/discovery_index.rb +258 -3
  179. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  180. data/lib/rigor/scope/resolution_chain.rb +1032 -0
  181. data/lib/rigor/scope.rb +480 -328
  182. data/lib/rigor/sig_gen/alias_index.rb +1 -1
  183. data/lib/rigor/sig_gen/classification.rb +4 -1
  184. data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
  185. data/lib/rigor/sig_gen/generator.rb +77 -21
  186. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  187. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  188. data/lib/rigor/source/alias_names.rb +52 -0
  189. data/lib/rigor/source/literals.rb +3 -2
  190. data/lib/rigor/source/symbol_name.rb +20 -0
  191. data/lib/rigor/type/combinator.rb +59 -3
  192. data/lib/rigor/type/float_range.rb +4 -1
  193. data/lib/rigor/type/nominal.rb +6 -1
  194. data/lib/rigor/version.rb +1 -1
  195. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  196. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  197. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  198. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  199. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  200. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  201. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  202. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  203. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  204. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  205. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  206. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  207. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  208. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  209. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  210. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  211. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  212. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  213. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  214. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  215. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  216. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  217. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
  218. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  219. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  220. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  221. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  222. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  223. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  224. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  225. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  226. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  227. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  228. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  229. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  230. data/sig/rigor/environment.rbs +2 -0
  231. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  232. data/sig/rigor/scope.rbs +69 -4
  233. data/sig/rigor/source.rbs +4 -0
  234. data/sig/rigor.rbs +3 -1
  235. data/skills/rigor-project-init/SKILL.md +37 -11
  236. data/skills/rigor-project-init/references/02-configure.md +7 -2
  237. metadata +90 -1
@@ -0,0 +1,71 @@
1
+ # rigor-typelizer
2
+
3
+ Teaches `rigor unused` about the serializer classes the [typelizer](https://github.com/skryukov/typelizer) gem
4
+ generates TypeScript types from. typelizer writes an interface for every named class that does
5
+ `include Typelizer::DSL` or `extend Typelizer::DSL`, and for all of its subclasses. Such a class may never be
6
+ referenced from Ruby and still be used by your frontend through the generated type, so without this plugin
7
+ `rigor unused` lists it as a removal candidate. The plugin adds no diagnostic and no return type.
8
+
9
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
10
+
11
+ ```yaml
12
+ plugins:
13
+ - rigor-typelizer
14
+ ```
15
+
16
+ ## What it roots
17
+
18
+ ```ruby
19
+ # app/serializers/application_serializer.rb
20
+ class ApplicationSerializer
21
+ include Typelizer::DSL # rooted
22
+ end
23
+
24
+ # app/serializers/user_serializer.rb
25
+ class UserSerializer < ApplicationSerializer # rooted: a subclass of a DSL class
26
+ end
27
+
28
+ # app/serializers/event_serializer.rb
29
+ class EventSerializer
30
+ extend Typelizer::DSL # rooted
31
+ end
32
+
33
+ class PlainFormatter # not rooted: no Typelizer::DSL on its chain
34
+ end
35
+ ```
36
+
37
+ A subclass is followed through any chain of project classes, and a compact header such as
38
+ `class Admin::UserSerializer < Base` resolves `Base` the way Ruby does (against the lexical scope, not
39
+ `Admin`).
40
+
41
+ ## What it does not root
42
+
43
+ - A class outside the configured `dirs`. typelizer generates any DSL class that is loaded, but it loads only `dirs`;
44
+ a class elsewhere is loaded when something references it, so it is not dead.
45
+ - A `module` that includes `Typelizer::DSL`. typelizer registers the module's own name and then calls
46
+ `.descendants` on it, which a plain module does not have, so it generates no interface.
47
+ - A DSL call that does not run with the class as `self`: inside a method, a block, a lambda or `class << self`.
48
+ - A class whose DSL comes only from an `ActiveSupport::Concern` `included do include Typelizer::DSL end` block.
49
+ typelizer does register such a class; the plugin does not follow module hooks, so this is a missed root and the
50
+ class stays a candidate.
51
+
52
+ `reject_class`, the lambda typelizer's configuration uses to drop a class from the output, is evaluated at
53
+ runtime and is not modelled. A rejected class is still rooted, which hides a candidate and never invents a
54
+ finding.
55
+
56
+ ## Configuration
57
+
58
+ ```yaml
59
+ plugins:
60
+ - gem: rigor-typelizer
61
+ config:
62
+ dirs: ["app/resources", "app/serializers"] # default
63
+ ```
64
+
65
+ `dirs` mirrors `Typelizer.dirs`; the default is what `Typelizer::Railtie` sets when you configure none. Set it
66
+ to the same list you give typelizer. Superclasses are resolved over all of your project's `paths:`, so a class
67
+ that shadows a base outside `dirs` is noticed.
68
+
69
+ ## More
70
+
71
+ Internals and the known limits: [plugins/rigor-typelizer/README.md](../../../plugins/rigor-typelizer/README.md).
@@ -40,8 +40,10 @@ module Rigor
40
40
  NODE_CLASSES = [Prism::DefNode].freeze
41
41
  RULE_WALK_GATES = %i[inside_def detached_ivar_facet].freeze
42
42
 
43
- # Returns `Hash[class_name (String) => Hash[ivar_name (Symbol) => Array<{node:, type:}>]]`. Empty
44
- # when the tree has no qualifying writes.
43
+ # Returns `Hash[class_name (String) => Hash[ivar_name (Symbol) => Array<{node:, type:, declared:}>]]`.
44
+ # Empty when the tree has no qualifying writes. `declared` is the translated type of the class's RBS
45
+ # declaration of the ivar — an `@x: T` member or the slot an `attr_*` member implies, own or
46
+ # inherited — and nil when the RBS declares none.
45
47
  def initialize(scope_index)
46
48
  @scope_index = scope_index
47
49
  @accumulator = {}
@@ -112,9 +114,31 @@ module Rigor
112
114
  return if scope.nil?
113
115
 
114
116
  rvalue_type = scope.type_of(node.value)
117
+ declared = declared_ivar(scope, class_name, node.name)
115
118
  @accumulator[class_name] ||= {}
116
119
  @accumulator[class_name][node.name] ||= []
117
- @accumulator[class_name][node.name] << { node: node, type: rvalue_type }
120
+ @accumulator[class_name][node.name] << { node: node, type: rvalue_type, declared: declared }
121
+ end
122
+
123
+ def declared_ivar(scope, class_name, ivar_name)
124
+ loader = scope.environment&.rbs_loader
125
+ return nil if loader.nil?
126
+
127
+ variable = loader.instance_definition(class_name)&.instance_variables&.[](ivar_name)
128
+ return nil if variable.nil?
129
+
130
+ translate_declared(variable.type, loader, class_name)
131
+ end
132
+
133
+ # `self` / `instance` in a class's declaration is that class; a module's stays untyped, since its
134
+ # instances belong to includers no RBS type names. A translation failure reads as untyped, which only
135
+ # ever withholds a finding.
136
+ def translate_declared(rbs_type, loader, class_name)
137
+ owner = Type::Combinator.nominal_of(class_name) unless loader.rbs_module?(class_name)
138
+ Inference::RbsTypeTranslator.translate(rbs_type, self_type: owner, instance_type: owner,
139
+ alias_expander: loader)
140
+ rescue StandardError
141
+ Type::Combinator.untyped
118
142
  end
119
143
  end
120
144
  end
@@ -3,7 +3,6 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "../../inference/scope_indexer"
6
- require_relative "../../source/constant_path"
7
6
  require_relative "../../source/node_children"
8
7
 
9
8
  module Rigor
@@ -12,13 +11,9 @@ module Rigor
12
11
  # Issue #1120 — the two ways one file makes a method callable at some sites and not at others, which
13
12
  # `call.undefined-method` has to ask the file's syntax about because no project-wide table can answer:
14
13
  #
15
- # - **Refinements.** `using M` activates M's refinements from the end of the call to the end of the body
16
- # that holds it — the file's top level or a `class` / `module` body, including everything nested in it —
17
- # and nowhere else, not even in another file. A `refine X do … end` block is active inside itself. What
18
- # each module refines is the project-wide `Scope#discovered_refinements`; this answers whether any of
19
- # those modules is in effect at a call site. A `using` whose argument is not a constant
20
- # (`using Module.new { refine … }`) names no module, so every refinement counts as in effect throughout
21
- # its file. A `using` inside a `def` raises in Ruby and activates nothing.
14
+ # - **Refinements.** Issue #1673 — answered by the file's in-effect refinements ({#refinements}, the
15
+ # {Inference::InEffectRefinements} the typer reads too); the rules ask it for derived answers
16
+ # (`refinement_active?`, `any_at?`, `refinement_def?`).
22
17
  # - **Singleton defs on locals.** `def o.m` defines `m` on the one object `o` holds. The local's type
23
18
  # is not changed; `o.m` is simply not reported within the scope the `def` is written in (the
24
19
  # enclosing `def`, `class` / `module` body, or file — a block shares its enclosing scope's locals).
@@ -26,42 +21,18 @@ module Rigor
26
21
  # Built lazily: the walk runs the first time a would-fire call asks, so a file with no such call pays
27
22
  # one small object and nothing else.
28
23
  class LexicalMethodSites
29
- def initialize(root)
24
+ # Issue #1703 — the file's root, which `call.possible-nil-receiver` re-walks for a `key?` guard.
25
+ attr_reader :root
26
+
27
+ # `refinements` is the query `Inference::ScopeIndexer` stamped on the file's discovery index, so the rules and
28
+ # the typer share one walk; a query over another tree, or none, builds this file's own on the first ask.
29
+ def initialize(root, refinements = nil)
30
30
  @root = root
31
31
  @built = false
32
+ @refinements = refinements if refinements&.over?(root)
32
33
  end
33
34
 
34
- # Is a refinement from one of `modules` (refining-module names) in effect at `call_node`?
35
- def refinement_active?(call_node, modules)
36
- build
37
- return true if @unresolved_using
38
-
39
- offset = call_node.location.start_offset
40
- return true if @refine_blocks.any? { |start, stop| offset >= start && offset < stop }
41
-
42
- @usings.any? do |start, stop, candidates|
43
- offset >= start && offset < stop && candidates.any? { |name| modules.include?(name) }
44
- end
45
- end
46
-
47
- # Issue #1367 — is any refinement in effect at `node`: a `using` of any module, a `using` of a non-constant, or
48
- # the inside of a `refine` block? The `global.*` stream check asks it where it cannot name the refining
49
- # module (a `define_method` or `alias_method` inside `refine` records none).
50
- def using_in_effect?(node)
51
- build
52
- return true if @unresolved_using
53
-
54
- offset = node.location.start_offset
55
- @refine_blocks.any? { |start, stop| offset >= start && offset < stop } ||
56
- @usings.any? { |start, stop, _candidates| offset >= start && offset < stop }
57
- end
58
-
59
- # Is `def_node` one of the defs a `refine X do … end` body defines on X? Such a def redefines X's method by
60
- # design, so X's declared signature for the name is not its contract (issue #1120, maintainer ruling a′).
61
- def refinement_def?(def_node)
62
- build
63
- @refinement_defs.include?(def_node.location.start_offset)
64
- end
35
+ def refinements = (@refinements ||= Inference::InEffectRefinements.new(@root))
65
36
 
66
37
  # Does a `def <local>.<name>` for this call's local receiver and method name sit in the scope the call
67
38
  # is in?
@@ -92,45 +63,35 @@ module Rigor
92
63
  return if @built
93
64
 
94
65
  @built = true
95
- @usings = []
96
- @refine_blocks = []
97
66
  @singleton_defs = []
98
67
  @scopes = []
99
- @refinement_defs = Set.new
100
- @unresolved_using = false
101
68
  return if @root.nil?
102
69
 
103
70
  location = @root.location
104
71
  span = [location.start_offset, location.end_offset]
105
- walk(@root, [], span, span, false)
72
+ walk(@root, span)
106
73
  end
107
74
 
108
- # `body` is the `[start, end]` of the body a `using` here stays in effect to the end of; `locals` is the
109
- # span of the scope a local written here belongs to.
110
- def walk(node, prefix, body, locals, in_def)
75
+ # `locals` is the span of the scope a local written here belongs to.
76
+ def walk(node, locals)
111
77
  case node
112
78
  when Prism::ClassNode, Prism::ModuleNode
113
- span = scope_span(node)
114
- inner = Source::ConstantPath.declaration_prefix(prefix, node.constant_path) || prefix
115
- return walk_children(node.body, inner, span, span, false)
79
+ return walk_children(node.body, scope_span(node))
116
80
  when Prism::SingletonClassNode
117
- walk(node.expression, prefix, body, locals, in_def)
118
- span = scope_span(node)
119
- return walk_children(node.body, prefix, span, span, false)
81
+ walk(node.expression, locals)
82
+ return walk_children(node.body, scope_span(node))
120
83
  when Prism::DefNode
121
84
  record_singleton_def(node, locals)
122
- return walk_children(node.body, prefix, body, scope_span(node), true)
123
- when Prism::CallNode
124
- record_call(node, prefix, body, in_def)
85
+ return walk_children(node.body, scope_span(node))
125
86
  end
126
87
 
127
- walk_children(node, prefix, body, locals, in_def)
88
+ walk_children(node, locals)
128
89
  end
129
90
 
130
- def walk_children(node, prefix, body, locals, in_def)
91
+ def walk_children(node, locals)
131
92
  return if node.nil?
132
93
 
133
- node.rigor_each_child { |child| walk(child, prefix, body, locals, in_def) }
94
+ node.rigor_each_child { |child| walk(child, locals) }
134
95
  end
135
96
 
136
97
  def record_singleton_def(node, locals)
@@ -140,38 +101,6 @@ module Rigor
140
101
  @singleton_defs << [receiver.name, node.name, *locals]
141
102
  end
142
103
 
143
- def record_call(node, prefix, body, in_def)
144
- if Inference::ScopeIndexer.refine_target(node)
145
- @refine_blocks << span_of(node.block)
146
- record_refinement_defs(node.block.body)
147
- elsif using_call?(node) && !in_def
148
- record_using(node, prefix, body)
149
- end
150
- end
151
-
152
- def record_refinement_defs(body)
153
- return if body.nil?
154
-
155
- Inference::ScopeIndexer.each_refinement_def(body) do |def_node|
156
- @refinement_defs << def_node.location.start_offset
157
- end
158
- end
159
-
160
- def using_call?(node)
161
- node.name == :using && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
162
- node.arguments&.arguments&.size == 1
163
- end
164
-
165
- def record_using(node, prefix, body)
166
- argument = node.arguments.arguments.first
167
- if argument.is_a?(Prism::ConstantReadNode) || argument.is_a?(Prism::ConstantPathNode)
168
- candidates = Inference::ScopeIndexer.constant_receiver_candidates(argument, prefix)
169
- @usings << [node.location.end_offset, body.last, candidates]
170
- else
171
- @unresolved_using = true
172
- end
173
- end
174
-
175
104
  # The span of a node that opens a local scope, recorded for {#nested_scope_holds?}.
176
105
  def scope_span(node)
177
106
  span = span_of(node)
@@ -0,0 +1,275 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "rule_ids"
6
+ require_relative "../diagnostic"
7
+ require_relative "../../inference/version_guard"
8
+ require_relative "../../source/constant_path"
9
+
10
+ module Rigor
11
+ module Analysis
12
+ module CheckRules
13
+ # Issue #1692 (the ADR-47 WD5 amendment) — `call.deprecated-ruby2-keywords`: a call to a method of the
14
+ # `ruby2_keywords` family, which Ruby 4.1 deprecates (Feature #22205). `Module#ruby2_keywords`, top-level
15
+ # `ruby2_keywords` and `Proc#ruby2_keywords` are removed in Ruby 4.4, `Hash.ruby2_keywords_hash?` and
16
+ # `Hash.ruby2_keywords_hash` in Ruby 4.5.
17
+ #
18
+ # The rule runs only when `.rigor.yml` states the runtime: `target_ruby` set explicitly to 4.1 or later
19
+ # ({Configuration#stated_runtime_ruby}). On Ruby 4.0 the same calls are correct and silent, so the default
20
+ # `target_ruby` fires nothing. The file walk is paid only by a project that states such a target.
21
+ #
22
+ # A call counts only where it can reach the core method and nothing else:
23
+ #
24
+ # - `ruby2_keywords` with no receiver (or `self`), in a class or module body, a `class << self` body or a
25
+ # singleton method, where `self` is a Module. At the top level outside any block, it is the `main`
26
+ # method (a top-level `def` body included). Inside a top-level block, inside any `instance_eval` /
27
+ # `instance_exec` block, and in an instance method of a class or module, it is not judged.
28
+ # - `ruby2_keywords` on a receiver typed `Proc`.
29
+ # - `ruby2_keywords_hash?` / `ruby2_keywords_hash` on `Hash` or a class RBS knows as its subclass, or with
30
+ # no receiver where `self` is one.
31
+ # - The same names through `send` / `__send__` with a literal Symbol, which also reach the private
32
+ # `Module#ruby2_keywords` on an explicit Module receiver.
33
+ #
34
+ # A project or `pre_eval:` file that defines a method by the same name anywhere silences that name, since
35
+ # the call may then reach the project's method. A call in an arm of a version guard that cannot run on the
36
+ # stated Ruby, or whose guard cannot be decided, is not reported, nor are the statements after a guard
37
+ # that jumps away (`return if RUBY_VERSION < "2.7"`).
38
+ module RubyDeprecations
39
+ module_function
40
+
41
+ # The first Ruby that deprecates the family.
42
+ DEPRECATED_SINCE = "4.1"
43
+
44
+ MODULE_FORM = ["Module#ruby2_keywords", "4.4"].freeze
45
+ TOPLEVEL_FORM = ["top-level ruby2_keywords", "4.4"].freeze
46
+ PROC_FORM = ["Proc#ruby2_keywords", "4.4"].freeze
47
+ HASH_FORMS = {
48
+ ruby2_keywords_hash?: ["Hash.ruby2_keywords_hash?", "4.5"].freeze,
49
+ ruby2_keywords_hash: ["Hash.ruby2_keywords_hash", "4.5"].freeze
50
+ }.freeze
51
+ NAMES = (%i[ruby2_keywords] + HASH_FORMS.keys).freeze
52
+ SEND_NAMES = %i[send __send__].freeze
53
+ INSTANCE_EVAL_NAMES = %i[instance_eval instance_exec].freeze
54
+ private_constant :MODULE_FORM, :TOPLEVEL_FORM, :PROC_FORM, :HASH_FORMS, :NAMES, :SEND_NAMES,
55
+ :INSTANCE_EVAL_NAMES
56
+
57
+ # The walk's lexical context: whether a block hides `self`, and whether a version guard makes the node
58
+ # unreportable.
59
+ Context = Data.define(:in_block, :in_instance_eval, :guarded)
60
+ ROOT_CONTEXT = Context.new(in_block: false, in_instance_eval: false, guarded: false)
61
+ private_constant :Context, :ROOT_CONTEXT
62
+
63
+ # @param stated_ruby — {Configuration#stated_runtime_ruby}, or nil
64
+ # @return whether the rule runs at all for this target
65
+ def active?(stated_ruby)
66
+ return false if stated_ruby.nil? || !::Gem::Version.correct?(stated_ruby)
67
+
68
+ ::Gem::Version.new(stated_ruby) >= ::Gem::Version.new(DEPRECATED_SINCE)
69
+ end
70
+
71
+ def diagnostics(path, root, scope_index, stated_ruby)
72
+ return [] unless active?(stated_ruby)
73
+
74
+ guard_ruby = guard_version(stated_ruby)
75
+ results = []
76
+ shadowed = {}
77
+ walk(root, ROOT_CONTEXT, guard_ruby) do |call, context|
78
+ form = deprecated_form(call, context, scope_index, shadowed)
79
+ results << build(path, call, form, stated_ruby) if form
80
+ end
81
+ results
82
+ end
83
+
84
+ # `RUBY_VERSION` reads `"4.1.0"` on Ruby 4.1, so a stated `"4.1"` is padded before a guard compares it
85
+ # as a String.
86
+ def guard_version(stated_ruby)
87
+ segments = stated_ruby.split(".")
88
+ (segments + (["0"] * (3 - segments.size))).join(".")
89
+ end
90
+
91
+ def walk(node, context, guard_ruby, &)
92
+ # Nothing under a guarded node is reported, so the walk does not descend into it.
93
+ return if !node.is_a?(Prism::Node) || context.guarded
94
+
95
+ yield node, context if node.is_a?(Prism::CallNode) && candidate?(node)
96
+
97
+ jumped = false
98
+ node.rigor_each_child do |child|
99
+ child_context = child_context(node, child, context, guard_ruby)
100
+ child_context = child_context.with(guarded: true) if jumped
101
+ walk(child, child_context, guard_ruby, &)
102
+ jumped ||= node.is_a?(Prism::StatementsNode) && jumping_version_guard?(child)
103
+ end
104
+ end
105
+
106
+ def candidate?(call)
107
+ return true if NAMES.include?(call.name)
108
+
109
+ SEND_NAMES.include?(call.name) && NAMES.include?(send_target(call))
110
+ end
111
+
112
+ # The Symbol a `send` names as its first argument, or nil.
113
+ def send_target(call)
114
+ first = call.arguments&.arguments&.first
115
+ first.is_a?(Prism::SymbolNode) ? first.unescaped.to_sym : nil
116
+ end
117
+
118
+ def child_context(node, child, context, guard_ruby)
119
+ if node.is_a?(Prism::CallNode) && child.equal?(node.block) && child.is_a?(Prism::BlockNode)
120
+ instance_eval = context.in_instance_eval || INSTANCE_EVAL_NAMES.include?(node.name)
121
+ context = context.with(in_block: true, in_instance_eval: instance_eval)
122
+ end
123
+ return context if context.guarded
124
+
125
+ version_guard_live?(node, child, guard_ruby) ? context : context.with(guarded: true)
126
+ end
127
+
128
+ # Whether `child` stays reportable under `node`'s condition. An `if` / `unless` arm is, when its guard is
129
+ # decided on the stated Ruby and selects it. Any other branch of a conditional whose condition reads
130
+ # `RUBY_VERSION` is not, because the stated version is the lowest one the project runs on and an
131
+ # undecided guard may deselect the call on it.
132
+ def version_guard_live?(node, child, guard_ruby)
133
+ case node
134
+ when Prism::IfNode, Prism::UnlessNode
135
+ return true if child.equal?(node.predicate) || !reads_ruby_version?(node.predicate)
136
+
137
+ arm_live?(node, child, guard_ruby)
138
+ when Prism::CaseNode, Prism::CaseMatchNode, Prism::WhileNode, Prism::UntilNode
139
+ condition = node.predicate
140
+ condition.nil? || child.equal?(condition) || !reads_ruby_version?(condition)
141
+ when Prism::AndNode, Prism::OrNode
142
+ child.equal?(node.left) || !reads_ruby_version?(node.left)
143
+ else
144
+ true
145
+ end
146
+ end
147
+
148
+ def arm_live?(node, child, guard_ruby)
149
+ verdict = Inference::VersionGuard.verdict(node.predicate, stated_ruby: guard_ruby)
150
+ return false if verdict.nil?
151
+
152
+ then_live = verdict == (node.is_a?(Prism::IfNode) ? :truthy : :falsey)
153
+ child.equal?(node.statements) ? then_live : !then_live
154
+ end
155
+
156
+ # `return if RUBY_VERSION < "2.7"` (or `raise` / `next` / `break` under such a guard): the statements
157
+ # after it run only on the Rubies the guard lets through.
158
+ def jumping_version_guard?(node)
159
+ return false unless node.is_a?(Prism::IfNode) || node.is_a?(Prism::UnlessNode)
160
+ return false unless reads_ruby_version?(node.predicate)
161
+
162
+ other_arm = node.is_a?(Prism::IfNode) ? node.subsequent : node.else_clause
163
+ contains_jump?(node.statements) || contains_jump?(other_arm)
164
+ end
165
+
166
+ JUMP_CALLS = %i[raise fail exit exit! abort].freeze
167
+ private_constant :JUMP_CALLS
168
+
169
+ def contains_jump?(node)
170
+ return false unless node.is_a?(Prism::Node)
171
+
172
+ case node
173
+ when Prism::ReturnNode, Prism::NextNode, Prism::BreakNode then return true
174
+ when Prism::CallNode then return true if node.receiver.nil? && JUMP_CALLS.include?(node.name)
175
+ end
176
+ found = false
177
+ node.rigor_each_child { |child| found ||= contains_jump?(child) }
178
+ found
179
+ end
180
+
181
+ def reads_ruby_version?(node)
182
+ return false unless node.is_a?(Prism::Node)
183
+ return true if (node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)) &&
184
+ Source::ConstantPath.qualified_name_or_nil(node) == "RUBY_VERSION"
185
+
186
+ found = false
187
+ node.rigor_each_child { |child| found ||= reads_ruby_version?(child) }
188
+ found
189
+ end
190
+
191
+ # @return `[display, removal]` for a call that reaches a deprecated method, or nil
192
+ def deprecated_form(call, context, scope_index, shadowed)
193
+ scope = scope_index[call]
194
+ return nil if scope.nil?
195
+
196
+ via_send = SEND_NAMES.include?(call.name)
197
+ name = via_send ? send_target(call) : call.name
198
+ shadowed[name] = project_defines?(scope, name) unless shadowed.key?(name)
199
+ return nil if shadowed[name]
200
+
201
+ receiver = call.receiver
202
+ if receiver.nil? || receiver.is_a?(Prism::SelfNode)
203
+ self_form(name, scope, context)
204
+ else
205
+ receiver_form(name, scope.type_of(receiver), scope, via_send)
206
+ end
207
+ end
208
+
209
+ def self_form(name, scope, context)
210
+ self_type = scope.self_type
211
+ if self_type.nil?
212
+ return nil if context.in_block || name != :ruby2_keywords
213
+
214
+ return TOPLEVEL_FORM
215
+ end
216
+ return nil if context.in_instance_eval || !self_type.is_a?(Type::Singleton)
217
+
218
+ name == :ruby2_keywords ? MODULE_FORM : hash_form(name, self_type, scope)
219
+ end
220
+
221
+ def receiver_form(name, type, scope, via_send)
222
+ if name == :ruby2_keywords
223
+ return PROC_FORM if type.is_a?(Type::Nominal) && class_at_or_below?(type.class_name, "Proc", scope)
224
+
225
+ # `Module#ruby2_keywords` is private: only `send` reaches it on an explicit receiver.
226
+ return via_send && type.is_a?(Type::Singleton) ? MODULE_FORM : nil
227
+ end
228
+ type.is_a?(Type::Singleton) ? hash_form(name, type, scope) : nil
229
+ end
230
+
231
+ def hash_form(name, singleton, scope)
232
+ return nil unless HASH_FORMS.key?(name)
233
+
234
+ class_at_or_below?(singleton.class_name, "Hash", scope) ? HASH_FORMS.fetch(name) : nil
235
+ end
236
+
237
+ def class_at_or_below?(class_name, ancestor, scope)
238
+ return true if class_name == ancestor
239
+ return false if scope.environment.nil?
240
+
241
+ %i[equal subclass].include?(Rigor::Reflection.class_ordering(class_name, ancestor, scope: scope))
242
+ end
243
+
244
+ # Whether a project or `pre_eval:` file defines a method called `name` anywhere — on any class, on either
245
+ # side, or at the top level. Asked once per name per file.
246
+ def project_defines?(scope, name)
247
+ return true if scope.discovered_methods.any? { |_owner, table| table.key?(name) }
248
+ return true if scope.discovered_def_nodes.any? { |_owner, table| table.key?(name) }
249
+
250
+ registry = scope.environment&.project_patched_methods
251
+ return false if registry.nil? || registry.empty?
252
+
253
+ registry.by_key.each_key.any? { |(_owner, method_name, _kind)| method_name == name }
254
+ end
255
+
256
+ def build(path, call, form, stated_ruby)
257
+ display, removal = form
258
+ tail = if ::Gem::Version.new(stated_ruby) >= ::Gem::Version.new(removal)
259
+ "was removed in Ruby #{removal}"
260
+ else
261
+ "is deprecated since Ruby 4.1 and will be removed in Ruby #{removal}"
262
+ end
263
+ Diagnostic.from_message_loc(
264
+ call,
265
+ rule: RULE_DEPRECATED_RUBY2_KEYWORDS,
266
+ path: path,
267
+ message: "`#{display}' #{tail} (target_ruby: #{stated_ruby.inspect})",
268
+ severity: :warning,
269
+ method_name: display
270
+ )
271
+ end
272
+ end
273
+ end
274
+ end
275
+ end
@@ -78,6 +78,9 @@ module Rigor
78
78
  # (`SpecialGlobalSetters`), not the global's RBS declaration.
79
79
  RULE_GLOBAL_WRITE_TYPE_MISMATCH = "global.write-type-mismatch"
80
80
  RULE_GLOBAL_READONLY_WRITE = "global.readonly-write"
81
+ # Issue #1692 (the ADR-47 WD5 amendment) — a call to the `ruby2_keywords` family Ruby 4.1 deprecates. Runs only
82
+ # under an explicit `target_ruby` of 4.1 or later; see {RubyDeprecations}.
83
+ RULE_DEPRECATED_RUBY2_KEYWORDS = "call.deprecated-ruby2-keywords"
81
84
 
82
85
  ALL_RULES = [
83
86
  RULE_UNDEFINED_METHOD,
@@ -87,6 +90,7 @@ module Rigor
87
90
  RULE_ARGUMENT_TYPE,
88
91
  RULE_NIL_RECEIVER,
89
92
  RULE_RAISE_NON_EXCEPTION,
93
+ RULE_DEPRECATED_RUBY2_KEYWORDS,
90
94
  RULE_DUMP_TYPE,
91
95
  RULE_ASSERT_TYPE,
92
96
  RULE_ALWAYS_RAISES,