rigortype 0.3.6 → 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 +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/manual/02-cli-reference.md +16 -7
- 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 +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/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 +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 +93 -1
- 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 +147 -10
- 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/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/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 +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.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/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/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/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
- metadata +69 -3
|
@@ -18,21 +18,31 @@ module Rigor
|
|
|
18
18
|
freeze
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# Entries are keyed by the de-rooted constant path (`"User"`, `"Admin::User"` — never `"::User"`;
|
|
22
|
+
# see {AttachmentDiscoverer}), while a QUERY may legitimately arrive rooted: a `Nominal` receiver
|
|
23
|
+
# for `::User` renders its class name as `"::User"`. The root marker is dropped here, once, so no
|
|
24
|
+
# caller needs an `attachments_for(name) || attachments_for("::#{name}")` retry (#621).
|
|
21
25
|
def attachments_for(class_name)
|
|
22
|
-
entries[class_name.to_s]
|
|
26
|
+
entries[class_name.to_s.delete_prefix("::")]
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
def class_names = entries.keys
|
|
26
30
|
|
|
27
31
|
def empty? = entries.empty?
|
|
28
32
|
|
|
33
|
+
# Rows that name the SAME class are UNIONed, not replaced. A model is routinely declared more than
|
|
34
|
+
# once — `app/models/user.rb` holds the real class and a concern or an engine's file reopens it —
|
|
35
|
+
# and a reopen ADDS attachments rather than replacing the class, so keeping only the last row in the
|
|
36
|
+
# glob dropped the earlier declaration's `has_one_attached` and left `user.avatar` untyped. A
|
|
37
|
+
# redeclared attachment NAME resolves to the later row, as it does at load time.
|
|
29
38
|
def self.build(rows:)
|
|
30
39
|
entries = rows.each_with_object({}) do |row, acc|
|
|
31
40
|
class_name = row.fetch(:class_name)
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
by_name = acc[class_name] || {}
|
|
42
|
+
Array(row[:attachments]).each { |attachment| by_name[attachment[:name]] = attachment.freeze }
|
|
43
|
+
acc[class_name] = by_name
|
|
34
44
|
end
|
|
35
|
-
new(entries.freeze)
|
|
45
|
+
new(entries.transform_values { |by_name| by_name.values.freeze }.freeze)
|
|
36
46
|
end
|
|
37
47
|
end
|
|
38
48
|
end
|
|
@@ -37,7 +37,10 @@ module Rigor
|
|
|
37
37
|
class Activestorage < Rigor::Plugin::Base
|
|
38
38
|
manifest(
|
|
39
39
|
id: "activestorage",
|
|
40
|
-
|
|
40
|
+
# Bumped 2026-09-02 (#621) — attachment owners are keyed by their de-rooted name and a reopened
|
|
41
|
+
# class's attachments are UNIONed rather than clobbered, so a cached 0.1.0 index can be missing the
|
|
42
|
+
# attachments the merge restores.
|
|
43
|
+
version: "0.2.0",
|
|
41
44
|
description: "Types ActiveStorage attachment macros (has_one_attached / has_many_attached) on AR models.",
|
|
42
45
|
config_schema: {
|
|
43
46
|
"model_search_paths" => { kind: :array, default: ["app/models"] }
|
|
@@ -91,8 +94,8 @@ module Rigor
|
|
|
91
94
|
receiver_type = scope.type_of(call_node.receiver)
|
|
92
95
|
next nil unless receiver_type.is_a?(Rigor::Type::Nominal)
|
|
93
96
|
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
# `AttachmentIndex#attachments_for` de-roots the query itself (#621) — no retry arm here.
|
|
98
|
+
attachments = index.attachments_for(receiver_type.class_name)
|
|
96
99
|
next nil if attachments.nil?
|
|
97
100
|
|
|
98
101
|
attachment = attachments.find { |a| a[:name] == call_node.name.to_s }
|
data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb
CHANGED
|
@@ -53,7 +53,7 @@ module Rigor
|
|
|
53
53
|
module_function
|
|
54
54
|
|
|
55
55
|
def attributions
|
|
56
|
-
clock_rows + zone_rows + current_rows + notification_rows +
|
|
56
|
+
clock_rows + zone_rows + current_rows + notification_rows + in_time_zone_rows
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def clock_rows
|
|
@@ -90,19 +90,36 @@ module Rigor
|
|
|
90
90
|
row(TIME, :zone=, ["global.write"], singleton: true,
|
|
91
91
|
why: "rewrites the process/fiber's time zone for everything downstream"),
|
|
92
92
|
row(TIME, :use_zone, ["global.write"], singleton: true,
|
|
93
|
-
why: "swaps the zone around a block; the block's own origins join by containment")
|
|
93
|
+
why: "swaps the zone around a block; the block's own origins join by containment"),
|
|
94
|
+
# Issue #658 declares these three in the RBS bundle, so they get their attributions here for
|
|
95
|
+
# the same reason `DateTime#in_time_zone` did: the impurity is invisible at the signature.
|
|
96
|
+
row(TIME, :zone_default, ZONE_READ, singleton: true,
|
|
97
|
+
why: "the `attr_accessor` behind `Time.zone`'s fallback — the same mutable process state"),
|
|
98
|
+
row(TIME, :zone_default=, ["global.write"], singleton: true,
|
|
99
|
+
why: "rewrites the fallback zone for every fiber that has not set its own"),
|
|
100
|
+
row(TIME, :days_in_month, CLOCK, singleton: true,
|
|
101
|
+
why: "`year` defaults to `current.year`, so an omitted second argument reads the clock"),
|
|
102
|
+
row(TIME, :days_in_year, CLOCK, singleton: true, why: "the same default, through `days_in_month`")
|
|
94
103
|
]
|
|
95
104
|
end
|
|
96
105
|
|
|
97
|
-
#
|
|
106
|
+
# `#in_time_zone` — a gap the #388 `%a{pure}` sweep over `sig/active_support/core_ext.rbs`
|
|
98
107
|
# surfaced: `in_time_zone(zone = ::Time.zone)` reads `Time.zone` through its own default argument
|
|
99
108
|
# whenever the caller doesn't pass one explicitly, so it cannot be annotated `%a{pure}` there. It
|
|
100
109
|
# belongs here rather than in the RBS because the label is `global.read` alone (not the
|
|
101
110
|
# `nondet.time` + `global.read` pair `CLOCK` carries) — `in_time_zone` converts an already-fixed
|
|
102
111
|
# instant into a zone, it does not read the clock itself.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
#
|
|
113
|
+
# `DateAndTime::Zones` is included into `Time` as well as `Date` and `DateTime`, so all three take
|
|
114
|
+
# the identical row: #658 declared the `Time` spelling in the RBS bundle and #670 the `Date` one,
|
|
115
|
+
# which is what earns `Date` its row here — the label is a fact about the module, not about the
|
|
116
|
+
# receiver, and the three only ever differed in which spellings the bundle had got round to
|
|
117
|
+
# declaring.
|
|
118
|
+
def in_time_zone_rows
|
|
119
|
+
[TIME, DATE, DATETIME].map do |receiver|
|
|
120
|
+
row(receiver, :in_time_zone, ZONE_READ,
|
|
121
|
+
why: "the default argument reads `Time.zone` when the caller doesn't name one explicitly")
|
|
122
|
+
end
|
|
106
123
|
end
|
|
107
124
|
|
|
108
125
|
def current_rows
|
|
@@ -6,11 +6,31 @@ require_relative "activesupport_core_ext/effects"
|
|
|
6
6
|
|
|
7
7
|
module Rigor
|
|
8
8
|
module Plugin
|
|
9
|
-
# ADR-25 — a pure RBS-bundle plugin. It
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
9
|
+
# ADR-25 — very nearly a pure RBS-bundle plugin. It emits no diagnostic and declares no producer; its
|
|
10
|
+
# main contribution is the manifest's `signature_paths: ["sig"]`, which declares the bundled
|
|
11
|
+
# ActiveSupport `core_ext` RBS directory. `Plugin::Loader` resolves that directory against this gem's
|
|
12
|
+
# root and `Environment.for_project` merges it into the RBS environment, so the ActiveSupport
|
|
13
|
+
# core-extension selectors (`3.days`, `"x".squish`, `Time.current`, …) resolve.
|
|
14
|
+
#
|
|
15
|
+
# The one exception is the {DURATION_MULTIPLIERS} pair of `dynamic_return` rules, which give `1.day` /
|
|
16
|
+
# `5.minutes` an `ActiveSupport::Duration` nominal and keep the arithmetic around it honest. They
|
|
17
|
+
# cannot live in the RBS bundle; see the comment on that constant for why.
|
|
18
|
+
#
|
|
19
|
+
# Issue #632 — `sig/active_support/core_ext.rbs` now ALSO declares `ActiveSupport::Duration` itself
|
|
20
|
+
# (the reader surface: `ago`/`until`/`since`/`from_now`/`before`/`after`, `to_i`/`in_seconds`, `to_f`,
|
|
21
|
+
# `in_minutes`/`in_hours`/`in_days`/`in_weeks`/`in_months`/`in_years`, `iso8601`, `parts`), which is
|
|
22
|
+
# what lets `30.minutes.ago.iso8601` / `1.day.to_i * 2` resolve past the first reader. That is new: the
|
|
23
|
+
# multiplier fix above went out of its way NOT to name the class (naming it makes it RBS-known, and a
|
|
24
|
+
# known class with an incomplete signature turns every member the declaration omits into a false
|
|
25
|
+
# `call.undefined-method` — Duration forwards everything else to the wrapped numeric via
|
|
26
|
+
# `method_missing`). Declaring it now is safe because `ActiveSupport::Duration` is protected TWICE:
|
|
27
|
+
# this manifest's own `open_receivers:` (ADR-26, below — the mechanism `rigor-activerecord` uses for
|
|
28
|
+
# `ActiveRecord::Relation`) covers the plugin-loaded path, and
|
|
29
|
+
# `Rigor::Analysis::CheckRules::GEM_OVERLAY_OPEN_RECEIVERS` covers the auto-applied
|
|
30
|
+
# `data/gem_overlay/activesupport/core_ext.rbs` twin (issue #449's overlay-has-no-manifest gap — see that
|
|
31
|
+
# constant's comment). Either alone would keep `call.undefined-method` from ever firing against a
|
|
32
|
+
# Duration receiver; both exist because a plugin manifest cannot protect a receiver an unrelated,
|
|
33
|
+
# manifest-less RBS file also declares.
|
|
14
34
|
#
|
|
15
35
|
# Activate it like any plugin — no path, no vendoring:
|
|
16
36
|
#
|
|
@@ -22,11 +42,20 @@ module Rigor
|
|
|
22
42
|
class ActivesupportCoreExt < Rigor::Plugin::Base
|
|
23
43
|
manifest(
|
|
24
44
|
id: "activesupport-core-ext",
|
|
25
|
-
# Bumped 2026-
|
|
26
|
-
#
|
|
27
|
-
version: "0.
|
|
28
|
-
description: "RBS bundle for the most-frequently-flagged ActiveSupport core_ext extensions
|
|
45
|
+
# Bumped 2026-09-02 (#632) — `ActiveSupport::Duration`'s reader surface (`ago`/`to_i`/`iso8601`/…)
|
|
46
|
+
# is now declared, open_receivers-protected; see the class comment above.
|
|
47
|
+
version: "0.4.0",
|
|
48
|
+
description: "RBS bundle for the most-frequently-flagged ActiveSupport core_ext extensions, " \
|
|
49
|
+
"plus the `ActiveSupport::Duration` type of the numeric time multipliers and its " \
|
|
50
|
+
"reader surface.",
|
|
29
51
|
signature_paths: ["sig"],
|
|
52
|
+
# ADR-26 — `ActiveSupport::Duration` forwards any method its own class doesn't define to the
|
|
53
|
+
# wrapped numeric via `method_missing` (audited against ActiveSupport 8.1.3.1's
|
|
54
|
+
# `lib/active_support/duration.rb`), so `sig/`'s necessarily-partial declaration of it must not let
|
|
55
|
+
# `call.undefined-method` fire against it. Distinct from the RECEIVER-side FP the multiplier
|
|
56
|
+
# `dynamic_return` gate guards (`Time#day` vs `Duration#day`, in the class comment above) — this is
|
|
57
|
+
# the class's OWN unenumerable member set.
|
|
58
|
+
open_receivers: ["ActiveSupport::Duration"],
|
|
30
59
|
# ADR-103 WD10 (#387) — the IMPURE half of ActiveSupport: the clock, the notification bus and
|
|
31
60
|
# `CurrentAttributes`. The `%a{pure}` sweep over the predicate surface is issue #388 and lands in
|
|
32
61
|
# `sig/`, not here. See {Effects}.
|
|
@@ -34,6 +63,232 @@ module Rigor
|
|
|
34
63
|
effect_labels: %w[rails.current.read rails.current.write],
|
|
35
64
|
effect_attributions: Effects.attributions
|
|
36
65
|
)
|
|
66
|
+
|
|
67
|
+
# Issue #534 item 3 — `core_ext/numeric/time`'s Duration multipliers. The 2026-09-01 corpus sweep
|
|
68
|
+
# measured ~265 mastodon sites on these, every one of them `Dynamic[top]` with cause
|
|
69
|
+
# `explicit_untyped`: the RBS bundle declares them `() -> untyped` so that `1.day` merely *resolves*,
|
|
70
|
+
# and the value it produces then carries nothing.
|
|
71
|
+
#
|
|
72
|
+
# ## Why this is a `dynamic_return` and not an RBS return type
|
|
73
|
+
#
|
|
74
|
+
# The obvious fix — changing `core_ext.rbs`'s `def day: () -> untyped` to
|
|
75
|
+
# `() -> ActiveSupport::Duration` — was not available when this rule was written (#534): RBS could
|
|
76
|
+
# not name a class the bundle did not declare without making it RBS-KNOWN, and a known class with an
|
|
77
|
+
# incomplete signature was worse than no class at all — `call.undefined-method` would stop declining
|
|
78
|
+
# at `Rigor::Reflection.rbs_class_known?`, so every member the declaration omitted would become a
|
|
79
|
+
# false positive on working code. Issue #632 changed that premise: `sig/active_support/core_ext.rbs`
|
|
80
|
+
# now declares `ActiveSupport::Duration`'s reader surface, protected by `open_receivers:` above
|
|
81
|
+
# rather than by staying unnamed, so the fix IS available now — #660 tracks moving the multiplier
|
|
82
|
+
# return itself into RBS to match. This rule stays today's answer because that move hasn't happened
|
|
83
|
+
# yet, not because RBS still cannot express it. `Duration`'s real surface is `method_missing`-forwarded
|
|
84
|
+
# to the wrapped numeric plus `ago` / `since` / `from_now` / `until` / `before` / `after` / `in_*` /
|
|
85
|
+
# `iso8601` and every `Numeric` operator, so "omits a member" is guaranteed. The contribution tier sits ABOVE
|
|
86
|
+
# `RbsDispatch` (`MethodDispatcher#resolve`), so declaring the multiplier in RBS and typing it here
|
|
87
|
+
# is not a contradiction: the RBS declaration is what makes `1.day` resolve at all, and this supplies
|
|
88
|
+
# the answer the declaration deliberately withholds.
|
|
89
|
+
#
|
|
90
|
+
# ## The receiver gate is the FP boundary
|
|
91
|
+
#
|
|
92
|
+
# `day`, `month`, `year`, `hour`, `minute`, `second` and `week` are all real methods on `Time` and
|
|
93
|
+
# `Date` that return an `Integer`, so a name-only rule would silently retype `created_at.day` from
|
|
94
|
+
# `Integer` to a Duration — a wrong precise type on a hot core method. The gate therefore admits only
|
|
95
|
+
# a receiver Rigor has actually proven numeric, including the folded literal carriers (`Constant[1]`,
|
|
96
|
+
# `IntegerRange`) which is what `1.day` is at the call site, and declines on everything else,
|
|
97
|
+
# `Dynamic` included. Declining on `Dynamic` costs a few sites on untyped receivers and is what keeps
|
|
98
|
+
# a `record.days` on some project's own object untouched.
|
|
99
|
+
DURATION_MULTIPLIERS = %i[
|
|
100
|
+
second seconds minute minutes hour hours day days week weeks
|
|
101
|
+
fortnight fortnights month months year years
|
|
102
|
+
].freeze
|
|
103
|
+
|
|
104
|
+
# The nominal every multiplier returns. It is a *lenient* nominal: the site becomes a concrete
|
|
105
|
+
# receiver that `coverage --protection` counts, and `call.undefined-method` never fires against it
|
|
106
|
+
# (ADR-26 `open_receivers:` below) regardless of what its RBS does or doesn't enumerate. Issue #632
|
|
107
|
+
# gave `ActiveSupport::Duration` an actual (partial) RBS declaration — `sig/active_support/
|
|
108
|
+
# core_ext.rbs`'s reader surface, `to_i` / `in_seconds` / `to_f` / the `in_minutes` family / `iso8601`
|
|
109
|
+
# / `parts` — so THOSE now resolve precisely instead of to `Dynamic`. `ago` / `until` / `before` /
|
|
110
|
+
# `since` / `from_now` / `after` stay undeclared on purpose (issue #659, blocked on #658: typing them
|
|
111
|
+
# needs the Rails `Time` instance surface first) and the rest of Duration's real API — the arithmetic
|
|
112
|
+
# operators, `==`, anything `method_missing` forwards to the wrapped numeric — was simply never in
|
|
113
|
+
# scope. Every one of those still resolves lenient-to-`Dynamic` rather than to a diagnostic.
|
|
114
|
+
DURATION_NOMINAL = "ActiveSupport::Duration"
|
|
115
|
+
|
|
116
|
+
# The receiver class names the multipliers are real methods on. `Numeric` is included for a receiver
|
|
117
|
+
# typed at the abstract class (a `Numeric` parameter); `Rational` / `BigDecimal` are not, because the
|
|
118
|
+
# RBS bundle does not declare the multipliers on them, so `2r.days` does not resolve in the first
|
|
119
|
+
# place and typing its result would be a claim about a call Rigor cannot see.
|
|
120
|
+
DURATION_RECEIVER_CLASSES = %w[Integer Float Numeric].freeze
|
|
121
|
+
|
|
122
|
+
dynamic_return methods: DURATION_MULTIPLIERS do |call_node, scope|
|
|
123
|
+
next nil unless call_node.is_a?(Prism::CallNode)
|
|
124
|
+
next nil unless call_node.receiver # `day` with no receiver is somebody's own method
|
|
125
|
+
next nil unless call_node.arguments.nil?
|
|
126
|
+
next nil unless call_node.block.nil?
|
|
127
|
+
next nil unless numeric_receiver?(scope&.type_of(call_node.receiver))
|
|
128
|
+
|
|
129
|
+
Rigor::Type::Combinator.nominal_of(DURATION_NOMINAL)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# ## Duration arithmetic — a correction, not a feature
|
|
133
|
+
#
|
|
134
|
+
# Typing the multiplier alone REGRESSES three shapes, measured on a fixture before this rule existed.
|
|
135
|
+
# A Duration is an ordinary argument to the core operators, and the overload selector has to choose
|
|
136
|
+
# among `Time#-`'s `(Time) -> Float` / `(Numeric) -> Time` with an argument whose class it has no RBS
|
|
137
|
+
# for. Given `Dynamic[top]` — the pre-#534 answer — it widened to the union of the returns and stayed
|
|
138
|
+
# lenient; given a *named* class it commits, and it commits wrong:
|
|
139
|
+
#
|
|
140
|
+
# Time.now - 30.minutes before: Dynamic[Float | Time] after (no rule): Float
|
|
141
|
+
# 2 * 1.day before: Dynamic[BigDecimal | …] after (no rule): Integer
|
|
142
|
+
# 3 + 1.day before: Dynamic[BigDecimal | …] after (no rule): Integer
|
|
143
|
+
# Date.today - 1.week before: Dynamic[Date | Rational] after (no rule): Rational
|
|
144
|
+
#
|
|
145
|
+
# Each of those is a wrong precise type, and each turns the next line into a false positive:
|
|
146
|
+
# `(Time.current - 30.minutes).beginning_of_day` became `undefined method 'beginning_of_day' for
|
|
147
|
+
# Float`, `(2 * 1.day).ago` became one for Integer, `(Date.today - 1.week).year` one for Rational
|
|
148
|
+
# (5/5 measured). `Time.current - 1.day` is ordinary Rails, so shipping the multipliers without this
|
|
149
|
+
# rule would trade ~265 typed sites for diagnostics on working code — the trade AGENTS.md forbids.
|
|
150
|
+
# The rule restores the runtime answer at each of those sites:
|
|
151
|
+
#
|
|
152
|
+
# - `Time`/`DateTime` ± Duration → the receiver's own class. Rails' `plus_with_duration` /
|
|
153
|
+
# `minus_with_duration` return `other.since(self)` / `other.until(self)`, i.e. the receiver kind.
|
|
154
|
+
# - `Date` ± Duration → `Date | Time`, and the union is the honest answer rather than a hedge:
|
|
155
|
+
# `Duration#since` returns a Date for a date-part duration and a Time for a sub-day one, so
|
|
156
|
+
# `Date.today + 1.day` really is a Date and `Date.today + 1.hour` really is a Time. Rigor's union
|
|
157
|
+
# receiver declines a method that only one arm has (measured: `.hour` on `Date | Time` is silent),
|
|
158
|
+
# so the union costs nothing in false positives while keeping both arms named.
|
|
159
|
+
# - Numeric ± Duration, and Numeric * Duration → Duration. `Duration#coerce` wraps the numeric in a
|
|
160
|
+
# `Duration::Scalar`, so `2 * 1.day` is a two-day Duration, not an Integer.
|
|
161
|
+
# - Duration ± Duration and Duration * Numeric → Duration. Not a correction (the RBS-less receiver
|
|
162
|
+
# was already lenient) but the same contract, and it is what carries a `1.day + 1.hour` chain.
|
|
163
|
+
#
|
|
164
|
+
# `/` is deliberately absent: `1.day / 2` is a Duration but `1.day / 1.hour` is a plain `24`, and the
|
|
165
|
+
# answer depends on the operand kind in a way this table would have to guess at. So is
|
|
166
|
+
# `Duration * Duration` — not a quantity Rails promises anything about. So is a Duration RECEIVER
|
|
167
|
+
# with a `Time` / `DateTime` / `Date` argument (#588): `30.minutes + Time.now` raises, under every
|
|
168
|
+
# operator — the table types values, and a crashing expression has none.
|
|
169
|
+
|
|
170
|
+
# Receiver class → what `receiver <op> duration` is. `Time` / `DateTime` keep their own kind; `Date`
|
|
171
|
+
# widens to the two kinds `Duration#since` can produce; a numeric or another Duration is a Duration.
|
|
172
|
+
DURATION_ARITHMETIC_SELF_KINDS = %w[Time DateTime].freeze
|
|
173
|
+
DURATION_ARITHMETIC_NUMERIC_KINDS = %w[Integer Float Numeric].freeze
|
|
174
|
+
DURATION_ARITHMETIC_DATE_KINDS = %w[Date Time].freeze
|
|
175
|
+
|
|
176
|
+
# The operand kinds whose arithmetic with a Duration is itself a Duration — on BOTH sides of the
|
|
177
|
+
# operator. See {#duration_valued_pair?} for why the argument side is checked too.
|
|
178
|
+
DURATION_VALUED_OPERAND_KINDS = %i[duration numeric].freeze
|
|
179
|
+
|
|
180
|
+
dynamic_return methods: %i[+ - *] do |call_node, scope|
|
|
181
|
+
next nil unless call_node.is_a?(Prism::CallNode)
|
|
182
|
+
|
|
183
|
+
argument = duration_arithmetic_argument(call_node)
|
|
184
|
+
next nil if argument.nil?
|
|
185
|
+
|
|
186
|
+
# The ARGUMENT is checked before the receiver: it is the cheaper of the two on the overwhelmingly
|
|
187
|
+
# common shapes (`i + 1`, `n * 2`), and a call with no Duration operand must cost as little as
|
|
188
|
+
# possible — this rule is consulted on every `+` / `-` / `*` in the project. The early exit is
|
|
189
|
+
# what makes the ordering worth anything: an argument that is no Duration operand at all
|
|
190
|
+
# (`"a" + b`, `list + other`) declines here and never types its receiver on this rule's behalf.
|
|
191
|
+
argument_kind = duration_operand_kind(scope&.type_of(argument))
|
|
192
|
+
next nil if argument_kind.nil?
|
|
193
|
+
|
|
194
|
+
receiver_kind = duration_operand_kind(scope&.type_of(call_node.receiver))
|
|
195
|
+
next nil if receiver_kind.nil?
|
|
196
|
+
|
|
197
|
+
duration_arithmetic_result(call_node.name, receiver_kind, argument_kind)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# ADR-88 WD1 — both tables are static and the RBS bundle ships with the gem; nothing here reads a
|
|
201
|
+
# project file, so a project enabling this plugin stays incremental-capable.
|
|
202
|
+
def incremental_state_fingerprint
|
|
203
|
+
"static-duration-multipliers"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
private
|
|
207
|
+
|
|
208
|
+
# True only for a receiver Rigor has actually proven numeric. `Constant` covers the literal form the
|
|
209
|
+
# idiom is written in (`1.day`); `IntegerRange` covers a refined integer; `Nominal` covers a value
|
|
210
|
+
# typed at the class. Everything else — `Dynamic`, a user nominal, a union — declines.
|
|
211
|
+
def numeric_receiver?(type)
|
|
212
|
+
case type
|
|
213
|
+
when Rigor::Type::Constant then type.value.is_a?(Integer) || type.value.is_a?(Float)
|
|
214
|
+
when Rigor::Type::IntegerRange then true
|
|
215
|
+
when Rigor::Type::Nominal then DURATION_RECEIVER_CLASSES.include?(type.class_name)
|
|
216
|
+
else false
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# The single positional argument of a binary operator call, or nil for every other shape. Purely
|
|
221
|
+
# syntactic, so it runs before any type is computed.
|
|
222
|
+
def duration_arithmetic_argument(call_node)
|
|
223
|
+
arguments = call_node.arguments&.arguments
|
|
224
|
+
return nil unless arguments&.size == 1
|
|
225
|
+
|
|
226
|
+
argument = arguments.first
|
|
227
|
+
return nil if argument.is_a?(Prism::SplatNode) || argument.is_a?(Prism::KeywordHashNode)
|
|
228
|
+
|
|
229
|
+
argument
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# The operand's role in Duration arithmetic — `:duration`, `:numeric`, or its own class name for the
|
|
233
|
+
# date/time kinds — or nil for everything else, which is what declines the rule. Note the exact
|
|
234
|
+
# class-name match on the date/time kinds: a `Time` subclass is not retyped as its parent.
|
|
235
|
+
def duration_operand_kind(type)
|
|
236
|
+
case type
|
|
237
|
+
when Rigor::Type::Constant
|
|
238
|
+
:numeric if type.value.is_a?(Integer) || type.value.is_a?(Float)
|
|
239
|
+
when Rigor::Type::IntegerRange then :numeric
|
|
240
|
+
when Rigor::Type::Nominal then duration_operand_kind_for_class(type.class_name)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def duration_operand_kind_for_class(class_name)
|
|
245
|
+
return :duration if class_name == DURATION_NOMINAL
|
|
246
|
+
return :numeric if DURATION_ARITHMETIC_NUMERIC_KINDS.include?(class_name)
|
|
247
|
+
return :date if class_name == "Date"
|
|
248
|
+
|
|
249
|
+
class_name if DURATION_ARITHMETIC_SELF_KINDS.include?(class_name)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# The runtime answer for `receiver <op> argument`, or nil when the pair is not one this rule speaks
|
|
253
|
+
# for. At least one operand must be a Duration — otherwise the call is ordinary arithmetic and keeps
|
|
254
|
+
# whatever answer it always had (`1 + 1`, `Time - Time`, `"a" + "b"`).
|
|
255
|
+
def duration_arithmetic_result(operator, receiver_kind, argument_kind)
|
|
256
|
+
return nil unless receiver_kind == :duration || argument_kind == :duration
|
|
257
|
+
|
|
258
|
+
if DURATION_ARITHMETIC_SELF_KINDS.include?(receiver_kind)
|
|
259
|
+
# `Time`/`DateTime` ± Duration → the receiver's own kind. Multiplication is not defined.
|
|
260
|
+
return Rigor::Type::Combinator.nominal_of(receiver_kind) if operator != :*
|
|
261
|
+
elsif receiver_kind == :date
|
|
262
|
+
# `Date` ± Duration → whichever kind the duration's parts make of it.
|
|
263
|
+
return date_arithmetic_union if operator != :*
|
|
264
|
+
elsif duration_valued_pair?(operator, receiver_kind, argument_kind)
|
|
265
|
+
# Duration ± Duration, Duration ± numeric, numeric ± Duration, and the `*` forms of each: a
|
|
266
|
+
# Duration, via `Duration#coerce`'s `Duration::Scalar` wrapper.
|
|
267
|
+
return Rigor::Type::Combinator.nominal_of(DURATION_NOMINAL)
|
|
268
|
+
end
|
|
269
|
+
nil
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# The pairs whose value is a Duration: a Duration or numeric on BOTH sides, minus `Duration *
|
|
273
|
+
# Duration` (not a quantity Rails promises). The argument side of the check is #588 — a Duration
|
|
274
|
+
# receiver with a `Time` / `DateTime` / `Date` argument is an expression that RAISES:
|
|
275
|
+
# `30.minutes + Time.now` adds the Time to the seconds part and `Integer#+` cannot coerce it
|
|
276
|
+
# (TypeError), `30.minutes - Time.now` sends `-@` to the Time (NoMethodError), and `*` is a
|
|
277
|
+
# TypeError from `Duration#calculate` — 7/7 shapes measured on ActiveSupport 8.1. Claiming
|
|
278
|
+
# `Duration` there would type crashing code; declining leaves the RBS-less receiver lenient, which
|
|
279
|
+
# is the honest answer for an expression that has no value.
|
|
280
|
+
def duration_valued_pair?(operator, receiver_kind, argument_kind)
|
|
281
|
+
return false unless DURATION_VALUED_OPERAND_KINDS.include?(receiver_kind)
|
|
282
|
+
return false unless DURATION_VALUED_OPERAND_KINDS.include?(argument_kind)
|
|
283
|
+
|
|
284
|
+
!(operator == :* && receiver_kind == :duration && argument_kind == :duration)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def date_arithmetic_union
|
|
288
|
+
Rigor::Type::Combinator.union(
|
|
289
|
+
*DURATION_ARITHMETIC_DATE_KINDS.map { |name| Rigor::Type::Combinator.nominal_of(name) }
|
|
290
|
+
)
|
|
291
|
+
end
|
|
37
292
|
end
|
|
38
293
|
|
|
39
294
|
Rigor::Plugin.register(ActivesupportCoreExt)
|