rigortype 0.4.1 → 0.4.3
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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/data/builtins/ruby_core/io.yml +1 -0
- data/data/builtins/ruby_core/proc.yml +4 -0
- data/data/builtins/ruby_core/random.yml +4 -0
- data/data/builtins/ruby_core/struct.yml +3 -0
- data/data/core_overlay/env.rbs +34 -0
- data/data/core_overlay/gc.rbs +50 -0
- data/data/core_overlay/hash.rbs +45 -0
- data/data/core_overlay/integer.rbs +31 -0
- data/data/core_overlay/io_buffer.rbs +27 -0
- data/data/core_overlay/kernel.rbs +40 -0
- data/data/core_overlay/match_data.rbs +28 -0
- data/data/core_overlay/module.rbs +51 -0
- data/data/core_overlay/object_space.rbs +29 -0
- data/data/core_overlay/proc.rbs +45 -0
- data/data/core_overlay/range.rbs +40 -0
- data/data/core_overlay/source_range.rbs +78 -0
- data/data/core_overlay/string.rbs +86 -0
- data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
- data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
- data/docs/handbook/01-getting-started.md +3 -3
- data/docs/handbook/06-classes.md +3 -1
- data/docs/handbook/11-sig-gen.md +18 -5
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/manual/02-cli-reference.md +19 -1
- data/docs/manual/03-configuration.md +7 -5
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +41 -4
- data/docs/manual/plugins/README.md +6 -1
- data/docs/manual/plugins/rigor-activerecord.md +21 -0
- data/docs/manual/plugins/rigor-alba.md +60 -0
- data/docs/manual/plugins/rigor-minitest.md +22 -4
- data/docs/manual/plugins/rigor-typelizer.md +71 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +11 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +292 -59
- data/lib/rigor/analysis/dependency_recorder.rb +15 -0
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
- data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/erb_template_detector.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +29 -9
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
- data/lib/rigor/analysis/incremental_run_slot.rb +6 -4
- data/lib/rigor/analysis/incremental_session.rb +103 -23
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +241 -37
- data/lib/rigor/analysis/reachability/scan.rb +490 -77
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +16 -1
- data/lib/rigor/analysis/run_cache_probe.rb +1 -1
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +113 -6
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -19
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +62 -13
- data/lib/rigor/analysis/worker_session.rb +8 -5
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +55 -5
- data/lib/rigor/cli/annotate_command.rb +5 -0
- data/lib/rigor/cli/baseline_command.rb +7 -18
- data/lib/rigor/cli/check_command.rb +140 -40
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +6 -3
- data/lib/rigor/cli/unused_command.rb +166 -27
- data/lib/rigor/cli.rb +39 -5
- data/lib/rigor/configuration/severity_profile.rb +6 -0
- data/lib/rigor/configuration.rb +61 -2
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +316 -14
- data/lib/rigor/environment/required_features.rb +208 -0
- data/lib/rigor/environment.rb +33 -8
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/builtins/cruby_definers.rb +78 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/captured_locals.rb +43 -3
- data/lib/rigor/inference/def_node_resolver.rb +23 -1
- data/lib/rigor/inference/expression_typer.rb +261 -29
- data/lib/rigor/inference/global_write_census.rb +7 -4
- data/lib/rigor/inference/guard_rebinding.rb +1 -1
- data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
- data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +286 -0
- data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
- data/lib/rigor/inference/in_effect_refinements.rb +256 -0
- data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
- data/lib/rigor/inference/key_presence_guard.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +59 -12
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
- data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
- data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +247 -28
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +96 -14
- data/lib/rigor/inference/module_function_state.rb +2 -1
- data/lib/rigor/inference/narrowing.rb +20 -6
- data/lib/rigor/inference/object_mixins.rb +180 -0
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +12 -7
- data/lib/rigor/inference/project_patched_scanner.rb +108 -26
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +475 -0
- data/lib/rigor/inference/refined_dispatch.rb +320 -0
- data/lib/rigor/inference/repeated_or_writes.rb +2 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
- data/lib/rigor/inference/scope_indexer.rb +827 -65
- data/lib/rigor/inference/statement_evaluator.rb +294 -51
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +87 -5
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +118 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +433 -0
- data/lib/rigor/lens/members.rb +153 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +280 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +40 -0
- data/lib/rigor/plugin/inflector.rb +1 -1
- data/lib/rigor/plugin/io_boundary.rb +42 -3
- data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
- data/lib/rigor/project_environment.rb +48 -6
- data/lib/rigor/protection/mutation_cache.rb +3 -2
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/reflection/constant_ancestors.rb +127 -11
- data/lib/rigor/reflection/constant_path.rb +13 -2
- data/lib/rigor/reflection.rb +40 -8
- data/lib/rigor/scope/discovery_index.rb +40 -1
- data/lib/rigor/scope/resolution_chain.rb +159 -11
- data/lib/rigor/scope.rb +211 -21
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +100 -10
- data/lib/rigor/sig_gen/method_candidate.rb +2 -2
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
- data/lib/rigor/source/alias_names.rb +52 -0
- data/lib/rigor/source/literals.rb +3 -2
- data/lib/rigor/source/symbol_name.rb +20 -0
- data/lib/rigor/type/combinator.rb +34 -2
- data/lib/rigor/type/float_range.rb +4 -1
- data/lib/rigor/type/nominal.rb +6 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
- data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
- data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
- data/plugins/rigor-alba/sig/alba.rbs +4 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
- data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
- data/sig/rigor/plugin/io_boundary.rbs +3 -0
- data/sig/rigor/scope.rbs +30 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +9 -4
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +83 -1
|
@@ -89,6 +89,14 @@ array. Line queries add `line_enumerations` metadata with the
|
|
|
89
89
|
shown and total expression counts. An exact position is the same
|
|
90
90
|
query the editor integration answers on hover.
|
|
91
91
|
|
|
92
|
+
Inside a `rescue` or `ensure` clause, `type-of` and hover can
|
|
93
|
+
disagree with `rigor check`. `check` reads a variable that the
|
|
94
|
+
`begin` body assigns as untyped there, because the clause may run
|
|
95
|
+
after the body raised partway through. `type-of` shows the value
|
|
96
|
+
the variable had before the `begin`. In
|
|
97
|
+
`conn = nil; begin; conn = open_conn; work; rescue; conn.close; end`,
|
|
98
|
+
`type-of` reports `conn` as `nil` and `check` reports nothing.
|
|
99
|
+
|
|
92
100
|
## `rigor trace` — watch the inference happen
|
|
93
101
|
|
|
94
102
|
Where `annotate` and `type-of` show the *answer*, `rigor trace
|
|
@@ -110,11 +118,53 @@ typer visits; the default keeps only the three teachable event
|
|
|
110
118
|
kinds. The JSON stream is stable enough to build course
|
|
111
119
|
material or figures from.
|
|
112
120
|
|
|
121
|
+
## `rigor lens` — a file's declarations and where their types come from
|
|
122
|
+
|
|
123
|
+
`rigor lens` lists every class, module, method and attribute a
|
|
124
|
+
file declares, with the type each one has and where that type
|
|
125
|
+
came from: `sig/`, an inline `#:` / `@rbs` comment, a plugin, a
|
|
126
|
+
library, or Rigor's inference from the body. It is experimental:
|
|
127
|
+
the command and its `lens/v0` JSON may still change.
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
rigor lens lib/shop.rb # text, for reading
|
|
131
|
+
rigor lens --format=json lib/*.rb # one JSON document
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
lib/shop.rb xxh3-64:845f7426b6acfe12 (2 omitted)
|
|
136
|
+
4:7c90 class Shop
|
|
137
|
+
8:e37e Shop#label: (::Integer n) -> ::String [contract inline]
|
|
138
|
+
14:ce93 Shop#total: () -> Integer [single_site inferred]
|
|
139
|
+
16:182b Shop#fetch [contested; also lib/legacy_shop.rb:2]
|
|
140
|
+
omitted constant Shop::LIMIT (line 5)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The answer is the one `rigor check` reaches. A lens runs the
|
|
144
|
+
same incremental analysis as `rigor check --incremental` and
|
|
145
|
+
shares its cache, so the first run costs a full check and later
|
|
146
|
+
runs reuse it. The diagnostics it reports for a file are the
|
|
147
|
+
ones `check` reports.
|
|
148
|
+
|
|
149
|
+
- A method Ruby could take from more than one definition (the
|
|
150
|
+
same method defined in two files) is `contested`. Rigor does
|
|
151
|
+
not guess which one runs, so it prints no signature for it.
|
|
152
|
+
- A declaration this version does not model yet (a constant, an
|
|
153
|
+
`alias`, a `define_method`) is listed as omitted, never
|
|
154
|
+
dropped.
|
|
155
|
+
- `LINE:HASH` is an anchor: it changes when the declaration or
|
|
156
|
+
the type comments above it change, and not when prose
|
|
157
|
+
comments do.
|
|
158
|
+
|
|
159
|
+
The JSON document is specified in
|
|
160
|
+
[the lens output contract](../internal-spec/lens.md).
|
|
161
|
+
|
|
113
162
|
## Which to reach for
|
|
114
163
|
|
|
115
164
|
| You want… | Use |
|
|
116
165
|
| --- | --- |
|
|
117
166
|
| One expression's type, from the shell | `rigor type-of` |
|
|
167
|
+
| A file's declarations, their types and where each came from | `rigor lens` |
|
|
118
168
|
| Every line of a file surveyed | `rigor annotate` |
|
|
119
169
|
| The derivation replayed step by step | `rigor trace` |
|
|
120
170
|
| A type printed mid-analysis, in context | `dump_type` |
|
data/docs/manual/06-baseline.md
CHANGED
|
@@ -77,6 +77,8 @@ a diagnostic another layer has already suppressed.
|
|
|
77
77
|
| `rigor baseline drift` | Show how buckets have moved — `--only=over` for buckets that grew, `reducible` for ones you have already improved past, `cleared` for empty ones. |
|
|
78
78
|
| `rigor baseline prune` | Drop buckets that match nothing any more. `--dry-run` previews. |
|
|
79
79
|
|
|
80
|
+
`generate` and `regenerate` analyse with your loaded configuration unchanged except that `baseline:` is off, so options such as `bleeding_edge`, `target_ruby` or `severity_overrides` shape the recorded set exactly as they shape `rigor check`.
|
|
81
|
+
|
|
80
82
|
`generate` and `regenerate` take `--match-mode=rule` (the
|
|
81
83
|
default — one bucket per file × rule) or `--match-mode=message`
|
|
82
84
|
(a bucket per distinct message: more precise, more churn).
|
data/docs/manual/07-plugins.md
CHANGED
|
@@ -54,7 +54,7 @@ each plugin's options — but the families today are:
|
|
|
54
54
|
`rigor-ffi` provides the `ffi_binding_recognizer` class DSL on
|
|
55
55
|
`Rigor::Plugin::Base` to recognize custom binding definitions.
|
|
56
56
|
- **Other ecosystems** — `rigor-sinatra`, `rigor-hanami`,
|
|
57
|
-
`rigor-devise`, `rigor-pundit`, `rigor-sidekiq`,
|
|
57
|
+
`rigor-devise`, `rigor-pundit`, `rigor-alba`, `rigor-typelizer`, `rigor-sidekiq`,
|
|
58
58
|
`rigor-graphql`, `rigor-statesman`, `rigor-sorbet`,
|
|
59
59
|
`rigor-typescript-utility-types`,
|
|
60
60
|
`rigor-activesupport-core-ext`.
|
data/docs/manual/12-caching.md
CHANGED
|
@@ -184,7 +184,8 @@ file, which other files its analysis read from, so it knows
|
|
|
184
184
|
exactly which files an edit can affect. A continuous-integration
|
|
185
185
|
gate, `rigor check --verify-incremental`, asserts this on every
|
|
186
186
|
build: it runs the incremental analyzer and a full analysis and
|
|
187
|
-
fails if they disagree on a single diagnostic
|
|
187
|
+
fails if they disagree on a single diagnostic, or list the same
|
|
188
|
+
diagnostics in a different order.
|
|
188
189
|
|
|
189
190
|
Rigor is also precise about *what* an edit changed. Re-checking a
|
|
190
191
|
file that many others depend on — a base class, a widely-used
|
|
@@ -259,6 +260,22 @@ version change does, or `--clear-cache`. So the shortcut can stay
|
|
|
259
260
|
off indefinitely; run `rigor check --incremental --clear-cache`
|
|
260
261
|
once to turn it back on.
|
|
261
262
|
|
|
263
|
+
|
|
264
|
+
A warm `--incremental` run says on stderr how far the edit spread,
|
|
265
|
+
for example `rigor: --incremental warm — re-analysed 3 of 1295 files
|
|
266
|
+
(1 changed), reused the rest`. The first count is the files the
|
|
267
|
+
recheck re-analysed, out of M, the analysed set: every project file
|
|
268
|
+
the run covers, or only the subset under a path argument such as
|
|
269
|
+
`rigor check --incremental lib`. The re-analysed files are M minus
|
|
270
|
+
those served from the snapshot. They include files that a change
|
|
271
|
+
reached without editing them, such as a file whose inferred
|
|
272
|
+
parameter types moved (ADR-67), not only the edited files and their
|
|
273
|
+
dependents. The parenthesis lists `changed`, and `added` or
|
|
274
|
+
`removed` when there are some. View templates from a plugin are not
|
|
275
|
+
in M. The recheck re-runs every template on every warm run, so they
|
|
276
|
+
are counted after the file clause, as `plus 2 view templates`. An
|
|
277
|
+
unchanged run reads `re-analysed 0 of 1295 files, reused all`.
|
|
278
|
+
|
|
262
279
|
`--incremental` is most useful for fast local re-checks and CI
|
|
263
280
|
on a changed branch.
|
|
264
281
|
|
|
@@ -116,8 +116,9 @@ gap in coverage rather than a finding about your code.
|
|
|
116
116
|
rigor unused --entry-point='lib/cli.rb' --entry-point='lib/workers/**/*.rb'
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
Repeatable. Each glob is matched against every declaration's path
|
|
120
|
-
|
|
119
|
+
Repeatable. Each glob is matched against every declaration's path as
|
|
120
|
+
written, relative to the project root, and relative to the directory
|
|
121
|
+
you run in, and roots every declaration in a matching file.
|
|
121
122
|
|
|
122
123
|
**File-level references**, free. Code that runs on load — an
|
|
123
124
|
initializer naming a class, a `.rake` task — roots what it names. This
|
|
@@ -145,7 +146,7 @@ Four sections, needing four different decisions:
|
|
|
145
146
|
| **Reachable only from test code** | Live test, no production caller Rigor can see or suspect | Work these first |
|
|
146
147
|
| **Candidates** | Nothing reachable names it | Adjudicate — most are still live |
|
|
147
148
|
| **Cannot decide** | Something can name it at runtime | Read the reason; do not delete from here |
|
|
148
|
-
| **Namespace-only** | A module wrapping
|
|
149
|
+
| **Namespace-only** | A module wrapping code production reaches | Excluded from candidates; count only |
|
|
149
150
|
|
|
150
151
|
Work them in that order — which is not the order you *read* the
|
|
151
152
|
summary in, where `roots` comes first because it tells you whether to
|
|
@@ -162,6 +163,15 @@ section held 22 policy classes whose only remaining caller was
|
|
|
162
163
|
`spec/policies/`. Those 22 were not adjudicated, so treat the section
|
|
163
164
|
as high-signal rather than high-yield; the yield is unmeasured.
|
|
164
165
|
|
|
166
|
+
"Test code" is decided by path, relative to the project root: the directory of the `.rigor.yml` in
|
|
167
|
+
use, or the directory you run `rigor unused` in when there is none or when the config sits below it
|
|
168
|
+
(`--config=ci/rigor.yml` from the project root). Running it from a subdirectory with `--config` reads
|
|
169
|
+
the same project, and references are read from the directory you run it in as well. A file counts
|
|
170
|
+
as test code when its path as written below either directory says so. The test paths are `spec/` and `test/` anywhere below the root,
|
|
171
|
+
`*_spec.rb` and `*_test.rb`, and the end-to-end trees `qa/`, `e2e/` and `features/` at the root. Tooling directories such as
|
|
172
|
+
`rubocop/`, `keeps/`, `tooling/` and `scripts/` count as production, so a class used only by them is
|
|
173
|
+
rooted rather than listed: deleting it would break the tooling.
|
|
174
|
+
|
|
165
175
|
### Candidates: sort by how conventional the class is
|
|
166
176
|
|
|
167
177
|
A candidate row is its name and declaration site:
|
|
@@ -171,6 +181,23 @@ Candidates — nothing reachable references these (45)
|
|
|
171
181
|
1 Api::V1::Timelines::TopicController app/controllers/api/v1/timelines/topic_controller.rb:3
|
|
172
182
|
```
|
|
173
183
|
|
|
184
|
+
A base class is reachable whenever one of its subclasses is:
|
|
185
|
+
`class Sub < Base` names `Base` on `Sub`'s behalf. So a base class
|
|
186
|
+
that nothing else names appears together with its subclasses when none
|
|
187
|
+
of them is reachable, and the decision belongs to the whole family
|
|
188
|
+
rather than to one row. When a subclass sits under **Cannot decide**,
|
|
189
|
+
its base moves there too.
|
|
190
|
+
|
|
191
|
+
A class declared outside `paths:` — `config/application.rb`'s
|
|
192
|
+
`Application`, a class in an initializer, a spec helper — is not
|
|
193
|
+
itself in the report, so what it names counts as named from its file:
|
|
194
|
+
`config.middleware.use MyMw` keeps `MyMw` live, and a fake subclass in
|
|
195
|
+
`spec/support` keeps its base reachable from tests. The same holds for
|
|
196
|
+
a gem class you reopen inside `paths:`. A name the file declares
|
|
197
|
+
itself still means that declaration: a migration's local
|
|
198
|
+
`class LegacyThing < ActiveRecord::Base` stub does not keep the app's
|
|
199
|
+
`LegacyThing` model live.
|
|
200
|
+
|
|
174
201
|
The 53 false positives in the adjudicated run fell into a few recurring
|
|
175
202
|
shapes, and 28 of them were the first one. Recognising these lets you
|
|
176
203
|
skip most of a list quickly:
|
|
@@ -230,7 +257,17 @@ belongs here instead, with the file named.
|
|
|
230
257
|
|
|
231
258
|
`"Foo".constantize` names `Foo` exactly, so it counts as an ordinary
|
|
232
259
|
reference and never reaches this section. `"Foo::#{key}".constantize`
|
|
233
|
-
can only bound the namespace, so everything under `Foo` is demoted
|
|
260
|
+
can only bound the namespace, so everything under `Foo` is demoted;
|
|
261
|
+
`"V#{version}"` is the start of a name, so `V1`, `V2_0` and everything
|
|
262
|
+
under them are. `const_get` looks the name up from its receiver, so
|
|
263
|
+
`const_get("V#{version}")` inside `class Migration` demotes
|
|
264
|
+
`Migration::V1` — and the `V*` names under `Migration`'s superclasses
|
|
265
|
+
and mixins and at the top level as well, unless the call passes `false`
|
|
266
|
+
as its second argument. A literal `const_get("Handler")` is a reference
|
|
267
|
+
looked up in the same order, so it names `Migration::Handler` when
|
|
268
|
+
that exists rather than a top-level `Handler`. A name built into a
|
|
269
|
+
local variable first (`name = "V#{version}"; const_get(name)`) is read
|
|
270
|
+
the same way. A
|
|
234
271
|
class name appearing as a string in a `.yml`, `.json` or template file
|
|
235
272
|
demotes the same way — weaker evidence than a constant reference, and
|
|
236
273
|
neither proof of use nor grounds to call it dead.
|
|
@@ -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)` |
|
|
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(...)` |
|
|
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)` |
|
|
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)` |
|
|
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:}>]]`.
|
|
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.**
|
|
16
|
-
#
|
|
17
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
72
|
+
walk(@root, span)
|
|
106
73
|
end
|
|
107
74
|
|
|
108
|
-
# `
|
|
109
|
-
|
|
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
|
-
|
|
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,
|
|
118
|
-
|
|
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,
|
|
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,
|
|
88
|
+
walk_children(node, locals)
|
|
128
89
|
end
|
|
129
90
|
|
|
130
|
-
def walk_children(node,
|
|
91
|
+
def walk_children(node, locals)
|
|
131
92
|
return if node.nil?
|
|
132
93
|
|
|
133
|
-
node.rigor_each_child { |child| walk(child,
|
|
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)
|