rigortype 0.3.5 → 0.3.7
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 +5 -5
- data/data/builtins/ruby_core/date.yml +6 -3
- data/data/builtins/ruby_core/enumerable.yml +1 -0
- data/data/builtins/ruby_core/exception.yml +2 -1
- data/data/builtins/ruby_core/file.yml +28 -14
- data/data/builtins/ruby_core/hash.yml +5 -2
- data/data/builtins/ruby_core/io.yml +33 -16
- data/data/builtins/ruby_core/random.yml +2 -1
- data/data/builtins/ruby_core/re.yml +6 -3
- data/data/builtins/ruby_core/struct.yml +2 -0
- data/data/builtins/ruby_core/time.yml +2 -1
- data/data/gem_overlay/activesupport/core_ext.rbs +442 -11
- data/docs/handbook/02-everyday-types.md +1 -1
- data/docs/handbook/04-tuples-and-shapes.md +1 -1
- data/docs/handbook/08-understanding-errors.md +1 -1
- data/docs/handbook/09-plugins.md +2 -2
- data/docs/handbook/10-sorbet.md +1 -1
- data/docs/handbook/README.md +2 -2
- data/docs/handbook/appendix-go.md +1 -1
- data/docs/handbook/appendix-java-csharp.md +2 -2
- data/docs/handbook/appendix-mypy.md +1 -1
- data/docs/handbook/appendix-protocols-and-structural-typing.md +2 -2
- data/docs/handbook/appendix-rust.md +1 -1
- data/docs/handbook/appendix-type-theory.md +2 -2
- data/docs/handbook/appendix-typescript.md +4 -4
- data/docs/manual/02-cli-reference.md +29 -16
- data/docs/manual/03-configuration.md +1 -1
- data/docs/manual/04-diagnostics.md +12 -0
- data/docs/manual/05-inspecting-types.md +20 -8
- data/docs/manual/07-plugins.md +4 -0
- data/docs/manual/08-skills.md +1 -1
- data/docs/manual/11-ci.md +1 -1
- data/docs/manual/12-caching.md +1 -1
- data/docs/manual/15-type-protection-coverage.md +1 -1
- data/docs/manual/18-removing-dead-code.md +13 -4
- data/docs/manual/19-effect-labels.md +19 -12
- data/docs/manual/README.md +2 -2
- data/docs/manual/plugins/rigor-actionmailer.md +4 -4
- data/docs/manual/plugins/rigor-actionpack.md +61 -0
- data/docs/manual/plugins/rigor-activejob.md +3 -3
- data/docs/manual/plugins/rigor-activerecord.md +78 -5
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
- data/docs/manual/plugins/rigor-rails-i18n.md +5 -5
- data/docs/manual/plugins/rigor-rspec.md +6 -2
- data/docs/manual/plugins/rigor-sidekiq.md +22 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
- data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
- data/lib/rigor/analysis/check_rules.rb +383 -48
- data/lib/rigor/analysis/crash_signature.rb +116 -0
- data/lib/rigor/analysis/dependency_recorder.rb +18 -3
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/diagnostic.rb +17 -6
- data/lib/rigor/analysis/effects_cache_probe.rb +132 -0
- data/lib/rigor/analysis/incremental.rb +28 -0
- data/lib/rigor/analysis/incremental_session.rb +44 -9
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
- data/lib/rigor/analysis/reachability/graph.rb +44 -16
- data/lib/rigor/analysis/reachability/plugin_roots.rb +11 -8
- data/lib/rigor/analysis/reachability/scan.rb +27 -7
- data/lib/rigor/analysis/reachability/scan_cache.rb +130 -0
- data/lib/rigor/analysis/result.rb +32 -0
- data/lib/rigor/analysis/run_cache_key.rb +53 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +14 -8
- data/lib/rigor/analysis/runner/pool_coordinator.rb +34 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
- data/lib/rigor/analysis/runner.rb +298 -38
- data/lib/rigor/analysis/worker_session.rb +10 -1
- data/lib/rigor/bleeding_edge.rb +7 -6
- data/lib/rigor/builtins/hkt_builtins.rb +45 -6
- data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
- data/lib/rigor/builtins/static_return_refinements.rb +20 -1
- data/lib/rigor/cache/descriptor.rb +42 -2
- data/lib/rigor/cache/file_digest.rb +20 -2
- data/lib/rigor/cache/incremental_snapshot.rb +40 -3
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +31 -8
- data/lib/rigor/cache/store.rb +10 -1
- data/lib/rigor/cli/check_command.rb +67 -48
- data/lib/rigor/cli/coverage_command.rb +28 -24
- data/lib/rigor/cli/coverage_mutation.rb +13 -17
- data/lib/rigor/cli/coverage_scan.rb +47 -6
- data/lib/rigor/cli/doc_links.rb +100 -0
- data/lib/rigor/cli/docs_command.rb +32 -2
- data/lib/rigor/cli/effects_command.rb +27 -2
- data/lib/rigor/cli/effects_diff_renderer.rb +82 -12
- data/lib/rigor/cli/effects_explain_renderer.rb +6 -3
- data/lib/rigor/cli/effects_snapshot_command.rb +52 -7
- data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
- data/lib/rigor/cli/fused_protection_report.rb +23 -2
- data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
- data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
- data/lib/rigor/cli/mutation_protection_report.rb +31 -2
- data/lib/rigor/cli/sig_gen_command.rb +18 -0
- data/lib/rigor/cli/type_of_command.rb +188 -41
- data/lib/rigor/cli/type_of_renderer.rb +72 -3
- data/lib/rigor/cli/type_scan_command.rb +10 -1
- data/lib/rigor/cli/unused_command.rb +65 -16
- data/lib/rigor/cli.rb +22 -9
- data/lib/rigor/configuration.rb +7 -4
- data/lib/rigor/effects/definition_lines.rb +100 -0
- data/lib/rigor/effects/envelope_check.rb +18 -1
- data/lib/rigor/effects/liskov_check.rb +17 -8
- data/lib/rigor/effects/plugin_facts.rb +1 -1
- data/lib/rigor/effects/scanner.rb +2 -3
- data/lib/rigor/effects/signature_sources.rb +13 -2
- data/lib/rigor/effects/snapshot.rb +53 -21
- data/lib/rigor/effects/snapshot_diff.rb +26 -3
- data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
- data/lib/rigor/environment/lockfile_resolver.rb +15 -5
- data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
- data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
- data/lib/rigor/environment/rbs_loader.rb +525 -46
- data/lib/rigor/environment.rb +38 -15
- data/lib/rigor/inference/acceptance.rb +172 -20
- data/lib/rigor/inference/captured_locals.rb +62 -0
- data/lib/rigor/inference/content_join.rb +347 -0
- data/lib/rigor/inference/def_handle.rb +15 -3
- data/lib/rigor/inference/def_node_resolver.rb +62 -3
- data/lib/rigor/inference/expression_typer.rb +1020 -141
- data/lib/rigor/inference/fork_map.rb +6 -1
- data/lib/rigor/inference/hkt_reducer.rb +12 -1
- data/lib/rigor/inference/hkt_registry.rb +46 -0
- data/lib/rigor/inference/hkt_sugar_translator.rb +93 -0
- data/lib/rigor/inference/index_write_widening.rb +48 -0
- data/lib/rigor/inference/indexed_narrowing.rb +14 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
- data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
- data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
- data/lib/rigor/inference/method_dispatcher.rb +155 -14
- data/lib/rigor/inference/method_parameter_binder.rb +3 -3
- data/lib/rigor/inference/mutation_widening.rb +244 -174
- data/lib/rigor/inference/narrowing.rb +331 -27
- data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
- data/lib/rigor/inference/pre_eval_constants.rb +15 -0
- data/lib/rigor/inference/precision_scanner.rb +5 -2
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +89 -39
- data/lib/rigor/inference/scope_indexer.rb +1493 -140
- data/lib/rigor/inference/statement_evaluator.rb +276 -91
- data/lib/rigor/inference/struct_fold_safety.rb +186 -27
- data/lib/rigor/inference/synthetic_method_scanner.rb +7 -0
- data/lib/rigor/inference/version_guard.rb +229 -0
- data/lib/rigor/language_server/selection_range_provider.rb +1 -1
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/inflector.rb +14 -5
- data/lib/rigor/plugin/io_boundary.rb +107 -4
- data/lib/rigor/plugin/loader.rb +14 -0
- data/lib/rigor/protection/analysis_guard.rb +60 -0
- data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
- data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
- data/lib/rigor/protection/discovery_seed.rb +1 -1
- data/lib/rigor/protection/measurement_integrity.rb +36 -0
- data/lib/rigor/protection/mutation_scanner.rb +61 -6
- data/lib/rigor/protection/mutator.rb +121 -18
- data/lib/rigor/rbs_extended/envelope_scanner.rb +9 -0
- data/lib/rigor/reflection.rb +182 -30
- data/lib/rigor/scope/discovery_index.rb +39 -1
- data/lib/rigor/scope.rb +385 -4
- data/lib/rigor/sig_gen/classification.rb +12 -1
- data/lib/rigor/sig_gen/generator.rb +254 -5
- data/lib/rigor/sig_gen/renderer.rb +1 -6
- data/lib/rigor/sig_gen/writer.rb +3 -0
- data/lib/rigor/source/constant_path.rb +79 -2
- data/lib/rigor/triage/catalogue.rb +1 -1
- data/lib/rigor/type/combinator.rb +10 -0
- data/lib/rigor/type/maybe.rb +47 -0
- data/lib/rigor/type/refined.rb +1 -2
- data/lib/rigor/type/result.rb +53 -0
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
- data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
- data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
- data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
- data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
- data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
- data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
- data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
- data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
- data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
- data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
- data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
- data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
- data/sig/rigor/analysis/baseline.rbs +1 -1
- data/sig/rigor/environment.rbs +3 -0
- data/sig/rigor/inference.rbs +4 -1
- data/sig/rigor/plugin/io_boundary.rbs +2 -0
- data/sig/rigor/reflection.rbs +3 -1
- data/sig/rigor/scope.rbs +25 -1
- data/sig/rigor/type.rbs +95 -1
- data/sig/rigor.rbs +16 -0
- data/skills/rigor-ci-setup/SKILL.md +2 -2
- data/skills/rigor-editor-setup/SKILL.md +2 -2
- data/skills/rigor-mcp-setup/SKILL.md +2 -2
- data/skills/rigor-monkeypatch-resolve/SKILL.md +1 -1
- data/skills/rigor-plugin-review/SKILL.md +3 -3
- metadata +73 -3
|
@@ -18,11 +18,11 @@ plugins:
|
|
|
18
18
|
## What it checks
|
|
19
19
|
|
|
20
20
|
```text
|
|
21
|
-
demo.rb:
|
|
22
|
-
demo.rb:
|
|
21
|
+
demo.rb:18:1: info: `User.find` returns User (table: `users`) [plugin.activerecord.model-call]
|
|
22
|
+
demo.rb:21:1: info: `User.where` (:admin) on table `users` [plugin.activerecord.model-call]
|
|
23
23
|
|
|
24
|
-
errors_demo.rb:
|
|
25
|
-
errors_demo.rb:
|
|
24
|
+
errors_demo.rb:12:1: error: `User.where(emial: ...)` references unknown column `emial` on table `users` (did you mean `:email`?) [plugin.activerecord.unknown-column]
|
|
25
|
+
errors_demo.rb:24:1: error: `User.find` expects at least 1 argument, got 0 [plugin.activerecord.wrong-arity]
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
| Diagnostic | Severity | Rule |
|
|
@@ -30,7 +30,8 @@ errors_demo.rb:25:1: error: `User.find` expects at least 1 argument, got 0 [plug
|
|
|
30
30
|
| Recognised `Model.find` / `Model.find_by` / `Model.where` call | `:info` | `plugin.activerecord.model-call` |
|
|
31
31
|
| `Model.find_by(unknown: ...)` / `Model.where(unknown: ...)` | `:error` | `plugin.activerecord.unknown-column` |
|
|
32
32
|
| `Model.find` with 0 args | `:error` | `plugin.activerecord.wrong-arity` |
|
|
33
|
-
| No schema source (`db/schema.rb` or `db/structure.sql`)
|
|
33
|
+
| No schema source (`db/schema.rb` or `db/structure.sql`) present — reduced mode | `:info` | `plugin.activerecord.load-error` |
|
|
34
|
+
| A schema source that exists but cannot be read or parsed | `:warning` | `plugin.activerecord.load-error` |
|
|
34
35
|
|
|
35
36
|
Did-you-mean suggestions use `DidYouMean` fuzzy matching against
|
|
36
37
|
the resolved table's column names.
|
|
@@ -75,18 +76,90 @@ Chained query methods keep the element type, and iteration
|
|
|
75
76
|
scope invoked on a typed relation (`User.where(...).published`)
|
|
76
77
|
never surfaces a false `call.undefined-method`.
|
|
77
78
|
|
|
79
|
+
`User.table_name` types as `String`, and as the exact string
|
|
80
|
+
only when your source says the name: a literal
|
|
81
|
+
`self.table_name = "people"` on the class or on an STI ancestor,
|
|
82
|
+
with nothing in that chain computing the name at runtime (a
|
|
83
|
+
`def self.table_name`, a `class << self` version of it, or an
|
|
84
|
+
interpolated assignment all count as computing it). Every other
|
|
85
|
+
name — anything the plugin derived by pluralizing the class name —
|
|
86
|
+
stays plain `String`.
|
|
87
|
+
|
|
88
|
+
That includes names that look confirmed. A `users` table in your
|
|
89
|
+
schema is not evidence that it is `User`'s table: with a
|
|
90
|
+
`self.table_name_prefix` on the base class, `User` really reads
|
|
91
|
+
`app_users`, and a `users` table belonging to some other model
|
|
92
|
+
would "confirm" the wrong guess. A wrong exact string is worse
|
|
93
|
+
than an honest `String` — code comparing `User.table_name` would
|
|
94
|
+
quietly take the wrong branch — so the plugin pins only what you
|
|
95
|
+
wrote down. `User.quoted_table_name` is always `String`; the
|
|
96
|
+
quoting is up to the database adapter.
|
|
97
|
+
|
|
98
|
+
A model declared inside a Ruby module or class (`Blog::Post`)
|
|
99
|
+
resolves its table the way Rails does for the cases below: the
|
|
100
|
+
namespace is dropped, not flattened into the name, so `Blog::Post`
|
|
101
|
+
reads `posts`, not `blog_posts`. A `table_name_prefix` /
|
|
102
|
+
`table_name_suffix` the enclosing namespace declares as a literal
|
|
103
|
+
(`def self.table_name_prefix = "blog_"`, `class << self` with the
|
|
104
|
+
same, or `mattr_accessor :table_name_prefix, default: "blog_"`) is
|
|
105
|
+
applied on top, so the same model reads `blog_posts` once `Blog`
|
|
106
|
+
sets that. `mattr_writer` does not count — it defines no reader, so
|
|
107
|
+
Rails never actually reads the value back, and neither does the
|
|
108
|
+
plugin.
|
|
109
|
+
|
|
110
|
+
`Blog::Post.table_name` still reads as the plain demodulized name
|
|
111
|
+
(`posts`) when `Blog`'s prefix/suffix is declared in a shape the
|
|
112
|
+
plugin cannot read as a literal (a computed value, two disagreeing
|
|
113
|
+
declarations) — but that string is informational only in this case.
|
|
114
|
+
The plugin does not trust it enough to look up columns against it:
|
|
115
|
+
guessing a bare name is the guess most likely to hit an unrelated
|
|
116
|
+
REAL table in a namespaced app, and a wrong corroboration is worse
|
|
117
|
+
than none, so `Blog::Post`'s column, alias and association checks
|
|
118
|
+
stand down entirely rather than run against a table that might not
|
|
119
|
+
be the real one.
|
|
120
|
+
|
|
78
121
|
## Limitations
|
|
79
122
|
|
|
80
123
|
- **Direct-superclass match only.** `class Admin < User` where
|
|
81
124
|
`User < ApplicationRecord` is not discovered. Either add `User`
|
|
82
125
|
to `model_base_classes`, or list every concrete model
|
|
83
126
|
explicitly.
|
|
127
|
+
- **A model nested inside ANOTHER non-abstract model class stands down rather
|
|
128
|
+
than guesses.** `Post::Comment` where `Post < ApplicationRecord` and `Post`
|
|
129
|
+
is not abstract hits a different Rails naming rule entirely — the parent's own
|
|
130
|
+
table name is spliced into the middle of the child's, not a
|
|
131
|
+
prefix/suffix — so the plugin recognises the shape and stands
|
|
132
|
+
`Comment`'s column / alias / association checks down instead of
|
|
133
|
+
computing (or guessing at) the real name. (A model nested inside
|
|
134
|
+
an *abstract* parent class, such as `Base::Comment` where `Base` declares
|
|
135
|
+
`self.abstract_class = true` or `primary_abstract_class`, correctly resolves
|
|
136
|
+
its plain demodulized table name with full column checks.)
|
|
137
|
+
- **External `table_name_prefix` / `table_name_suffix` declarations and
|
|
138
|
+
engines.** Declarations outside `model_search_paths` (e.g. in `lib/` or an
|
|
139
|
+
engine's `isolate_namespace`) are detected across the project and cause
|
|
140
|
+
affected models to safely stand down with an empty column set, rather than
|
|
141
|
+
guessing an incorrect table name. Within `model_search_paths`, model-level
|
|
142
|
+
and base-class `table_name_prefix` declarations (literal or computed) are
|
|
143
|
+
resolved directly.
|
|
84
144
|
- **PostgreSQL `db/structure.sql` fallback.** When `db/schema.rb` is
|
|
85
145
|
absent, the plugin parses `db/structure.sql` (the `schema_format =
|
|
86
146
|
:sql` dump) for the same column/type table. It reads PostgreSQL DDL
|
|
87
147
|
only; a column whose SQL type has no Ruby mapping (a custom enum,
|
|
88
148
|
`tsvector`, `ltree`) degrades to `Object` (never dropped), and
|
|
89
149
|
non-`public`-schema partition tables are skipped.
|
|
150
|
+
- **No committed schema — reduced mode.** A project that ships raw
|
|
151
|
+
migrations and gitignores `db/schema.rb` (the DB-agnostic Rails
|
|
152
|
+
pattern) still gets table names, finders, scopes and associations:
|
|
153
|
+
those are read from your model source, not from the schema. Only
|
|
154
|
+
the column-dependent half stands down — column readers stay
|
|
155
|
+
untyped and `where(col:)` keys are not validated, exactly as they
|
|
156
|
+
are for a table the schema does not describe. The plugin says so
|
|
157
|
+
once per run at `:info`. Committing a schema dump (or pointing
|
|
158
|
+
`schema_file` / `structure_sql_file` at one) turns the column half
|
|
159
|
+
back on from the next cold run — a warm cache keeps serving the
|
|
160
|
+
reduced index until it is invalidated, so use `rigor check
|
|
161
|
+
--no-cache` (or `make cache-clean`) if you want to see the change
|
|
162
|
+
immediately.
|
|
90
163
|
- **Column reads, not setters.** The plugin types instance-side
|
|
91
164
|
column *reads* (`user.name`, `user.admin?`) and singular
|
|
92
165
|
associations, but not the `name=` setter or the dirty-tracking
|
|
@@ -44,7 +44,9 @@ Roughly the top ~40 selectors plus their close neighbours, across:
|
|
|
44
44
|
`#constantize`, `#pluralize`, …), filters (`#squish`, `#truncate`),
|
|
45
45
|
`#html_safe`, `#starts_with?` / `#ends_with?`, conversions.
|
|
46
46
|
- **Time / Date / DateTime** — `.current`, `.zone`, `#yesterday`,
|
|
47
|
-
`#tomorrow`, `#beginning_of_*` / `#end_of_*`, `#ago`, `#since`.
|
|
47
|
+
`#tomorrow`, `#beginning_of_*` / `#end_of_*`, `#ago`, `#since`. `Time`
|
|
48
|
+
additionally carries its **whole** Rails instance surface (see below);
|
|
49
|
+
`Date` and `DateTime` carry the same subset they always did.
|
|
48
50
|
- **Array** — `.wrap`, `#to_sentence`, `#in_groups_of`, `#second` …
|
|
49
51
|
`#fifth`, `#compact_blank`, `#exclude?`.
|
|
50
52
|
- **Hash** — `#symbolize_keys` / `#stringify_keys` (+ deep / bang),
|
|
@@ -57,25 +59,125 @@ Roughly the top ~40 selectors plus their close neighbours, across:
|
|
|
57
59
|
Time.current # without the bundle: call.undefined-method Time.current
|
|
58
60
|
```
|
|
59
61
|
|
|
62
|
+
## Durations are typed
|
|
63
|
+
|
|
64
|
+
`1.day`, `5.minutes`, `2.5.hours` and every other multiplier type as
|
|
65
|
+
`ActiveSupport::Duration`, and the arithmetic around them keeps its
|
|
66
|
+
meaning:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
1.day # ActiveSupport::Duration
|
|
70
|
+
Time.current - 30.minutes # Time
|
|
71
|
+
2 * 1.day # ActiveSupport::Duration
|
|
72
|
+
1.day + 1.hour # ActiveSupport::Duration
|
|
73
|
+
Date.today - 1.week # Date | Time
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`Date ± duration` is a union because that is what Rails does: a
|
|
77
|
+
date-part duration gives you back a `Date`, a sub-day one gives you a
|
|
78
|
+
`Time`.
|
|
79
|
+
|
|
80
|
+
Rigor ships a **partial** signature for `ActiveSupport::Duration`: the
|
|
81
|
+
reader surface — `#to_i` / `#in_seconds`, `#to_f`, `#in_minutes` /
|
|
82
|
+
`#in_hours` / `#in_days` / `#in_weeks` / `#in_months` / `#in_years`,
|
|
83
|
+
`#iso8601`, `#parts` — is typed, so `3.hours.in_minutes` is `Float` and
|
|
84
|
+
`1.day.to_i * 2` is `Integer`. `#ago` / `#until` / `#before` / `#since`
|
|
85
|
+
/ `#from_now` / `#after` are NOT part of that surface — they default
|
|
86
|
+
to `Time.current`, and typing them was blocked on Rails' `Time`
|
|
87
|
+
instance extensions being declared first, which the section below now
|
|
88
|
+
does; the multipliers themselves are tracked separately. Every
|
|
89
|
+
other member — the arithmetic operators above aside, `==`, and
|
|
90
|
+
anything else Duration forwards through `method_missing` — resolves
|
|
91
|
+
without a diagnostic too, while the site still counts as a concrete
|
|
92
|
+
receiver for `rigor coverage --protection`. Naming
|
|
93
|
+
`ActiveSupport::Duration` at all would normally be the wrong move — a
|
|
94
|
+
partial signature on a class whose real surface forwards to
|
|
95
|
+
`method_missing` turns every omitted member into a false
|
|
96
|
+
`call.undefined-method` — so the plugin lists it under
|
|
97
|
+
`open_receivers:`, the same exemption `rigor-activerecord` gives
|
|
98
|
+
`ActiveRecord::Relation`.
|
|
99
|
+
|
|
100
|
+
The multiplier only fires on a receiver Rigor has proven numeric, so
|
|
101
|
+
`created_at.day`, `Date.today.year` and your own object's `#days` keep
|
|
102
|
+
the answers they always had.
|
|
103
|
+
|
|
104
|
+
## The Rails `Time` instance surface is declared, not sampled
|
|
105
|
+
|
|
106
|
+
`Time` is a core Ruby class, so RBS knows it fully and it is **closed**:
|
|
107
|
+
a name the signatures do not declare is reported
|
|
108
|
+
`call.undefined-method`. That makes an omission on `Time` just as much
|
|
109
|
+
of a false positive as a wrong return type, with no gradual middle, so
|
|
110
|
+
this bundle declares the surface ActiveSupport adds by audit against the
|
|
111
|
+
gem's own sources rather than by a "top selectors" sample.
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
Time.current.to_fs(:db) # String
|
|
115
|
+
Time.current.formatted_offset # String
|
|
116
|
+
Time.current.past? # bool
|
|
117
|
+
Time.current.at_beginning_of_hour # Time
|
|
118
|
+
Time.current.days_ago(3).all_week # Range[Time]
|
|
119
|
+
Time.current.in_time_zone("Hawaii") # untyped (ActiveSupport::TimeWithZone)
|
|
120
|
+
Time.current.definitely_not_here # still call.undefined-method
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
That is the predicates (`#past?`, `#future?`, `#today?`, `#on_weekend?`,
|
|
124
|
+
…), the whole `#days_ago` / `#months_since` / `#next_occurring` family,
|
|
125
|
+
the quarter and `at_`-prefixed spellings, the `#all_week` / `#all_month`
|
|
126
|
+
/ `#all_quarter` / `#all_year` ranges, `#to_fs` / `#to_formatted_s` /
|
|
127
|
+
`#formatted_offset` / `#rfc3339`, `#in_time_zone`, and the `Time.`
|
|
128
|
+
singletons `.days_in_month`, `.days_in_year`, `.rfc3339`, `.use_zone`,
|
|
129
|
+
`.find_zone` / `.find_zone!` and `.zone_default`.
|
|
130
|
+
|
|
131
|
+
Where a return cannot honestly be named it is widened rather than
|
|
132
|
+
guessed: `#in_time_zone` answers an `ActiveSupport::TimeWithZone`, which
|
|
133
|
+
this bundle does not model, so it reads `untyped`.
|
|
134
|
+
|
|
135
|
+
What is left out is twelve names, measured against a real
|
|
136
|
+
`require "active_support/all"`: ten instance and two singleton, every one
|
|
137
|
+
an `alias_method` artefact of ActiveSupport's own `+` / `-` / `<=>` /
|
|
138
|
+
`eql?` / `Time.at` overrides — the `plus_with{,out}_duration`,
|
|
139
|
+
`minus_with{,out}_duration`, `minus_with{,out}_coercion`,
|
|
140
|
+
`compare_with{,out}_coercion`, `eql_with{,out}_coercion` and
|
|
141
|
+
`Time.at_with{,out}_coercion` pairs. They are public at runtime and
|
|
142
|
+
`:nodoc:` in the source, and nothing outside ActiveSupport calls them;
|
|
143
|
+
code that does will see them reported.
|
|
144
|
+
|
|
145
|
+
`Date` and `DateTime` are extended by the same ActiveSupport modules and
|
|
146
|
+
do **not** carry this yet — `Date.current.past?` still reports.
|
|
147
|
+
|
|
60
148
|
## No diagnostics, no config
|
|
61
149
|
|
|
62
|
-
The plugin
|
|
63
|
-
|
|
64
|
-
listed under `plugins:`.
|
|
150
|
+
The plugin emits no diagnostics and has no configuration knobs. It
|
|
151
|
+
contributes its signatures — and the Duration typing above —
|
|
152
|
+
unconditionally when listed under `plugins:`.
|
|
65
153
|
|
|
66
154
|
## Limitations
|
|
67
155
|
|
|
68
|
-
- **Conservative return types.** `
|
|
69
|
-
`
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
156
|
+
- **Conservative return types.** `#html_safe` is typed `String` (not
|
|
157
|
+
`SafeBuffer`) and `#try` / `#try!` return `untyped` — the goal for
|
|
158
|
+
those is to silence undefined-method, not to give precise returns.
|
|
159
|
+
(The Duration multipliers are one exception: they are declared
|
|
160
|
+
`untyped` in the bundle and then typed by the plugin instead — not
|
|
161
|
+
because the bundle can't name `ActiveSupport::Duration` (it does,
|
|
162
|
+
described above), but because moving the multiplier return itself
|
|
163
|
+
into RBS to match hasn't happened yet. `ActiveSupport::Duration`'s
|
|
164
|
+
own reader surface is the other exception, described above.)
|
|
165
|
+
- **`duration / x` is not typed.** `1.day / 2` is a Duration but
|
|
166
|
+
`1.day / 1.hour` is a plain `24`; the answer depends on the operand,
|
|
167
|
+
so Rigor declines rather than guessing.
|
|
168
|
+
- **`duration + Time` is not typed either.** `30.minutes + Time.now`
|
|
169
|
+
raises at runtime — `Duration#+` cannot coerce a Time, and `-`, `*`,
|
|
170
|
+
and a `Date` or `DateTime` on the right fail the same way — so Rigor
|
|
171
|
+
claims nothing for it. `Time.now + 30.minutes` is the form that has a
|
|
172
|
+
value, and it is typed `Time`.
|
|
74
173
|
- **Project-private monkey-patches are not covered** — only real
|
|
75
174
|
ActiveSupport extensions. For your own core-class patches see the
|
|
76
175
|
`pre_eval:` mechanism ([ADR-17](../../adr/17-monkey-patch-pre-evaluation.md)).
|
|
77
|
-
- **Top ~40 selectors, not exhaustive
|
|
78
|
-
|
|
176
|
+
- **Top ~40 selectors, not exhaustive** — except on `Time`, where the
|
|
177
|
+
closed-core-class argument above makes a sample unsound and the audit
|
|
178
|
+
is exhaustive but for the twelve `:nodoc:` alias-chain artefacts named
|
|
179
|
+
there. Elsewhere ActiveSupport ships hundreds of extensions and this
|
|
180
|
+
covers the head of the real-world distribution.
|
|
79
181
|
|
|
80
182
|
## Plugin internals
|
|
81
183
|
|
|
@@ -19,11 +19,11 @@ Against a locale catalogue, every statically-resolvable call site
|
|
|
19
19
|
is validated:
|
|
20
20
|
|
|
21
21
|
```text
|
|
22
|
-
demo.rb:
|
|
23
|
-
errors_demo.rb:
|
|
24
|
-
errors_demo.rb:
|
|
25
|
-
errors_demo.rb:
|
|
26
|
-
errors_demo.rb:
|
|
22
|
+
demo.rb:12:1: info: `t('users.welcome')` resolves in en, ja [plugin.rails-i18n.translation-call]
|
|
23
|
+
errors_demo.rb:10:1: error: missing translation key `users.welcom` in any locale (did you mean `users.welcome`?) [plugin.rails-i18n.unknown-key]
|
|
24
|
+
errors_demo.rb:14:1: error: `t('users.welcome')` expects interpolation `name`, got (none) [plugin.rails-i18n.wrong-interpolation]
|
|
25
|
+
errors_demo.rb:18:1: warning: `t('users.welcome')` does not use interpolation `extra` (known placeholders: `name`) [plugin.rails-i18n.extra-interpolation]
|
|
26
|
+
errors_demo.rb:23:1: warning: `t('errors.messages.blank')` is missing from locale(s) ja [plugin.rails-i18n.missing-locale]
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
1. **Key existence** — a key absent from every locale is flagged,
|
|
@@ -29,9 +29,13 @@ RSpec.describe "User" do
|
|
|
29
29
|
end
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
The snippet above is condensed for reading; the output below is what
|
|
33
|
+
Rigor actually prints for the plugin's own demo
|
|
34
|
+
(`plugins/rigor-rspec/demo/`), so the line numbers are that file's:
|
|
35
|
+
|
|
32
36
|
```text
|
|
33
|
-
spec/
|
|
34
|
-
spec/
|
|
37
|
+
spec/errors_spec.rb:23:3: warning: duplicate `let(:user)` in this scope (first declared at line 22); the last declaration wins at runtime [plugin.rspec.duplicate-let]
|
|
38
|
+
spec/errors_spec.rb:27:3: error: `let(:tags)` references its own name `tags` — this will infinite-loop at runtime [plugin.rspec.self-reference]
|
|
35
39
|
```
|
|
36
40
|
|
|
37
41
|
1. **Duplicate `let` / `subject` declarations** within the same
|
|
@@ -42,6 +42,28 @@ arguments.
|
|
|
42
42
|
| `plugin.sidekiq.missing-schedule` | error | `perform_in()` / `perform_at()` called with zero arguments (the schedule is required even when `#perform` takes none) |
|
|
43
43
|
| `plugin.sidekiq.load-error` | warning | worker discovery failed (parse/read error) — once per file |
|
|
44
44
|
|
|
45
|
+
## What it types
|
|
46
|
+
|
|
47
|
+
`perform_async` / `perform_in` / `perform_at` on a **discovered**
|
|
48
|
+
worker return the job id, so the value you assign is a `String`:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
jid = WelcomeEmailWorker.perform_async(123)
|
|
52
|
+
jid.upcase # String — resolved, and checked
|
|
53
|
+
OtherThing.perform_async # untouched: not a discovered worker
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`perform_inline` is not typed — it runs the job in-process and returns
|
|
57
|
+
whatever your `#perform` returns.
|
|
58
|
+
|
|
59
|
+
The type is plain `String`, not `String?`, even though
|
|
60
|
+
`Sidekiq::Client#push` returns nil when a client middleware halts the
|
|
61
|
+
chain. That path needs a middleware in your own app that returns false
|
|
62
|
+
from `#call` — rare, deliberate, and code that already knows to check —
|
|
63
|
+
whereas typing it nullable puts a `call.possible-nil-receiver` error on
|
|
64
|
+
the ordinary `jid = W.perform_async(id); jid.length`. Rigor takes the
|
|
65
|
+
answer that is silent on the common code.
|
|
66
|
+
|
|
45
67
|
## Configuration
|
|
46
68
|
|
|
47
69
|
```yaml
|
|
@@ -5,6 +5,7 @@ require "prism"
|
|
|
5
5
|
require_relative "../../source/node_children"
|
|
6
6
|
require_relative "../../inference/optimistic_origin"
|
|
7
7
|
require_relative "inferred_param_guard"
|
|
8
|
+
require_relative "published_constant_guard"
|
|
8
9
|
|
|
9
10
|
module Rigor
|
|
10
11
|
module Analysis
|
|
@@ -119,6 +120,17 @@ module Rigor
|
|
|
119
120
|
predicate_type = scope.type_of(predicate)
|
|
120
121
|
return unless predicate_type.is_a?(Type::Constant)
|
|
121
122
|
|
|
123
|
+
# Issue #644 — a predicate whose constancy rests on a value constant declared in ANOTHER file is
|
|
124
|
+
# not a logic error the reader's author can see: `if MODE == :production` folds only because the
|
|
125
|
+
# project-wide table published `MODE`, and a configuration constant read in ten files would put
|
|
126
|
+
# this warning in all ten. The value stays published (dispatch and argument typing keep it); only
|
|
127
|
+
# the firing is withheld, which is the direction the carrier discipline allows.
|
|
128
|
+
#
|
|
129
|
+
# LAST of the gates on purpose. It is the only one that can resolve a def and walk its body, and a
|
|
130
|
+
# predicate that does not fold to a `Type::Constant` was never going to fire — so the fold check
|
|
131
|
+
# above is what keeps the hop off every ordinary `if` in a project that publishes anything at all.
|
|
132
|
+
return if PublishedConstantGuard.rooted?(predicate, scope)
|
|
133
|
+
|
|
122
134
|
polarity = predicate_type.value.nil? || predicate_type.value == false ? :falsey : :truthy
|
|
123
135
|
@results << Result.new(node: predicate, polarity: polarity)
|
|
124
136
|
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../../inference/version_guard"
|
|
6
|
+
require_relative "../../source/node_children"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Analysis
|
|
10
|
+
module CheckRules
|
|
11
|
+
# ADR-47 WD5 — drops the diagnostics that land inside the **dead arm of a decidable version guard**
|
|
12
|
+
# (issue #627).
|
|
13
|
+
#
|
|
14
|
+
# {Inference::StatementEvaluator} already skips evaluating that arm, so its writes never join into the
|
|
15
|
+
# post-`if` scope. Skipping the evaluation is not by itself enough to silence the arm, though: the
|
|
16
|
+
# rule walk visits every node of the file whether or not the evaluator typed it, so a call whose
|
|
17
|
+
# receiver is typeable on its own — a constant, a literal — still reports. `mail/lib/mail/yaml.rb`'s
|
|
18
|
+
#
|
|
19
|
+
# ::YAML.safe_load(yaml, permitted_classes) # the Psych < 3.1 positional form
|
|
20
|
+
#
|
|
21
|
+
# is exactly that shape: honest against the Ruby 4 Psych signature, and unreachable on the Ruby the
|
|
22
|
+
# user is checking with. This is the second half of the fix — the arm stops producing diagnostics.
|
|
23
|
+
#
|
|
24
|
+
# The dead arms are found by re-asking {Inference::VersionGuard}, which is a pure function of the AST,
|
|
25
|
+
# so this filter and the evaluator's arm elision cannot disagree about which arm is dead.
|
|
26
|
+
#
|
|
27
|
+
# Applied to the type / flow rules only, and BEFORE `suppression.*` joins the list: a malformed
|
|
28
|
+
# `# rigor:disable` marker inside a dead arm is still a real authoring error — the marker's own
|
|
29
|
+
# well-formedness does not depend on whether the code around it runs.
|
|
30
|
+
module DeadVersionGuardArms
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# @param diagnostics [Array<Rigor::Analysis::Diagnostic>]
|
|
34
|
+
# @param root [Prism::Node]
|
|
35
|
+
# @return [Array<Rigor::Analysis::Diagnostic>]
|
|
36
|
+
def filter(diagnostics, root)
|
|
37
|
+
# The scan is a whole-file walk, so it is paid only when there is something to drop. A file with
|
|
38
|
+
# no diagnostics — the overwhelming majority — never walks.
|
|
39
|
+
return diagnostics if diagnostics.empty?
|
|
40
|
+
|
|
41
|
+
arms = scan(root)
|
|
42
|
+
return diagnostics if arms.empty?
|
|
43
|
+
|
|
44
|
+
diagnostics.reject { |diagnostic| arms.any? { |arm| covers?(arm, diagnostic) } }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @param root [Prism::Node]
|
|
48
|
+
# @return [Array<Prism::Location>] the source ranges of every dead version-guard arm
|
|
49
|
+
def scan(root)
|
|
50
|
+
arms = []
|
|
51
|
+
collect(root, arms)
|
|
52
|
+
arms
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def collect(node, arms)
|
|
56
|
+
return unless node.is_a?(Prism::Node)
|
|
57
|
+
|
|
58
|
+
dead = dead_arm(node)
|
|
59
|
+
arms << dead.location if dead
|
|
60
|
+
# Nothing inside a dead arm can produce a surviving diagnostic, so the walk does not descend into
|
|
61
|
+
# it — a nested guard there would only add a range already covered.
|
|
62
|
+
node.rigor_each_child { |child| collect(child, arms) unless dead && child.equal?(dead) }
|
|
63
|
+
end
|
|
64
|
+
private_class_method :collect
|
|
65
|
+
|
|
66
|
+
# The arm that cannot run, or nil when the guard is undecidable (both arms live — the pre-existing
|
|
67
|
+
# behaviour) or the dead arm is absent (`foo if RUBY_VERSION >= "3.1"` has no `else`).
|
|
68
|
+
def dead_arm(node)
|
|
69
|
+
case node
|
|
70
|
+
when Prism::IfNode
|
|
71
|
+
case Inference::VersionGuard.verdict(node.predicate)
|
|
72
|
+
when :truthy then node.subsequent
|
|
73
|
+
when :falsey then node.statements
|
|
74
|
+
end
|
|
75
|
+
when Prism::UnlessNode
|
|
76
|
+
# `unless` runs its body on the FALSEY edge, so the arms are swapped.
|
|
77
|
+
case Inference::VersionGuard.verdict(node.predicate)
|
|
78
|
+
when :truthy then node.statements
|
|
79
|
+
when :falsey then node.else_clause
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
private_class_method :dead_arm
|
|
84
|
+
|
|
85
|
+
# Prism columns are 0-based and `Diagnostic#column` is 1-based; the location's end is exclusive.
|
|
86
|
+
# Compared as `[line, column]` pairs rather than by line alone so a one-line guard
|
|
87
|
+
# (`RUBY_VERSION >= "3.1" ? a(1) : a(1, 2)`) drops only the dead half.
|
|
88
|
+
def covers?(location, diagnostic)
|
|
89
|
+
position = [diagnostic.line, diagnostic.column - 1]
|
|
90
|
+
return false if (position <=> [location.start_line, location.start_column]).negative?
|
|
91
|
+
|
|
92
|
+
(position <=> [location.end_line, location.end_column]).negative?
|
|
93
|
+
end
|
|
94
|
+
private_class_method :covers?
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -69,9 +69,9 @@ module Rigor
|
|
|
69
69
|
|
|
70
70
|
case node
|
|
71
71
|
when Prism::ClassNode, Prism::ModuleNode
|
|
72
|
-
|
|
73
|
-
if
|
|
74
|
-
walk(node.body,
|
|
72
|
+
child_prefix = Source::ConstantPath.declaration_prefix(qualified_prefix, node.constant_path)
|
|
73
|
+
if child_prefix
|
|
74
|
+
walk(node.body, child_prefix) if node.body
|
|
75
75
|
return
|
|
76
76
|
end
|
|
77
77
|
when Prism::DefNode
|