rigortype 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- 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 +409 -11
- data/docs/handbook/11-sig-gen.md +24 -14
- data/docs/manual/02-cli-reference.md +23 -7
- data/docs/manual/04-diagnostics.md +1 -1
- data/docs/manual/05-inspecting-types.md +20 -8
- data/docs/manual/07-plugins.md +4 -0
- data/docs/manual/plugins/rigor-actionpack.md +61 -0
- data/docs/manual/plugins/rigor-activerecord.md +74 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
- 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 +188 -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/incremental.rb +28 -0
- data/lib/rigor/analysis/incremental_session.rb +63 -16
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
- data/lib/rigor/analysis/reachability/graph.rb +14 -5
- data/lib/rigor/analysis/reachability/scan.rb +7 -3
- data/lib/rigor/analysis/result.rb +32 -0
- data/lib/rigor/analysis/run_cache_key.rb +41 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +233 -16
- data/lib/rigor/analysis/runner/pool_coordinator.rb +266 -47
- data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +12 -2
- data/lib/rigor/analysis/runner.rb +212 -16
- data/lib/rigor/analysis/worker_session.rb +37 -3
- data/lib/rigor/bleeding_edge.rb +7 -6
- data/lib/rigor/builtins/hkt_builtins.rb +45 -6
- data/lib/rigor/builtins/imported_refinements.rb +11 -9
- data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
- data/lib/rigor/builtins/static_return_refinements.rb +20 -1
- data/lib/rigor/cache/annotation_location.rb +72 -0
- data/lib/rigor/cache/descriptor.rb +42 -2
- data/lib/rigor/cache/incremental_snapshot.rb +40 -3
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +70 -8
- data/lib/rigor/cache/store.rb +17 -1
- data/lib/rigor/cli/coverage_command.rb +23 -18
- data/lib/rigor/cli/coverage_mutation.rb +13 -17
- data/lib/rigor/cli/coverage_scan.rb +47 -6
- 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 +47 -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.rb +1 -1
- data/lib/rigor/configuration.rb +7 -4
- data/lib/rigor/effects/plugin_facts.rb +1 -1
- data/lib/rigor/effects/scanner.rb +2 -3
- data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
- data/lib/rigor/environment/failure_slot.rb +28 -0
- 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 +743 -93
- data/lib/rigor/environment.rb +125 -25
- 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 +61 -3
- data/lib/rigor/inference/hkt_sugar_translator.rb +87 -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/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/plugin/registry.rb +25 -2
- 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 +8 -7
- data/lib/rigor/rbs_extended/hkt_directives.rb +16 -1
- data/lib/rigor/rbs_extended/reporter.rb +93 -13
- data/lib/rigor/rbs_extended.rb +6 -1
- 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 +320 -85
- data/lib/rigor/sig_gen/renderer.rb +14 -12
- 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 +9 -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
- metadata +71 -3
|
@@ -63,19 +63,31 @@ syntax-highlighted for a tty — through
|
|
|
63
63
|
colorizer; `--no-color` (and the `NO_COLOR` environment
|
|
64
64
|
variable) disable the colour.
|
|
65
65
|
|
|
66
|
-
## `rigor type-of` —
|
|
66
|
+
## `rigor type-of` — exact positions or a whole line
|
|
67
67
|
|
|
68
|
-
When you
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
When you need a few expression types — typically while chasing
|
|
69
|
+
down why a diagnostic did or did not fire — query the exact
|
|
70
|
+
positions together so Rigor loads the project and each source
|
|
71
|
+
file once:
|
|
71
72
|
|
|
72
73
|
```sh
|
|
73
|
-
rigor type-of lib/example.rb:12:8
|
|
74
|
+
rigor type-of lib/example.rb:12:8 lib/example.rb:12:14
|
|
74
75
|
```
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
Leave off the column to avoid counting it by hand. Rigor prints
|
|
78
|
+
a table of the first 40 expressions starting on the line,
|
|
79
|
+
outermost first at each 1-based column, and marks a truncated
|
|
80
|
+
table:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
rigor type-of lib/example.rb:12
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`--format=json` emits a machine-readable result for tooling: one
|
|
87
|
+
result stays a flat object, while several results use a `results`
|
|
88
|
+
array. Line queries add `line_enumerations` metadata with the
|
|
89
|
+
shown and total expression counts. An exact position is the same
|
|
90
|
+
query the editor integration answers on hover.
|
|
79
91
|
|
|
80
92
|
## `rigor trace` — watch the inference happen
|
|
81
93
|
|
data/docs/manual/07-plugins.md
CHANGED
|
@@ -49,6 +49,10 @@ each plugin's options — but the families today are:
|
|
|
49
49
|
`rigor-factorybot`.
|
|
50
50
|
- **dry-rb** — `rigor-dry-types`, `rigor-dry-schema`,
|
|
51
51
|
`rigor-dry-struct`, `rigor-dry-validation`.
|
|
52
|
+
- **FFI** — `rigor-ffi` and sub-plugins (`rigor-ffi-rzmq`,
|
|
53
|
+
`rigor-rbnacl`, `rigor-sassc`, `rigor-ethon`). For plugin authors,
|
|
54
|
+
`rigor-ffi` provides the `ffi_binding_recognizer` class DSL on
|
|
55
|
+
`Rigor::Plugin::Base` to recognize custom binding definitions.
|
|
52
56
|
- **Other ecosystems** — `rigor-sinatra`, `rigor-hanami`,
|
|
53
57
|
`rigor-devise`, `rigor-pundit`, `rigor-sidekiq`,
|
|
54
58
|
`rigor-graphql`, `rigor-statesman`, `rigor-sorbet`,
|
|
@@ -56,6 +56,43 @@ plugins:
|
|
|
56
56
|
view_search_paths: ["app/views"] # default
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
## What it types
|
|
60
|
+
|
|
61
|
+
Inside a controller, `params`, `request`, `session`, `flash` and
|
|
62
|
+
`cookies` type as their Action Pack classes, and so do the chains
|
|
63
|
+
built on them:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
request.post? # bool — and so do get? / put? / patch? / delete? /
|
|
67
|
+
# head? / options? / trace? / link? / unlink? /
|
|
68
|
+
# xhr? / xml_http_request? / ssl? / local? / form_data?
|
|
69
|
+
flash.now # ActionDispatch::Flash::FlashNow
|
|
70
|
+
flash.keep # ActionDispatch::Flash::FlashHash
|
|
71
|
+
flash[:notice] = "hi" # "hi" — an assignment is its right-hand side
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Rigor ships **no signature** for these Action Pack classes, on
|
|
75
|
+
purpose: the receiver becomes concrete (so `rigor coverage
|
|
76
|
+
--protection` counts the site) while the method surface stays
|
|
77
|
+
lenient, so `request.headers`, `flash.now[:alert] = x` and anything
|
|
78
|
+
else the framework adds resolve without a diagnostic. A partial
|
|
79
|
+
signature would be worse than none — every member it omitted would
|
|
80
|
+
become a false `call.undefined-method`.
|
|
81
|
+
|
|
82
|
+
The predicates are typed `bool` — the union of `true` and `false` —
|
|
83
|
+
which is both the real contract (every one of them is an `==`,
|
|
84
|
+
`match?` or `include?` in Rails or Rack) and the reason they are safe
|
|
85
|
+
to type at all: a condition that folds needs to prove *one* constant,
|
|
86
|
+
and a union of both never does. `return unless request.post?` and
|
|
87
|
+
`mode = request.get? ? :a : :b` read exactly as they did before.
|
|
88
|
+
|
|
89
|
+
`request.format` is **not** typed. That inertness argument is
|
|
90
|
+
narrower than it looks — it holds for a union of the two boolean
|
|
91
|
+
constants, not for a union of ordinary classes, which is nil-free and
|
|
92
|
+
so *can* fold a condition — and `Mime::NullType`, the value `format`
|
|
93
|
+
returns when there is no format, answers `nil?` with `true` while
|
|
94
|
+
being a real object. Typing it needs a nil-aware answer.
|
|
95
|
+
|
|
59
96
|
## Limitations
|
|
60
97
|
|
|
61
98
|
- **Implicit-self helpers only.** `*_path` / `*_url` calls with an
|
|
@@ -69,6 +106,30 @@ plugins:
|
|
|
69
106
|
knows what `rigor-rails-routes` published, and `permit`
|
|
70
107
|
validation only what `rigor-activerecord` published — enabling
|
|
71
108
|
those producers widens what this plugin can check.
|
|
109
|
+
- **`params[:key]` stays untyped.** Inside a controller, `params`
|
|
110
|
+
types as `ActionController::Parameters`, and so does the result
|
|
111
|
+
of every builder method that always returns one — `require`,
|
|
112
|
+
`permit`, `permit!`, `expect`, `slice`, `slice!`, `except`,
|
|
113
|
+
`without`, `extract!`, `merge`, `merge!`, `reverse_merge`,
|
|
114
|
+
`reverse_merge!`, `with_defaults`, `with_defaults!`, `compact`,
|
|
115
|
+
`compact_blank`, `deep_dup` — so a chain built from them keeps a
|
|
116
|
+
concrete receiver throughout. A subscript read is deliberately
|
|
117
|
+
left untyped: `params[:missing]` is `nil` at runtime, and a type
|
|
118
|
+
that says otherwise would let the flow rules fold live
|
|
119
|
+
conditions (`if params[:q]`, `url.nil?`) to a constant and report
|
|
120
|
+
working code. Methods whose result depends on the call — `dig`,
|
|
121
|
+
`fetch`, `compact!`, and the block-less `select` / `reject` /
|
|
122
|
+
`transform_keys` / `transform_values` — are untyped for the same
|
|
123
|
+
reason.
|
|
124
|
+
- **`flash[:key]` and `session[:key]` stay untyped too**, for that
|
|
125
|
+
same reason and measured the same way. Both are leaf reads that
|
|
126
|
+
return whatever was stored — or `nil` for a key that is not set.
|
|
127
|
+
A non-nil type folds `mode = flash[:notice] ? … : …` to one arm and
|
|
128
|
+
reports the live guard after it; a nullable one puts
|
|
129
|
+
`call.possible-nil-receiver` on `note = flash[:notice];
|
|
130
|
+
note.upcase`. Writing through them is unaffected: `flash[:k] = v`
|
|
131
|
+
is `v` because that is what an assignment expression means in Ruby,
|
|
132
|
+
with no rule needed.
|
|
72
133
|
|
|
73
134
|
## Plugin internals
|
|
74
135
|
|
|
@@ -30,7 +30,8 @@ errors_demo.rb:24: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
|
|
|
@@ -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
|