rigortype 0.4.1 → 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 (182) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  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/11-sig-gen.md +6 -0
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/03-configuration.md +1 -1
  27. data/docs/manual/04-diagnostics.md +2 -1
  28. data/docs/manual/06-baseline.md +2 -0
  29. data/docs/manual/07-plugins.md +1 -1
  30. data/docs/manual/18-removing-dead-code.md +32 -2
  31. data/docs/manual/plugins/README.md +6 -1
  32. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  33. data/docs/manual/plugins/rigor-alba.md +60 -0
  34. data/docs/manual/plugins/rigor-minitest.md +22 -4
  35. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  36. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  37. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  38. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  39. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  40. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  41. data/lib/rigor/analysis/check_rules.rb +289 -57
  42. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  43. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  44. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  45. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  46. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  47. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  48. data/lib/rigor/analysis/incremental.rb +29 -9
  49. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  50. data/lib/rigor/analysis/incremental_run_slot.rb +3 -1
  51. data/lib/rigor/analysis/incremental_session.rb +14 -1
  52. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  53. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  54. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  55. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  56. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
  57. data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
  58. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  59. data/lib/rigor/analysis/runner.rb +34 -3
  60. data/lib/rigor/analysis/worker_session.rb +7 -4
  61. data/lib/rigor/cache/incremental_snapshot.rb +22 -0
  62. data/lib/rigor/cli/annotate_command.rb +5 -0
  63. data/lib/rigor/cli/baseline_command.rb +7 -18
  64. data/lib/rigor/cli/check_command.rb +2 -1
  65. data/lib/rigor/cli/trace_command.rb +5 -0
  66. data/lib/rigor/cli/type_of_command.rb +5 -3
  67. data/lib/rigor/cli/unused_command.rb +13 -7
  68. data/lib/rigor/cli.rb +10 -2
  69. data/lib/rigor/configuration/severity_profile.rb +6 -0
  70. data/lib/rigor/configuration.rb +46 -2
  71. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  72. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  73. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  74. data/lib/rigor/environment/rbs_loader.rb +283 -14
  75. data/lib/rigor/environment/required_features.rb +206 -0
  76. data/lib/rigor/environment.rb +21 -3
  77. data/lib/rigor/inference/acceptance.rb +28 -3
  78. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  79. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  80. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  81. data/lib/rigor/inference/expression_typer.rb +215 -16
  82. data/lib/rigor/inference/global_write_census.rb +7 -4
  83. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  84. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  85. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  86. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  87. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  88. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  89. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  90. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  91. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  92. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  93. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  94. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  95. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  96. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  97. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  98. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  99. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  100. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +259 -28
  101. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  102. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  103. data/lib/rigor/inference/module_function_state.rb +2 -1
  104. data/lib/rigor/inference/narrowing.rb +20 -6
  105. data/lib/rigor/inference/object_mixins.rb +180 -0
  106. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  107. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  108. data/lib/rigor/inference/refine_census.rb +239 -0
  109. data/lib/rigor/inference/refine_self.rb +317 -0
  110. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  111. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  112. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  113. data/lib/rigor/inference/scope_indexer.rb +687 -60
  114. data/lib/rigor/inference/statement_evaluator.rb +71 -20
  115. data/lib/rigor/inference/string_mutation.rb +9 -3
  116. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  117. data/lib/rigor/inference/version_guard.rb +19 -10
  118. data/lib/rigor/language_server/project_context.rb +31 -0
  119. data/lib/rigor/language_server/server.rb +18 -0
  120. data/lib/rigor/plugin/inflector.rb +1 -1
  121. data/lib/rigor/plugin/io_boundary.rb +42 -3
  122. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  123. data/lib/rigor/project_environment.rb +29 -6
  124. data/lib/rigor/protection/mutation_cache.rb +1 -1
  125. data/lib/rigor/protection/mutator.rb +1 -1
  126. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  127. data/lib/rigor/reflection/constant_path.rb +13 -2
  128. data/lib/rigor/reflection.rb +40 -8
  129. data/lib/rigor/scope/discovery_index.rb +40 -1
  130. data/lib/rigor/scope/resolution_chain.rb +159 -11
  131. data/lib/rigor/scope.rb +189 -10
  132. data/lib/rigor/sig_gen/classification.rb +4 -1
  133. data/lib/rigor/sig_gen/generator.rb +55 -5
  134. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  135. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  136. data/lib/rigor/source/alias_names.rb +52 -0
  137. data/lib/rigor/source/literals.rb +3 -2
  138. data/lib/rigor/source/symbol_name.rb +20 -0
  139. data/lib/rigor/type/combinator.rb +34 -2
  140. data/lib/rigor/type/float_range.rb +4 -1
  141. data/lib/rigor/type/nominal.rb +6 -1
  142. data/lib/rigor/version.rb +1 -1
  143. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  144. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  145. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  146. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  147. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  148. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  149. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  150. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  151. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  152. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  153. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  154. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  156. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  157. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  158. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  159. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  160. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  161. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  162. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  163. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  164. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  165. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  166. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  167. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  168. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  169. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  170. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  171. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  172. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  173. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  174. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  175. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  176. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  177. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  178. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  179. data/sig/rigor/scope.rbs +27 -4
  180. data/sig/rigor/source.rbs +4 -0
  181. data/sig/rigor.rbs +3 -1
  182. metadata +66 -1
@@ -14,7 +14,8 @@ The full catalogue, with a one-line scope for every plugin, is
14
14
  ## Available pages
15
15
 
16
16
  - [rigor-activerecord](rigor-activerecord.md) — ActiveRecord
17
- finder / relation typing and schema-checked columns.
17
+ finder / relation typing and schema-checked columns; roots models reached only through an
18
+ association for `rigor unused`.
18
19
  - [rigor-rails-routes](rigor-rails-routes.md) — `*_path` / `*_url`
19
20
  helper validation against a parsed `config/routes.rb`.
20
21
  - [rigor-rails-i18n](rigor-rails-i18n.md) — `t(...)` / `I18n.t(...)`
@@ -69,6 +70,10 @@ The full catalogue, with a one-line scope for every plugin, is
69
70
  (`params`, `namespace`, verb macros, `desc`, `route_setting`) and
70
71
  `Grape::Entity` `expose` declarations, including the `instance_eval`'d
71
72
  block `self` bindings.
73
+ - [rigor-alba](rigor-alba.md) — types the inline `Alba.serialize { }` block `self`, and
74
+ roots the resources alba infers from `many :articles` for `rigor unused`.
75
+ - [rigor-typelizer](rigor-typelizer.md) — roots the serializer classes typelizer generates TypeScript
76
+ interfaces from, for `rigor unused`.
72
77
  - [rigor-rspec-rails](rigor-rspec-rails.md) — `have_http_status`
73
78
  argument validation (out-of-range codes, unknown status symbols).
74
79
  - [rigor-shoulda-matchers](rigor-shoulda-matchers.md) — shoulda matcher
@@ -228,6 +228,27 @@ member left out stays lenient rather than reported.
228
228
  `ActiveRecord::Base` is deliberately **not** declared: closing it would
229
229
  close every model in the project.
230
230
 
231
+ ## Roots for `rigor unused`
232
+
233
+ ```ruby
234
+ class Recipe < ApplicationRecord
235
+ belongs_to :user # ActiveRecord loads User
236
+ has_many :comments, as: :commentable # ... and Comment
237
+ has_many :tags, through: :taggings # no root of its own, see below
238
+ belongs_to :owner, polymorphic: true # no single target: no root
239
+ end
240
+ ```
241
+
242
+ `User` and `Comment` are named nowhere in the source, so [`rigor unused`](../02-cli-reference.md#rigor-unused) would list a model reached only this way. The plugin publishes the class each association resolves to as a `:reachability_roots` fact, mirroring ActiveRecord's own lookup:
243
+
244
+ - a literal `class_name:` (String or Symbol, or a rooted `"::Foo"`) wins;
245
+ - otherwise `has_many` / `has_and_belongs_to_many` use the singularized, camelized name and `belongs_to` / `has_one` the camelized name;
246
+ - the name is tried as nested in the owner itself (`Post` looks for `Post::Comment`), then in the owner's enclosing namespaces innermost first (`Admin::Recipe` looks for `Admin::User`), then at top level. If a candidate is not a model but is a namespace of one, nothing is rooted, since ActiveRecord would pick that module.
247
+
248
+ A root is published only when the project declares a model of that name. Nothing is published for a `polymorphic: true` association, a non-literal `class_name:` or `**options`, or a `through:` association without a literal `class_name:` / `source_type:` (a literal `source_type:` names the class; otherwise its target is the source association's, and that association roots it). Associations written inside `class << self`, a `def` or a block other than `with_options` are not seen, and neither is the block-parameter form `with_options(...) do |o| o.belongs_to :x end`. Literal options of a `with_options` group are merged into the associations inside it, the innermost group winning as in Rails; a group with non-literal options (`with_options opts do`) is skipped, as is `anonymous_class:`, an association whose own options hide the class (a `**splat`, an options variable, a non-literal `class_name:`), and a computed name equal to the owner's own name (Rails 7.2+ tries the top-level class first). The merged options also decide the association reader's type: `with_options optional: true` makes `belongs_to` readers nilable, and `class_name:` and `polymorphic:` set the target or make it `Dynamic`. The name is inflected with the real `ActiveSupport::Inflector`; if it cannot be loaded the plugin publishes nothing.
249
+
250
+ The roots are flat: an association on a model `rigor unused` itself reports as dead still roots its target, so a chain of dead models can leave the last one unlisted. Project-custom inflections are not read, so a model whose name relies on one is under-rooted rather than wrongly rooted. The roots are published in reduced mode too, since they do not depend on the schema.
251
+
231
252
  ## Limitations
232
253
 
233
254
  - **Direct-superclass match only.** `class Admin < User` where
@@ -0,0 +1,60 @@
1
+ # rigor-alba
2
+
3
+ Teaches Rigor about the [alba](https://github.com/okuramasafumi/alba) JSON serializer. alba does not ship its
4
+ RBS in the gem, so without this plugin every alba call in your project reads `Dynamic[top]`. The plugin only
5
+ removes false diagnostics and publishes `rigor unused` roots; it adds no diagnostic and no return type.
6
+
7
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
8
+
9
+ ```yaml
10
+ plugins:
11
+ - rigor-alba
12
+ ```
13
+
14
+ ## What it does
15
+
16
+ ```ruby
17
+ # Inline resources: the block runs on an anonymous Alba::Resource class, so `attributes` and friends resolve
18
+ # instead of firing call.unresolved-toplevel.
19
+ json = Alba.serialize(user) { attributes :id, :name }
20
+ hash = Alba.hashify(user) { attributes :id }
21
+ ```
22
+
23
+ The plugin contributes no return types: `Alba.serialize(obj)` runs your own `<Class>Resource#serialize`, which
24
+ you may override to return anything.
25
+
26
+ The block may sit anywhere a value can: `render json: Alba.serialize(x) { ... }` resolves the same as an
27
+ assignment.
28
+
29
+ ## Roots for `rigor unused`
30
+
31
+ ```ruby
32
+ class UserResource
33
+ include Alba::Resource
34
+
35
+ many :articles # alba loads ArticleResource, then ArticleSerializer
36
+ one :editor, resource: EditorResource # explicit: nothing inferred
37
+ end
38
+ ```
39
+
40
+ `ArticleResource` appears nowhere in the source, so `rigor unused` would list it. The plugin publishes it as a
41
+ root. It does so only for an association that gives no `resource:` / `serializer:`, no second argument and no
42
+ block, and only when a class of the inferred name exists in the project (nesting-aware: inside `module Admin`
43
+ alba tries `Admin::ArticleResource` first). The name is classified with the real `ActiveSupport::Inflector`;
44
+ if it cannot be loaded the plugin publishes nothing.
45
+
46
+ ## Configuration
47
+
48
+ ```yaml
49
+ plugins:
50
+ - gem: rigor-alba
51
+ config:
52
+ resource_search_paths: ["app"] # default
53
+ ```
54
+
55
+ `resource_search_paths` is where the plugin looks for resource classes. Widen it if your resources live
56
+ outside `app/`.
57
+
58
+ ## More
59
+
60
+ Internals and the known limits: [plugins/rigor-alba/README.md](../../../plugins/rigor-alba/README.md).
@@ -44,18 +44,36 @@ their `refute_*` equivalents.
44
44
  | --- | --- |
45
45
  | `assert_kind_of(T, x)` / `assert_instance_of(T, x)` | narrow to `T` |
46
46
  | `assert_nil(x)` | narrow to `Constant<nil>` |
47
- | `assert_equal(literal, x)` | narrow to `Constant<literal>` |
47
+ | `assert_equal(literal, x)` | meet with `Constant<literal>` (see below) |
48
48
  | `assert_match(regex, x)` | narrow to `String` |
49
49
  | `refute_kind_of` / `refute_instance_of` (+ `assert_not_*`) | narrow away from `T` |
50
50
  | `refute_nil(x)` / `assert_not_nil(x)` | narrow away from nil |
51
- | `refute_equal(literal, x)` / `assert_not_equal(...)` | narrow away from `Constant<literal>` |
51
+ | `refute_equal(literal, x)` / `assert_not_equal(...)` | unchanged |
52
52
  | `_(x).must_be_kind_of(T)` / `must_be_a(T)` / `must_be_instance_of(T)` / `must_be_an_instance_of(T)` | narrow to `T` |
53
53
  | `_(x).must_be_nil` | narrow to `Constant<nil>` |
54
- | `_(x).must_equal(literal)` | narrow to `Constant<literal>` |
54
+ | `_(x).must_equal(literal)` | meet with `Constant<literal>` (see below) |
55
55
  | `_(x).must_match(regex)` | narrow to `String` |
56
56
  | `_(x).wont_be_kind_of(T)` / `wont_be_instance_of(T)` | narrow away from `T` |
57
57
  | `_(x).wont_be_nil` | narrow away from nil |
58
- | `_(x).wont_equal(literal)` | narrow away from `Constant<literal>` |
58
+ | `_(x).wont_equal(literal)` | unchanged |
59
+
60
+ ### Equality is not identity
61
+
62
+ `assert_equal(literal, x)` and `_(x).must_equal(literal)` pass when
63
+ `literal == x`, and `==` can hold across classes: `1 == 1.0`,
64
+ `1 == 1r`, and a class with its own `==` (a `ModInt` that compares
65
+ `to_i`) passes `assert_equal 9, m`. The assertion therefore never
66
+ replaces `x`'s type:
67
+
68
+ - a `nil`, `true`, `false`, or Symbol literal has identity `==`, so `x`
69
+ narrows to that literal whatever its type was;
70
+ - an Integer, Float, or String literal narrows `x` only when every
71
+ member of `x`'s type is already of the literal's class (or a `nil`,
72
+ boolean, or Symbol the literal cannot equal). After
73
+ `x = rand(10); assert_equal 9, x`, `x` is `9`;
74
+ - otherwise — `x` is `Dynamic`, of a user class, or of another numeric
75
+ class (`assert_equal 9.0, x` with an Integer `x`), or the literal is
76
+ `0.0`, which `-0.0` also equals — `x` keeps its type.
59
77
 
60
78
  ## No diagnostics, no config
61
79
 
@@ -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)