rigortype 0.3.3 → 0.3.5
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/array.yml +11 -24
- data/data/builtins/ruby_core/enumerable.yml +4 -8
- data/data/builtins/ruby_core/exception.yml +6 -9
- data/data/builtins/ruby_core/hash.yml +13 -20
- data/data/builtins/ruby_core/io.yml +6 -9
- data/data/builtins/ruby_core/numeric.yml +2 -3
- data/data/builtins/ruby_core/proc.yml +4 -8
- data/data/builtins/ruby_core/re.yml +6 -9
- data/data/builtins/ruby_core/set.yml +2 -3
- data/data/builtins/ruby_core/string.yml +4 -7
- data/data/builtins/ruby_core/struct.yml +0 -6
- data/data/builtins/ruby_core/time.yml +3 -6
- data/data/effects/core.yml +860 -0
- data/data/effects/registry.yml +95 -0
- data/docs/handbook/07-rbs-and-extended.md +106 -5
- data/docs/manual/02-cli-reference.md +359 -2
- data/docs/manual/03-configuration.md +101 -1
- data/docs/manual/04-diagnostics.md +8 -3
- data/docs/manual/11-ci.md +37 -0
- data/docs/manual/12-caching.md +39 -0
- data/docs/manual/16-rbs-extended-annotations.md +90 -0
- data/docs/manual/18-removing-dead-code.md +326 -0
- data/docs/manual/19-effect-labels.md +671 -0
- data/docs/manual/README.md +10 -0
- data/docs/manual/ci-templates/README.md +9 -0
- data/docs/manual/plugins/rigor-actionmailer.md +5 -0
- data/docs/manual/plugins/rigor-activejob.md +47 -0
- data/docs/manual/plugins/rigor-factorybot.md +16 -0
- data/docs/manual/plugins/rigor-pundit.md +26 -0
- data/docs/manual/plugins/rigor-rails-routes.md +41 -2
- data/docs/manual/plugins/rigor-rspec-rails.md +5 -0
- data/docs/manual/plugins/rigor-rspec.md +8 -0
- data/docs/manual/plugins/rigor-sidekiq.md +54 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +28 -2
- data/lib/rigor/analysis/incremental_session.rb +140 -6
- data/lib/rigor/analysis/reachability/graph.rb +244 -0
- data/lib/rigor/analysis/reachability/plugin_roots.rb +164 -0
- data/lib/rigor/analysis/reachability/project_files.rb +58 -0
- data/lib/rigor/analysis/reachability/scan.rb +241 -0
- data/lib/rigor/analysis/reachability/signature_scan.rb +134 -0
- data/lib/rigor/analysis/rule_catalog.rb +168 -3
- data/lib/rigor/analysis/run_cache_key.rb +16 -0
- data/lib/rigor/analysis/run_cache_probe.rb +69 -1
- data/lib/rigor/analysis/runner/declaration_position.rb +38 -0
- data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +108 -0
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +358 -0
- data/lib/rigor/analysis/runner/envelope_messages.rb +61 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +93 -10
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -4
- data/lib/rigor/analysis/runner.rb +429 -10
- data/lib/rigor/analysis/worker_session.rb +60 -2
- data/lib/rigor/bleeding_edge.rb +22 -0
- data/lib/rigor/cache/incremental_snapshot.rb +24 -5
- data/lib/rigor/cli/check_command.rb +13 -1
- data/lib/rigor/cli/check_runner_factory.rb +4 -1
- data/lib/rigor/cli/effects_command.rb +245 -0
- data/lib/rigor/cli/effects_diff_renderer.rb +127 -0
- data/lib/rigor/cli/effects_explain_renderer.rb +68 -0
- data/lib/rigor/cli/effects_renderer.rb +92 -0
- data/lib/rigor/cli/effects_report.rb +163 -0
- data/lib/rigor/cli/effects_snapshot_command.rb +275 -0
- data/lib/rigor/cli/unused_command.rb +288 -0
- data/lib/rigor/cli.rb +28 -1
- data/lib/rigor/configuration/severity_profile.rb +23 -0
- data/lib/rigor/configuration.rb +322 -28
- data/lib/rigor/configuration_error.rb +20 -0
- data/lib/rigor/effects/attribution.rb +76 -0
- data/lib/rigor/effects/catalog.rb +275 -0
- data/lib/rigor/effects/collector.rb +219 -0
- data/lib/rigor/effects/config_envelopes.rb +185 -0
- data/lib/rigor/effects/discharge.rb +69 -0
- data/lib/rigor/effects/effect_table.rb +92 -0
- data/lib/rigor/effects/entry_points.rb +139 -0
- data/lib/rigor/effects/envelope.rb +86 -0
- data/lib/rigor/effects/envelope_check.rb +172 -0
- data/lib/rigor/effects/envelope_index.rb +157 -0
- data/lib/rigor/effects/file_collection.rb +174 -0
- data/lib/rigor/effects/framework_units.rb +222 -0
- data/lib/rigor/effects/identity.rb +104 -0
- data/lib/rigor/effects/inline_anchor.rb +134 -0
- data/lib/rigor/effects/label.rb +77 -0
- data/lib/rigor/effects/label_intent.rb +73 -0
- data/lib/rigor/effects/label_set.rb +136 -0
- data/lib/rigor/effects/liskov_check.rb +167 -0
- data/lib/rigor/effects/local_ownership.rb +132 -0
- data/lib/rigor/effects/method_key.rb +40 -0
- data/lib/rigor/effects/mutation_classifier.rb +92 -0
- data/lib/rigor/effects/narrowing.rb +202 -0
- data/lib/rigor/effects/origin.rb +66 -0
- data/lib/rigor/effects/path_finder.rb +89 -0
- data/lib/rigor/effects/plugin_facts.rb +384 -0
- data/lib/rigor/effects/propagator.rb +335 -0
- data/lib/rigor/effects/registry.rb +200 -0
- data/lib/rigor/effects/scanner.rb +302 -0
- data/lib/rigor/effects/signature_sources.rb +90 -0
- data/lib/rigor/effects/snapshot.rb +396 -0
- data/lib/rigor/effects/snapshot_diff.rb +265 -0
- data/lib/rigor/effects/summary.rb +154 -0
- data/lib/rigor/effects/taint_cause.rb +39 -0
- data/lib/rigor/effects/unit_scan.rb +647 -0
- data/lib/rigor/effects/unknown_label_check.rb +86 -0
- data/lib/rigor/effects/unknown_label_report.rb +59 -0
- data/lib/rigor/effects.rb +47 -0
- data/lib/rigor/environment/rbs_loader.rb +34 -0
- data/lib/rigor/environment.rb +8 -1
- data/lib/rigor/flow_contribution/element.rb +1 -0
- data/lib/rigor/flow_contribution/merge_result.rb +5 -3
- data/lib/rigor/flow_contribution/merger.rb +16 -1
- data/lib/rigor/flow_contribution.rb +20 -4
- data/lib/rigor/inference/expression_typer.rb +18 -4
- data/lib/rigor/inference/pre_eval_constants.rb +189 -0
- data/lib/rigor/inference/scope_indexer.rb +15 -3
- data/lib/rigor/plugin/base.rb +28 -0
- data/lib/rigor/plugin/box.rb +18 -2
- data/lib/rigor/plugin/effect_ancestry.rb +80 -0
- data/lib/rigor/plugin/effect_attribution.rb +208 -0
- data/lib/rigor/plugin/effect_edge.rb +101 -0
- data/lib/rigor/plugin/effect_entry_points.rb +51 -0
- data/lib/rigor/plugin/first_party.rb +57 -0
- data/lib/rigor/plugin/isolation.rb +4 -1
- data/lib/rigor/plugin/manifest.rb +127 -3
- data/lib/rigor/plugin/registry.rb +55 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/envelope_scanner.rb +160 -0
- data/lib/rigor/rbs_extended.rb +188 -0
- data/lib/rigor/reflection.rb +131 -30
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +90 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +7 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/effects.rb +85 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +8 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +152 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +9 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +156 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +113 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +68 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +186 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +12 -1
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +145 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/effects.rb +77 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +143 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -1
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +401 -29
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +27 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -2
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +151 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +46 -3
- data/plugins/rigor-rails/lib/rigor-rails.rb +1 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/effects.rb +47 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +7 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +81 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +13 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +202 -15
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +43 -3
- data/plugins/rigor-railties/lib/rigor/plugin/railties/effects.rb +171 -0
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +62 -0
- data/plugins/rigor-railties/lib/rigor-railties.rb +3 -0
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +56 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +75 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +122 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +68 -3
- data/sig/rigor/analysis/fact_store.rbs +1 -0
- data/sig/rigor/inference.rbs +2 -0
- data/sig/rigor/rbs_extended.rbs +6 -0
- data/sig/rigor.rbs +17 -1
- data/skills/rigor-unused-adjudicate/SKILL.md +90 -0
- metadata +84 -1
|
@@ -18,6 +18,57 @@
|
|
|
18
18
|
# Not exhaustive. ActiveSupport ships many more extensions across
|
|
19
19
|
# Hash / Array / String / Object / Date / Time / Integer / Module
|
|
20
20
|
# than this file covers; PRs welcome to extend.
|
|
21
|
+
#
|
|
22
|
+
# ---------------------------------------------------------------
|
|
23
|
+
# Purity (ADR-103 WD10; docs/type-specification/effect-labels.md; issue #388)
|
|
24
|
+
# ---------------------------------------------------------------
|
|
25
|
+
#
|
|
26
|
+
# `%a{pure}` below is audited one by one against the vendored ActiveSupport
|
|
27
|
+
# source (8.1.3.1), not assumed from the method name. Two recurring
|
|
28
|
+
# judgment calls, decided once here rather than repeated per method:
|
|
29
|
+
#
|
|
30
|
+
# - **Inflection rules are treated as pure.** `camelize` / `underscore` /
|
|
31
|
+
# `pluralize` / `singularize` / `humanize` / `titleize` / `tableize` /
|
|
32
|
+
# `classify` / `dasherize` / `demodulize` / `deconstantize` /
|
|
33
|
+
# `foreign_key` / `upcase_first` all read `ActiveSupport::Inflector`'s
|
|
34
|
+
# per-locale rule table (`inflections(locale)`) — process-global, but
|
|
35
|
+
# configured once at boot and never touched by request-handling code.
|
|
36
|
+
# Rigor's own ADR-90 (the standalone inflector) already treats
|
|
37
|
+
# inflections as pure functions of the rule set; this file inherits
|
|
38
|
+
# that assumption rather than re-litigating it. `parameterize` is the
|
|
39
|
+
# one exception in this family: it calls `I18n.transliterate`, which
|
|
40
|
+
# reads `I18n.locale` when no explicit `locale:` is given — a stronger,
|
|
41
|
+
# request-scoped global read the ADR-90 assumption does not cover, so
|
|
42
|
+
# it is skipped below.
|
|
43
|
+
# - **A method that dispatches to a name on its OWN receiver's declared
|
|
44
|
+
# type is fine; a method that dispatches to a name looked up on an
|
|
45
|
+
# ARGUMENT or ELEMENT of unknown type is not**, because the override is
|
|
46
|
+
# invisible to this audit (the `try` problem). `in?` / `presence_in`
|
|
47
|
+
# are the one deliberate exception: they call `#include?` / `#cover?`
|
|
48
|
+
# on the argument, which is near-universally a pure predicate, and the
|
|
49
|
+
# design note calls them out by name. `to_param` / `to_query` (Hash) and
|
|
50
|
+
# `to_fs(:db)` (Array, which calls `#id` on each element) are the
|
|
51
|
+
# converse case and are left unannotated.
|
|
52
|
+
#
|
|
53
|
+
# The clock, `Time.zone`, `ActiveSupport::Notifications` and
|
|
54
|
+
# `ActiveSupport::CurrentAttributes` are covered by this plugin's
|
|
55
|
+
# `effect_attributions:` manifest (`lib/rigor/plugin/activesupport_core_ext/effects.rb`,
|
|
56
|
+
# issue #387) rather than by an RBS annotation here — see that file for
|
|
57
|
+
# the impure half of this bundle. `DateTime#in_time_zone` was a gap in
|
|
58
|
+
# that sweep (it reads `Time.zone` via its default argument) and is
|
|
59
|
+
# closed there in this same change.
|
|
60
|
+
#
|
|
61
|
+
# Five `Date` singleton methods this file declares — `self.end_of_week`,
|
|
62
|
+
# `self.beginning_of_month`, `self.end_of_month`, `self.beginning_of_year`,
|
|
63
|
+
# `self.end_of_year` — do not exist in ActiveSupport 8.1
|
|
64
|
+
# (`Date.beginning_of_month` etc. are never defined; only the instance
|
|
65
|
+
# methods are). A sixth, `self.beginning_of_week`, DOES exist but with the
|
|
66
|
+
# wrong return type here (real `Date.beginning_of_week` is the week-start
|
|
67
|
+
# config getter, returning a `Symbol`, not a `Date`) — and reads
|
|
68
|
+
# fiber-local execution state either way. None of the six can be soundly
|
|
69
|
+
# annotated `%a{pure}`, so they are left exactly as they were; this
|
|
70
|
+
# paragraph is the flag for whoever fixes the six next (out of scope for
|
|
71
|
+
# the #388 purity sweep).
|
|
21
72
|
|
|
22
73
|
# ---------------------------------------------------------------
|
|
23
74
|
# Object — universal predicates and helpers
|
|
@@ -25,12 +76,15 @@
|
|
|
25
76
|
|
|
26
77
|
class Object
|
|
27
78
|
# @return [bool]
|
|
79
|
+
%a{pure}
|
|
28
80
|
def blank?: () -> bool
|
|
29
81
|
|
|
30
82
|
# @return [bool]
|
|
83
|
+
%a{pure}
|
|
31
84
|
def present?: () -> bool
|
|
32
85
|
|
|
33
86
|
# @return [self?]
|
|
87
|
+
%a{pure}
|
|
34
88
|
def presence: () -> self?
|
|
35
89
|
|
|
36
90
|
# `active_support/core_ext/object/json` monkey-patches
|
|
@@ -46,44 +100,72 @@ class Object
|
|
|
46
100
|
# `options` is the standard `only:` / `except:` / `methods:`
|
|
47
101
|
# / `include:` hash — left as untyped because the surface
|
|
48
102
|
# is large and per-call-site precision is rarely useful.
|
|
103
|
+
#
|
|
104
|
+
# NOT `%a{pure}`: `respond_to?(:to_hash) ? to_hash.as_json(options) :
|
|
105
|
+
# instance_values.as_json(options)` dispatches to `to_hash` on the
|
|
106
|
+
# receiver's own (unknown-at-this-declaration) class, exactly the way
|
|
107
|
+
# `try` dispatches to an argument-named method — a subclass override is
|
|
108
|
+
# invisible to this audit, and `Object` is as broad a receiver as RBS has.
|
|
49
109
|
def as_json: (?untyped options) -> untyped
|
|
50
110
|
|
|
51
111
|
# `try` swallows NoMethodError; declared return is `untyped`
|
|
52
112
|
# because the called method's signature is not known to RBS
|
|
53
113
|
# at this call site.
|
|
114
|
+
#
|
|
115
|
+
# NOT `%a{pure}`: `try` is pure only when the method it dispatches to is,
|
|
116
|
+
# and that is a fact about the *call site*, not about `try` itself.
|
|
54
117
|
def try: (Symbol | String) -> untyped
|
|
55
118
|
| (Symbol | String, *untyped) -> untyped
|
|
56
119
|
| (Symbol | String, *untyped) { (?) -> untyped } -> untyped
|
|
57
120
|
|
|
121
|
+
# NOT `%a{pure}`, same reasoning as `try`.
|
|
58
122
|
def try!: (Symbol | String) -> untyped
|
|
59
123
|
| (Symbol | String, *untyped) -> untyped
|
|
60
124
|
| (Symbol | String, *untyped) { (?) -> untyped } -> untyped
|
|
61
125
|
|
|
62
126
|
# `acts_like?(:string)` / `acts_like?(:date)` / `acts_like?(:time)`
|
|
63
|
-
# is ActiveSupport's "duck-typing helper" predicate
|
|
127
|
+
# is ActiveSupport's "duck-typing helper" predicate: `respond_to?
|
|
128
|
+
# :"acts_like_#{duck}?"`. Deterministic given the receiver's class.
|
|
129
|
+
%a{pure}
|
|
64
130
|
def acts_like?: (Symbol | String) -> bool
|
|
65
131
|
|
|
66
|
-
# `core_ext/object/inclusion` — `x.in?([a, b])` / `x.in?(1..10)`.
|
|
132
|
+
# `core_ext/object/inclusion` — `x.in?([a, b])` / `x.in?(1..10)`. Dispatches
|
|
133
|
+
# to `#include?` / `Range#cover?` on the ARGUMENT, the one deliberate
|
|
134
|
+
# exception to the try-shaped-dispatch rule above (design note § 11.2).
|
|
135
|
+
%a{pure}
|
|
67
136
|
def in?: (untyped) -> bool
|
|
68
137
|
end
|
|
69
138
|
|
|
70
139
|
# `nil.blank?` / `nil.present?` / `nil.try` are the most frequent
|
|
71
140
|
# call sites the survey turned up.
|
|
72
141
|
class NilClass
|
|
142
|
+
%a{pure}
|
|
73
143
|
def blank?: () -> true
|
|
144
|
+
%a{pure}
|
|
74
145
|
def present?: () -> false
|
|
146
|
+
%a{pure}
|
|
75
147
|
def presence: () -> nil
|
|
148
|
+
|
|
149
|
+
# Unlike `Object#try`, `NilClass#try` is a hardcoded override that
|
|
150
|
+
# returns `nil` unconditionally — it never sends the message, so
|
|
151
|
+
# unlike `Object#try` above it never dispatches at all.
|
|
152
|
+
%a{pure}
|
|
76
153
|
def try: (*untyped) -> nil
|
|
154
|
+
%a{pure}
|
|
77
155
|
def try!: (*untyped) -> nil
|
|
78
156
|
end
|
|
79
157
|
|
|
80
158
|
class TrueClass
|
|
159
|
+
%a{pure}
|
|
81
160
|
def blank?: () -> false
|
|
161
|
+
%a{pure}
|
|
82
162
|
def present?: () -> true
|
|
83
163
|
end
|
|
84
164
|
|
|
85
165
|
class FalseClass
|
|
166
|
+
%a{pure}
|
|
86
167
|
def blank?: () -> true
|
|
168
|
+
%a{pure}
|
|
87
169
|
def present?: () -> false
|
|
88
170
|
end
|
|
89
171
|
|
|
@@ -92,73 +174,119 @@ end
|
|
|
92
174
|
# ---------------------------------------------------------------
|
|
93
175
|
|
|
94
176
|
class String
|
|
95
|
-
# `core_ext/string/inflections`
|
|
177
|
+
# `core_ext/string/inflections` — the ADR-90 inflection-rule-table
|
|
178
|
+
# assumption at the top of this file applies to the whole group below.
|
|
179
|
+
%a{pure}
|
|
96
180
|
def underscore: () -> String
|
|
181
|
+
%a{pure}
|
|
97
182
|
def camelize: (?Symbol upper_case_first) -> String
|
|
183
|
+
%a{pure}
|
|
98
184
|
def camelcase: (?Symbol upper_case_first) -> String
|
|
185
|
+
%a{pure}
|
|
99
186
|
def classify: () -> String
|
|
100
|
-
def constantize: () -> untyped
|
|
101
|
-
def safe_constantize: () -> untyped
|
|
187
|
+
def constantize: () -> untyped # NOT %a{pure}: resolves a constant, may trigger autoload.
|
|
188
|
+
def safe_constantize: () -> untyped # NOT %a{pure}: same as constantize.
|
|
189
|
+
%a{pure}
|
|
102
190
|
def demodulize: () -> String
|
|
191
|
+
%a{pure}
|
|
103
192
|
def deconstantize: () -> String
|
|
193
|
+
%a{pure}
|
|
104
194
|
def titleize: () -> String
|
|
195
|
+
%a{pure}
|
|
105
196
|
def titlecase: () -> String
|
|
197
|
+
%a{pure}
|
|
106
198
|
def dasherize: () -> String
|
|
199
|
+
%a{pure}
|
|
107
200
|
def upcase_first: () -> String
|
|
201
|
+
# NOT %a{pure}: transliterates through `I18n.transliterate`, which reads
|
|
202
|
+
# `I18n.locale` when `locale:` is left at its `nil` default — a request-
|
|
203
|
+
# scoped global read the inflection-table assumption does not cover.
|
|
108
204
|
def parameterize: (?separator: String, ?preserve_case: bool, ?locale: Symbol?) -> String
|
|
205
|
+
%a{pure}
|
|
109
206
|
def tableize: () -> String
|
|
207
|
+
%a{pure}
|
|
110
208
|
def foreign_key: (?bool separate_class_name_and_id_with_underscore) -> String
|
|
209
|
+
%a{pure}
|
|
111
210
|
def pluralize: (?Integer count, ?Symbol locale) -> String
|
|
211
|
+
%a{pure}
|
|
112
212
|
def singularize: (?Symbol locale) -> String
|
|
213
|
+
%a{pure}
|
|
113
214
|
def humanize: (?capitalize: bool, ?keep_id_suffix: bool) -> String
|
|
114
215
|
|
|
115
216
|
# `core_ext/string/filters`
|
|
217
|
+
%a{pure}
|
|
116
218
|
def squish: () -> String
|
|
117
|
-
def squish!: () -> String
|
|
219
|
+
def squish!: () -> String # NOT %a{pure}: bang, mutates the receiver in place.
|
|
220
|
+
%a{pure}
|
|
118
221
|
def truncate: (Integer truncate_at, ?omission: String, ?separator: String | Regexp | nil) -> String
|
|
222
|
+
%a{pure}
|
|
119
223
|
def truncate_bytes: (Integer truncate_at, ?omission: String) -> String
|
|
224
|
+
%a{pure}
|
|
120
225
|
def truncate_words: (Integer words_count, ?omission: String, ?separator: String | Regexp | nil) -> String
|
|
121
226
|
# `remove` / `remove!` delete every occurrence of the given patterns.
|
|
227
|
+
%a{pure}
|
|
122
228
|
def remove: (*Regexp | String patterns) -> String
|
|
123
|
-
def remove!: (*Regexp | String patterns) -> String
|
|
229
|
+
def remove!: (*Regexp | String patterns) -> String # NOT %a{pure}: bang, mutates in place.
|
|
124
230
|
|
|
125
231
|
# `core_ext/string/output_safety` — `html_safe` returns an
|
|
126
232
|
# `ActiveSupport::SafeBuffer` (a String subclass). The closest
|
|
127
233
|
# RBS-canonical carrier is plain `String`; precision lost is the
|
|
128
|
-
# `html_safe?` predicate value.
|
|
234
|
+
# `html_safe?` predicate value. Neither mutates the receiver; `html_safe?`
|
|
235
|
+
# on a plain `String` receiver is a hardcoded `false`.
|
|
236
|
+
%a{pure}
|
|
129
237
|
def html_safe: () -> String
|
|
238
|
+
%a{pure}
|
|
130
239
|
def html_safe?: () -> bool
|
|
131
240
|
|
|
132
241
|
# `core_ext/string/indent`
|
|
242
|
+
%a{pure}
|
|
133
243
|
def indent: (Integer amount, ?String indent_string, ?bool indent_empty_lines) -> String
|
|
134
|
-
def indent!: (Integer amount, ?String indent_string, ?bool indent_empty_lines) -> String?
|
|
244
|
+
def indent!: (Integer amount, ?String indent_string, ?bool indent_empty_lines) -> String? # NOT %a{pure}: bang, mutates in place.
|
|
135
245
|
|
|
136
246
|
# `core_ext/string/starts_ends_with` — also covered by core
|
|
137
247
|
# `start_with?` / `end_with?`; these are the ActiveSupport aliases.
|
|
248
|
+
%a{pure}
|
|
138
249
|
def starts_with?: (*String prefixes) -> bool
|
|
250
|
+
%a{pure}
|
|
139
251
|
def ends_with?: (*String suffixes) -> bool
|
|
140
252
|
|
|
141
253
|
# `core_ext/string/conversions` — `to_time` / `to_date` /
|
|
142
|
-
# `to_datetime` return Time / Date / DateTime.
|
|
254
|
+
# `to_datetime` return Time / Date / DateTime. NONE are `%a{pure}`:
|
|
255
|
+
# all three default missing date/time components from `Time.now` /
|
|
256
|
+
# the current date when the string under-specifies them (verified
|
|
257
|
+
# against `activesupport/lib/active_support/core_ext/string/conversions.rb`)
|
|
258
|
+
# — `nondet.time`, not a pure function of the string alone.
|
|
143
259
|
def to_time: (?Symbol form) -> Time?
|
|
144
260
|
def to_date: () -> Date?
|
|
145
261
|
def to_datetime: () -> DateTime
|
|
262
|
+
# `to_hours` does not exist in vendored ActiveSupport 8.1.3.1 — no
|
|
263
|
+
# `String#to_hours` anywhere in `core_ext/string/`. Left unannotated
|
|
264
|
+
# because there is no implementation here to audit; flagged for a
|
|
265
|
+
# follow-up to confirm and either implement or remove the declaration.
|
|
146
266
|
def to_hours: () -> Float
|
|
147
267
|
|
|
148
268
|
# `core_ext/string/access`
|
|
269
|
+
%a{pure}
|
|
149
270
|
def at: (Integer | Range[Integer] | Regexp position) -> String?
|
|
271
|
+
%a{pure}
|
|
150
272
|
def from: (Integer position) -> String
|
|
273
|
+
%a{pure}
|
|
151
274
|
def to: (Integer position) -> String
|
|
275
|
+
%a{pure}
|
|
152
276
|
def first: (?Integer limit) -> String
|
|
277
|
+
%a{pure}
|
|
153
278
|
def last: (?Integer limit) -> String
|
|
154
279
|
|
|
155
280
|
# `core_ext/string/strip` — leading-blank strip with shared margin.
|
|
281
|
+
%a{pure}
|
|
156
282
|
def strip_heredoc: () -> String
|
|
157
283
|
|
|
158
284
|
# `core_ext/string/multibyte`
|
|
285
|
+
%a{pure}
|
|
159
286
|
def mb_chars: () -> untyped
|
|
160
287
|
|
|
161
288
|
# `core_ext/string/inquiry`
|
|
289
|
+
%a{pure}
|
|
162
290
|
def inquiry: () -> untyped # ActiveSupport::StringInquirer
|
|
163
291
|
end
|
|
164
292
|
|
|
@@ -167,45 +295,83 @@ end
|
|
|
167
295
|
# ---------------------------------------------------------------
|
|
168
296
|
|
|
169
297
|
class Integer
|
|
170
|
-
# `core_ext/numeric/time` — Duration multipliers.
|
|
298
|
+
# `core_ext/numeric/time` — Duration multipliers. Each just wraps `self`
|
|
299
|
+
# in an `ActiveSupport::Duration` value object (`Duration.seconds(self)`,
|
|
300
|
+
# …); the clock is read only later, by calling `.ago` / `.since` / etc.
|
|
301
|
+
# on the resulting Duration — attributed on `ActiveSupport::Duration` in
|
|
302
|
+
# `lib/rigor/plugin/activesupport_core_ext/effects.rb`, not here.
|
|
303
|
+
%a{pure}
|
|
171
304
|
def second: () -> untyped # ActiveSupport::Duration
|
|
305
|
+
%a{pure}
|
|
172
306
|
def seconds: () -> untyped
|
|
307
|
+
%a{pure}
|
|
173
308
|
def minute: () -> untyped
|
|
309
|
+
%a{pure}
|
|
174
310
|
def minutes: () -> untyped
|
|
311
|
+
%a{pure}
|
|
175
312
|
def hour: () -> untyped
|
|
313
|
+
%a{pure}
|
|
176
314
|
def hours: () -> untyped
|
|
315
|
+
%a{pure}
|
|
177
316
|
def day: () -> untyped
|
|
317
|
+
%a{pure}
|
|
178
318
|
def days: () -> untyped
|
|
319
|
+
%a{pure}
|
|
179
320
|
def week: () -> untyped
|
|
321
|
+
%a{pure}
|
|
180
322
|
def weeks: () -> untyped
|
|
323
|
+
%a{pure}
|
|
181
324
|
def fortnight: () -> untyped
|
|
325
|
+
%a{pure}
|
|
182
326
|
def fortnights: () -> untyped
|
|
327
|
+
%a{pure}
|
|
183
328
|
def month: () -> untyped
|
|
329
|
+
%a{pure}
|
|
184
330
|
def months: () -> untyped
|
|
331
|
+
%a{pure}
|
|
185
332
|
def year: () -> untyped
|
|
333
|
+
%a{pure}
|
|
186
334
|
def years: () -> untyped
|
|
187
335
|
|
|
188
|
-
# `core_ext/numeric/bytes`
|
|
336
|
+
# `core_ext/numeric/bytes` — plain arithmetic on `self`.
|
|
337
|
+
%a{pure}
|
|
189
338
|
def byte: () -> Integer
|
|
339
|
+
%a{pure}
|
|
190
340
|
def bytes: () -> Integer
|
|
341
|
+
%a{pure}
|
|
191
342
|
def kilobyte: () -> Integer
|
|
343
|
+
%a{pure}
|
|
192
344
|
def kilobytes: () -> Integer
|
|
345
|
+
%a{pure}
|
|
193
346
|
def megabyte: () -> Integer
|
|
347
|
+
%a{pure}
|
|
194
348
|
def megabytes: () -> Integer
|
|
349
|
+
%a{pure}
|
|
195
350
|
def gigabyte: () -> Integer
|
|
351
|
+
%a{pure}
|
|
196
352
|
def gigabytes: () -> Integer
|
|
353
|
+
%a{pure}
|
|
197
354
|
def terabyte: () -> Integer
|
|
355
|
+
%a{pure}
|
|
198
356
|
def terabytes: () -> Integer
|
|
357
|
+
%a{pure}
|
|
199
358
|
def petabyte: () -> Integer
|
|
359
|
+
%a{pure}
|
|
200
360
|
def petabytes: () -> Integer
|
|
361
|
+
%a{pure}
|
|
201
362
|
def exabyte: () -> Integer
|
|
363
|
+
%a{pure}
|
|
202
364
|
def exabytes: () -> Integer
|
|
203
365
|
|
|
204
366
|
# `core_ext/integer/multiple`
|
|
367
|
+
%a{pure}
|
|
205
368
|
def multiple_of?: (Integer) -> bool
|
|
206
369
|
|
|
207
|
-
# `core_ext/integer/inflections`
|
|
370
|
+
# `core_ext/integer/inflections` — `ActiveSupport::Inflector.ordinal(ize)`,
|
|
371
|
+
# the same inflection-rule-table assumption as the String group above.
|
|
372
|
+
%a{pure}
|
|
208
373
|
def ordinal: () -> String
|
|
374
|
+
%a{pure}
|
|
209
375
|
def ordinalize: () -> String
|
|
210
376
|
end
|
|
211
377
|
|
|
@@ -214,28 +380,52 @@ end
|
|
|
214
380
|
# ---------------------------------------------------------------
|
|
215
381
|
|
|
216
382
|
class Float
|
|
383
|
+
# Same reasoning as the `Integer` block above: value-object construction
|
|
384
|
+
# and arithmetic only.
|
|
385
|
+
%a{pure}
|
|
217
386
|
def second: () -> untyped
|
|
387
|
+
%a{pure}
|
|
218
388
|
def seconds: () -> untyped
|
|
389
|
+
%a{pure}
|
|
219
390
|
def minute: () -> untyped
|
|
391
|
+
%a{pure}
|
|
220
392
|
def minutes: () -> untyped
|
|
393
|
+
%a{pure}
|
|
221
394
|
def hour: () -> untyped
|
|
395
|
+
%a{pure}
|
|
222
396
|
def hours: () -> untyped
|
|
397
|
+
%a{pure}
|
|
223
398
|
def day: () -> untyped
|
|
399
|
+
%a{pure}
|
|
224
400
|
def days: () -> untyped
|
|
401
|
+
%a{pure}
|
|
225
402
|
def week: () -> untyped
|
|
403
|
+
%a{pure}
|
|
226
404
|
def weeks: () -> untyped
|
|
405
|
+
%a{pure}
|
|
227
406
|
def month: () -> untyped
|
|
407
|
+
%a{pure}
|
|
228
408
|
def months: () -> untyped
|
|
409
|
+
%a{pure}
|
|
229
410
|
def year: () -> untyped
|
|
411
|
+
%a{pure}
|
|
230
412
|
def years: () -> untyped
|
|
231
413
|
|
|
414
|
+
%a{pure}
|
|
232
415
|
def byte: () -> Float
|
|
416
|
+
%a{pure}
|
|
233
417
|
def bytes: () -> Float
|
|
418
|
+
%a{pure}
|
|
234
419
|
def kilobyte: () -> Float
|
|
420
|
+
%a{pure}
|
|
235
421
|
def kilobytes: () -> Float
|
|
422
|
+
%a{pure}
|
|
236
423
|
def megabyte: () -> Float
|
|
424
|
+
%a{pure}
|
|
237
425
|
def megabytes: () -> Float
|
|
426
|
+
%a{pure}
|
|
238
427
|
def gigabyte: () -> Float
|
|
428
|
+
%a{pure}
|
|
239
429
|
def gigabytes: () -> Float
|
|
240
430
|
end
|
|
241
431
|
|
|
@@ -244,6 +434,9 @@ end
|
|
|
244
434
|
# ---------------------------------------------------------------
|
|
245
435
|
|
|
246
436
|
class Time
|
|
437
|
+
# NOT %a{pure}: already attributed nondet.time/global.read/global.write
|
|
438
|
+
# by `lib/rigor/plugin/activesupport_core_ext/effects.rb` (issue #387).
|
|
439
|
+
# Duplicating an RBS envelope here risks the two disagreeing.
|
|
247
440
|
def self.current: () -> Time
|
|
248
441
|
def self.zone: () -> untyped # ActiveSupport::TimeZone | nil
|
|
249
442
|
def self.zone=: (String | Symbol | untyped) -> untyped
|
|
@@ -253,38 +446,80 @@ class Time
|
|
|
253
446
|
# Declaring them here raised `RBS::DuplicatedMethodDefinitionError` and collapsed the whole `Time`
|
|
254
447
|
# definition to `Dynamic[top]`.
|
|
255
448
|
|
|
256
|
-
# `core_ext/time/calculations`
|
|
449
|
+
# `core_ext/time/calculations` — every method below is computed from the
|
|
450
|
+
# RECEIVER's own fields (`change` / `advance` / `+ seconds`), verified
|
|
451
|
+
# against `activesupport/lib/active_support/core_ext/time/calculations.rb`.
|
|
452
|
+
# None of them consult `Time.zone`; that is what distinguishes an
|
|
453
|
+
# instance method here from the `self.*` singletons above.
|
|
454
|
+
%a{pure}
|
|
257
455
|
def yesterday: () -> Time
|
|
456
|
+
%a{pure}
|
|
258
457
|
def tomorrow: () -> Time
|
|
458
|
+
%a{pure}
|
|
259
459
|
def beginning_of_day: () -> Time
|
|
460
|
+
%a{pure}
|
|
260
461
|
def end_of_day: () -> Time
|
|
462
|
+
%a{pure}
|
|
261
463
|
def beginning_of_hour: () -> Time
|
|
464
|
+
%a{pure}
|
|
262
465
|
def end_of_hour: () -> Time
|
|
466
|
+
%a{pure}
|
|
263
467
|
def beginning_of_minute: () -> Time
|
|
468
|
+
%a{pure}
|
|
264
469
|
def end_of_minute: () -> Time
|
|
470
|
+
# NOT %a{pure}: `start_day` defaults to `Date.beginning_of_week`, a
|
|
471
|
+
# fiber-local config getter (`ActiveSupport::IsolatedExecutionState[:beginning_of_week]`)
|
|
472
|
+
# — a global read when the argument is omitted.
|
|
265
473
|
def beginning_of_week: (?Symbol start_day) -> Time
|
|
266
474
|
def end_of_week: (?Symbol start_day) -> Time
|
|
475
|
+
%a{pure}
|
|
267
476
|
def beginning_of_month: () -> Time
|
|
477
|
+
%a{pure}
|
|
268
478
|
def end_of_month: () -> Time
|
|
479
|
+
%a{pure}
|
|
269
480
|
def beginning_of_year: () -> Time
|
|
481
|
+
%a{pure}
|
|
270
482
|
def end_of_year: () -> Time
|
|
483
|
+
# `Time#ago` / `#since` / `#in` (receiver a `Time`, not a `Duration`):
|
|
484
|
+
# `since(seconds) = self + seconds`, purely computed from the receiver.
|
|
485
|
+
# Distinct from `ActiveSupport::Duration#ago` (receiver a Duration,
|
|
486
|
+
# reads the clock), attributed separately in `effects.rb`.
|
|
487
|
+
%a{pure}
|
|
271
488
|
def ago: (Numeric seconds) -> Time
|
|
489
|
+
%a{pure}
|
|
272
490
|
def since: (Numeric seconds) -> Time
|
|
491
|
+
%a{pure}
|
|
273
492
|
def in: (Numeric seconds) -> Time
|
|
493
|
+
%a{pure}
|
|
274
494
|
def change: (**untyped) -> Time
|
|
495
|
+
%a{pure}
|
|
275
496
|
def at_beginning_of_day: () -> Time
|
|
497
|
+
%a{pure}
|
|
276
498
|
def at_end_of_day: () -> Time
|
|
499
|
+
# NOT %a{pure}: same `Date.beginning_of_week` default-argument read as
|
|
500
|
+
# `beginning_of_week` / `end_of_week` above.
|
|
277
501
|
def at_beginning_of_week: () -> Time
|
|
278
502
|
def at_end_of_week: () -> Time
|
|
503
|
+
%a{pure}
|
|
279
504
|
def at_midnight: () -> Time
|
|
505
|
+
%a{pure}
|
|
280
506
|
def at_noon: () -> Time
|
|
507
|
+
%a{pure}
|
|
281
508
|
def midday: () -> Time
|
|
509
|
+
%a{pure}
|
|
282
510
|
def midnight: () -> Time
|
|
511
|
+
%a{pure}
|
|
283
512
|
def noon: () -> Time
|
|
284
513
|
# NOTE: `Time#utc?` is NOT declared here — it is Ruby core, already typed by rbs's `core/time.rbs`.
|
|
514
|
+
%a{pure}
|
|
285
515
|
def acts_like_time?: () -> true
|
|
286
516
|
# `advance(days: 1, months: -2)` and `all_day` (a `beginning_of_day..end_of_day` Range).
|
|
517
|
+
# Both computed from the receiver only (`advance` goes through `to_date.gregorian.advance` +
|
|
518
|
+
# `change`, never `Time.zone`; `all_day` composes the two pure `beginning_of_day` / `end_of_day`
|
|
519
|
+
# above) — unlike the same-named methods on `Date` below.
|
|
520
|
+
%a{pure}
|
|
287
521
|
def advance: (untyped options) -> Time
|
|
522
|
+
%a{pure}
|
|
288
523
|
def all_day: () -> Range[Time]
|
|
289
524
|
end
|
|
290
525
|
|
|
@@ -293,6 +528,11 @@ end
|
|
|
293
528
|
# ---------------------------------------------------------------
|
|
294
529
|
|
|
295
530
|
class Date
|
|
531
|
+
# NOT %a{pure}: `self.current` / `self.yesterday` / `self.tomorrow` are
|
|
532
|
+
# already attributed nondet.time/global.read by `effects.rb` (#387).
|
|
533
|
+
# The other five singletons declared here — `self.beginning_of_week`
|
|
534
|
+
# through `self.end_of_year` — do not soundly exist; see the file-header
|
|
535
|
+
# note. None get an annotation.
|
|
296
536
|
def self.current: () -> Date
|
|
297
537
|
def self.yesterday: () -> Date
|
|
298
538
|
def self.tomorrow: () -> Date
|
|
@@ -303,16 +543,31 @@ class Date
|
|
|
303
543
|
def self.beginning_of_year: () -> Date
|
|
304
544
|
def self.end_of_year: () -> Date
|
|
305
545
|
|
|
546
|
+
# `advance(days: -1)` / `advance(days: 1)` — pure y/m/d arithmetic on the
|
|
547
|
+
# receiver (`core_ext/date_and_time/calculations.rb`), unlike the
|
|
548
|
+
# `self.yesterday` / `self.tomorrow` singletons above.
|
|
549
|
+
%a{pure}
|
|
306
550
|
def yesterday: () -> Date
|
|
551
|
+
%a{pure}
|
|
307
552
|
def tomorrow: () -> Date
|
|
553
|
+
# NOT %a{pure}: `start_day` defaults to `Date.beginning_of_week`, a
|
|
554
|
+
# fiber-local config read — same reasoning as `Time#beginning_of_week`.
|
|
308
555
|
def beginning_of_week: (?Symbol start_day) -> Date
|
|
309
556
|
def end_of_week: (?Symbol start_day) -> Date
|
|
557
|
+
%a{pure}
|
|
310
558
|
def beginning_of_month: () -> Date
|
|
559
|
+
%a{pure}
|
|
311
560
|
def end_of_month: () -> Date
|
|
561
|
+
%a{pure}
|
|
312
562
|
def beginning_of_year: () -> Date
|
|
563
|
+
%a{pure}
|
|
313
564
|
def end_of_year: () -> Date
|
|
565
|
+
# NOT %a{pure}: `Date#ago` / `#since` route through `in_time_zone`
|
|
566
|
+
# (`in_time_zone.since(...)`), which reads `Time.zone` by default —
|
|
567
|
+
# unlike `Time#ago` / `#since` above, which never leave the receiver.
|
|
314
568
|
def ago: (Numeric seconds) -> Time
|
|
315
569
|
def since: (Numeric seconds) -> Time
|
|
570
|
+
%a{pure}
|
|
316
571
|
def acts_like_date?: () -> true
|
|
317
572
|
|
|
318
573
|
# `core_ext/date/calculations` — `Date#midnight` /
|
|
@@ -320,17 +575,46 @@ class Date
|
|
|
320
575
|
# Rails' `beginning_of_day` on Date returns a Time at
|
|
321
576
|
# midnight of that day (not a Date). Mastodon's
|
|
322
577
|
# `Date.current.at_midnight` shape relies on this.
|
|
578
|
+
#
|
|
579
|
+
# NONE of the six below are `%a{pure}`: every one is `in_time_zone.xxx`
|
|
580
|
+
# (`core_ext/date/zones.rb`, `DateAndTime::Zones#in_time_zone`), and
|
|
581
|
+
# `in_time_zone(zone = ::Time.zone)` reads `Time.zone` when no explicit
|
|
582
|
+
# zone is given. This is the one place `Date` and `Time` diverge on the
|
|
583
|
+
# exact same method names — verify against source before trusting the
|
|
584
|
+
# name alone.
|
|
323
585
|
def beginning_of_day: () -> Time
|
|
324
586
|
def midnight: () -> Time
|
|
325
587
|
def at_midnight: () -> Time
|
|
326
588
|
def at_beginning_of_day: () -> Time
|
|
327
589
|
def end_of_day: () -> Time
|
|
328
590
|
def at_end_of_day: () -> Time
|
|
329
|
-
# `advance(days: 1)
|
|
330
|
-
#
|
|
591
|
+
# `advance(days: 1)` is pure y/m/d arithmetic on the receiver, same as
|
|
592
|
+
# `yesterday` / `tomorrow` above. `all_day` is NOT %a{pure}: it composes
|
|
593
|
+
# `beginning_of_day..end_of_day`, and both of those read `Time.zone` on
|
|
594
|
+
# `Date` (unlike on `Time`, where `all_day` is pure). `to_time(form)` is
|
|
595
|
+
# `Time.public_send(form, year, month, day)` — pure, all three components
|
|
596
|
+
# come from the receiver and none default from the clock (contrast
|
|
597
|
+
# `String#to_time` above, which does).
|
|
598
|
+
%a{pure}
|
|
331
599
|
def advance: (untyped options) -> Date
|
|
332
600
|
def all_day: () -> Range[Time]
|
|
333
|
-
|
|
601
|
+
# NOTE: `Date#to_time` is an OVERLOAD CONTINUATION (`| ...`), not a plain
|
|
602
|
+
# declaration — stdlib `date` already types `to_time: () -> Time`, and a
|
|
603
|
+
# second full declaration of the same method from a second signature source
|
|
604
|
+
# makes `RBS::DefinitionBuilder` raise `DuplicatedMethodDefinitionError`,
|
|
605
|
+
# which collapses `Date` AND `DateTime` to `Dynamic[top]` for every project
|
|
606
|
+
# that activates this plugin (#437 — the same failure the `DateTime#to_time`
|
|
607
|
+
# note below records). The row cannot simply be DROPPED the way
|
|
608
|
+
# `DateTime#to_time` was: ActiveSupport genuinely widens the arity, and
|
|
609
|
+
# without the row `date.to_time(:utc)` — correct Rails code — would draw an
|
|
610
|
+
# arity diagnostic. `| ...` appends this overload ahead of the stdlib one, so
|
|
611
|
+
# both arities resolve and the `%a{pure}` envelope stays attached to the
|
|
612
|
+
# ActiveSupport overload (which is the one a no-argument call selects).
|
|
613
|
+
# `| ...` requires the base declaration to exist, which `Environment.for_project`
|
|
614
|
+
# guarantees: it merges `DEFAULT_LIBRARIES` — including `date` — into every
|
|
615
|
+
# environment that loads a plugin's `signature_paths`.
|
|
616
|
+
%a{pure}
|
|
617
|
+
def to_time: (?Symbol form) -> Time | ...
|
|
334
618
|
end
|
|
335
619
|
|
|
336
620
|
# ---------------------------------------------------------------
|
|
@@ -340,41 +624,61 @@ end
|
|
|
340
624
|
class Array[unchecked out Elem]
|
|
341
625
|
# `Array.wrap(x)` is the dominant Rails idiom: `nil → []`,
|
|
342
626
|
# `Array x → x`, `else → [x]`.
|
|
627
|
+
%a{pure}
|
|
343
628
|
def self.wrap: (untyped) -> Array[untyped]
|
|
344
629
|
|
|
345
630
|
# `core_ext/array/access`
|
|
631
|
+
%a{pure}
|
|
346
632
|
def from: (Integer position) -> Array[Elem]
|
|
633
|
+
%a{pure}
|
|
347
634
|
def to: (Integer position) -> Array[Elem]
|
|
635
|
+
%a{pure}
|
|
348
636
|
def second: () -> Elem?
|
|
637
|
+
%a{pure}
|
|
349
638
|
def third: () -> Elem?
|
|
639
|
+
%a{pure}
|
|
350
640
|
def fourth: () -> Elem?
|
|
641
|
+
%a{pure}
|
|
351
642
|
def fifth: () -> Elem?
|
|
643
|
+
%a{pure}
|
|
352
644
|
def forty_two: () -> Elem?
|
|
353
645
|
|
|
354
646
|
# `core_ext/array/grouping`
|
|
647
|
+
%a{pure}
|
|
355
648
|
def in_groups_of: (Integer number, ?untyped fill_with) -> Array[Array[Elem]]
|
|
356
649
|
| (Integer number, ?untyped fill_with) { (Array[Elem]) -> void } -> Array[Elem]
|
|
650
|
+
%a{pure}
|
|
357
651
|
def in_groups: (Integer number, ?untyped fill_with) -> Array[Array[Elem]]
|
|
358
652
|
| (Integer number, ?untyped fill_with) { (Array[Elem]) -> void } -> Array[Elem]
|
|
653
|
+
%a{pure}
|
|
359
654
|
def split: (?untyped value) -> Array[Array[Elem]]
|
|
360
655
|
| () { (Elem) -> bool } -> Array[Array[Elem]]
|
|
361
656
|
|
|
362
|
-
# `core_ext/array/conversions`
|
|
657
|
+
# `core_ext/array/conversions`. NEITHER `to_sentence` nor `to_fs` /
|
|
658
|
+
# `to_formatted_s` is `%a{pure}`: `to_sentence` calls `I18n.translate`
|
|
659
|
+
# for its connector words unless `locale: false` is passed explicitly
|
|
660
|
+
# (a request-scoped global read); `to_fs(:db)` calls `#id` on every
|
|
661
|
+
# element, a dispatch to an unknown-at-this-declaration method the same
|
|
662
|
+
# way `Object#try` does. `to_xml` takes a caller-supplied `:builder` and
|
|
663
|
+
# an optional block, both opaque callables — skipped for the same reason.
|
|
363
664
|
def to_sentence: (?two_words_connector: String, ?last_word_connector: String, ?words_connector: String, ?locale: Symbol?) -> String
|
|
364
665
|
def to_formatted_s: (?Symbol format) -> String
|
|
365
666
|
def to_fs: (?Symbol format) -> String
|
|
366
667
|
def to_xml: (**untyped) -> String
|
|
367
668
|
|
|
368
669
|
# `core_ext/array/inquiry`
|
|
670
|
+
%a{pure}
|
|
369
671
|
def inquiry: () -> untyped # ActiveSupport::ArrayInquirer
|
|
370
672
|
|
|
371
673
|
# `core_ext/array/extract`
|
|
372
|
-
def extract!: () { (Elem) -> bool } -> Array[Elem]
|
|
674
|
+
def extract!: () { (Elem) -> bool } -> Array[Elem] # NOT %a{pure}: bang, mutates in place.
|
|
373
675
|
|
|
374
676
|
# `core_ext/object/blank` / `core_ext/enumerable` — both
|
|
375
677
|
# ActiveSupport additions, shipped on Array specifically.
|
|
678
|
+
%a{pure}
|
|
376
679
|
def compact_blank: () -> Array[Elem]
|
|
377
|
-
def compact_blank!: () -> self
|
|
680
|
+
def compact_blank!: () -> self # NOT %a{pure}: bang (`delete_if`), mutates in place.
|
|
681
|
+
%a{pure}
|
|
378
682
|
def exclude?: (Elem) -> bool
|
|
379
683
|
end
|
|
380
684
|
|
|
@@ -383,20 +687,35 @@ end
|
|
|
383
687
|
# ---------------------------------------------------------------
|
|
384
688
|
|
|
385
689
|
module Enumerable[unchecked out Elem]
|
|
690
|
+
%a{pure}
|
|
386
691
|
def index_by: () { (Elem) -> untyped } -> Hash[untyped, Elem]
|
|
692
|
+
%a{pure}
|
|
387
693
|
def index_with: (?untyped default) { (Elem) -> untyped } -> Hash[Elem, untyped]
|
|
388
694
|
| (untyped default) -> Hash[Elem, untyped]
|
|
695
|
+
%a{pure}
|
|
389
696
|
def exclude?: (Elem) -> bool
|
|
697
|
+
%a{pure}
|
|
390
698
|
def including: (*Elem) -> Array[Elem]
|
|
699
|
+
%a{pure}
|
|
391
700
|
def excluding: (*Elem) -> Array[Elem]
|
|
701
|
+
%a{pure}
|
|
392
702
|
def without: (*Elem) -> Array[Elem]
|
|
703
|
+
%a{pure}
|
|
393
704
|
def pluck: (Symbol | String) -> Array[untyped]
|
|
394
705
|
| (*Symbol | String) -> Array[Array[untyped]]
|
|
706
|
+
%a{pure}
|
|
395
707
|
def pick: (Symbol | String) -> untyped
|
|
396
708
|
| (*Symbol | String) -> Array[untyped]?
|
|
709
|
+
%a{pure}
|
|
397
710
|
def maximum: (Symbol | String) -> untyped
|
|
711
|
+
%a{pure}
|
|
398
712
|
def minimum: (Symbol | String) -> untyped
|
|
713
|
+
# Raises `SoleItemExpectedError` rather than returning on 0-or-2+ items;
|
|
714
|
+
# raising is not an effect label (AGENTS.md / design note), so this is
|
|
715
|
+
# still pure.
|
|
716
|
+
%a{pure}
|
|
399
717
|
def sole: () -> Elem
|
|
718
|
+
%a{pure}
|
|
400
719
|
def compact_blank: () -> Array[Elem]
|
|
401
720
|
end
|
|
402
721
|
|
|
@@ -405,25 +724,39 @@ end
|
|
|
405
724
|
# ---------------------------------------------------------------
|
|
406
725
|
|
|
407
726
|
class Hash[unchecked out K, unchecked out V]
|
|
408
|
-
# `core_ext/hash/keys`
|
|
727
|
+
# `core_ext/hash/keys` — bang variants mutate in place, so only the
|
|
728
|
+
# non-bang half is `%a{pure}`. `assert_valid_keys` raises rather than
|
|
729
|
+
# mutating (raising is not an effect label).
|
|
730
|
+
%a{pure}
|
|
409
731
|
def symbolize_keys: () -> Hash[Symbol, V]
|
|
410
732
|
def symbolize_keys!: () -> self
|
|
733
|
+
%a{pure}
|
|
411
734
|
def deep_symbolize_keys: () -> Hash[untyped, untyped]
|
|
412
735
|
def deep_symbolize_keys!: () -> self
|
|
736
|
+
%a{pure}
|
|
413
737
|
def stringify_keys: () -> Hash[String, V]
|
|
414
738
|
def stringify_keys!: () -> self
|
|
739
|
+
%a{pure}
|
|
415
740
|
def deep_stringify_keys: () -> Hash[untyped, untyped]
|
|
416
741
|
def deep_stringify_keys!: () -> self
|
|
742
|
+
%a{pure}
|
|
417
743
|
def assert_valid_keys: (*K | Array[K]) -> self
|
|
744
|
+
%a{pure}
|
|
418
745
|
def deep_transform_keys: () { (K) -> K } -> Hash[K, untyped]
|
|
419
746
|
def deep_transform_keys!: () { (K) -> K } -> self
|
|
747
|
+
%a{pure}
|
|
420
748
|
def deep_transform_values: () { (V) -> untyped } -> Hash[K, untyped]
|
|
421
749
|
def deep_transform_values!: () { (V) -> untyped } -> self
|
|
422
750
|
|
|
423
|
-
# `core_ext/hash/deep_dup`
|
|
751
|
+
# `core_ext/hash/deep_dup` — allocates a deep copy, never touches the
|
|
752
|
+
# receiver.
|
|
753
|
+
%a{pure}
|
|
424
754
|
def deep_dup: () -> Hash[K, V]
|
|
425
755
|
|
|
426
|
-
# `core_ext/hash/deep_merge`
|
|
756
|
+
# `core_ext/hash/deep_merge` — the block receives `(key, this_val,
|
|
757
|
+
# other_val)` and returns the merged value; neither `deep_merge` nor its
|
|
758
|
+
# block mutates either hash (containment covers the block itself).
|
|
759
|
+
%a{pure}
|
|
427
760
|
def deep_merge: (Hash[K, V]) -> Hash[K, V]
|
|
428
761
|
| (Hash[K, V]) { (K, V, V) -> V } -> Hash[K, V]
|
|
429
762
|
def deep_merge!: (Hash[K, V]) -> self
|
|
@@ -433,15 +766,24 @@ class Hash[unchecked out K, unchecked out V]
|
|
|
433
766
|
# Ruby 3.0+; `except!` is ActiveSupport-only. ActiveSupport
|
|
434
767
|
# also aliases `Hash#without` to `Hash#except`, used by
|
|
435
768
|
# `Mastodon`-shaped `options.without('type').merge(...)` chains.
|
|
436
|
-
def except!: (*K) -> self
|
|
769
|
+
def except!: (*K) -> self # NOT %a{pure}: bang, mutates via `delete`.
|
|
770
|
+
%a{pure}
|
|
437
771
|
def without: (*K) -> Hash[K, V]
|
|
438
772
|
|
|
439
|
-
# `core_ext/hash/conversions`
|
|
773
|
+
# `core_ext/hash/conversions` — NOT `%a{pure}`: `to_query` / `to_param`
|
|
774
|
+
# recurse into `value.to_query` / `value.to_param` on every value, an
|
|
775
|
+
# unknown-at-this-declaration dispatch the same way `Object#try` and
|
|
776
|
+
# `Array#to_fs(:db)` are (a value's own `to_param` override is invisible
|
|
777
|
+
# here); `to_xml` takes a caller-supplied `:builder` and an optional
|
|
778
|
+
# block, both opaque callables.
|
|
440
779
|
def to_query: (?String namespace) -> String
|
|
441
780
|
def to_param: (?String namespace) -> String
|
|
442
781
|
def to_xml: (**untyped) -> String
|
|
443
782
|
|
|
444
|
-
# `core_ext/hash/indifferent_access`
|
|
783
|
+
# `core_ext/hash/indifferent_access` — wraps `self` in a new
|
|
784
|
+
# `HashWithIndifferentAccess`; the design note § 11.2 names this row
|
|
785
|
+
# `%a{pure}` explicitly.
|
|
786
|
+
%a{pure}
|
|
445
787
|
def with_indifferent_access: () -> untyped
|
|
446
788
|
|
|
447
789
|
# `core_ext/hash/conversions`
|
|
@@ -449,17 +791,19 @@ class Hash[unchecked out K, unchecked out V]
|
|
|
449
791
|
def self.from_trusted_xml: (String) -> Hash[String, untyped]
|
|
450
792
|
|
|
451
793
|
# `core_ext/object/blank` / `core_ext/hash`
|
|
794
|
+
%a{pure}
|
|
452
795
|
def compact_blank: () -> Hash[K, V]
|
|
453
|
-
def compact_blank!: () -> self
|
|
796
|
+
def compact_blank!: () -> self # NOT %a{pure}: bang (`delete_if`), mutates in place.
|
|
454
797
|
|
|
455
798
|
# `core_ext/hash/reverse_merge`
|
|
799
|
+
%a{pure}
|
|
456
800
|
def reverse_merge: (Hash[K, V]) -> Hash[K, V]
|
|
457
|
-
def reverse_merge!: (Hash[K, V]) -> self
|
|
801
|
+
def reverse_merge!: (Hash[K, V]) -> self # NOT %a{pure}: bang (`replace`), mutates in place.
|
|
458
802
|
|
|
459
803
|
# `core_ext/hash/slice` — `Hash#except` is in core RBS (Ruby 3.0+);
|
|
460
804
|
# `Hash#slice` is in core RBS (Ruby 2.5+); the bang variants are
|
|
461
805
|
# ActiveSupport-only.
|
|
462
|
-
def slice!: (*K) -> Hash[K, V]
|
|
806
|
+
def slice!: (*K) -> Hash[K, V] # NOT %a{pure}: bang (`replace`), mutates in place.
|
|
463
807
|
end
|
|
464
808
|
|
|
465
809
|
# ---------------------------------------------------------------
|
|
@@ -467,19 +811,43 @@ end
|
|
|
467
811
|
# ---------------------------------------------------------------
|
|
468
812
|
|
|
469
813
|
class DateTime
|
|
814
|
+
# `utc` is pure Rational arithmetic on the receiver's own offset
|
|
815
|
+
# (`core_ext/date_time/calculations.rb`), no zone lookup.
|
|
816
|
+
%a{pure}
|
|
470
817
|
def utc: () -> Time
|
|
471
818
|
# NOTE: `DateTime#to_time` is NOT declared here — stdlib `date` already types it.
|
|
819
|
+
# NOT %a{pure}: `in_time_zone(zone = ::Time.zone)` reads `Time.zone`
|
|
820
|
+
# when no explicit zone is passed (`core_ext/date_and_time/zones.rb`).
|
|
821
|
+
# Not previously covered by #387's `effect_attributions:`; the plugin
|
|
822
|
+
# manifest gains this row in the same change (`global.read`).
|
|
472
823
|
def in_time_zone: (?String | Symbol zone) -> untyped
|
|
824
|
+
# `yesterday` / `tomorrow` are inherited from `DateAndTime::Calculations`
|
|
825
|
+
# via `Date` (`DateTime < Date`), resolved against `DateTime`'s own
|
|
826
|
+
# `advance` — pure, same reasoning as `Date#yesterday` / `#tomorrow`
|
|
827
|
+
# above and unlike `Date.self.yesterday` / `.tomorrow`.
|
|
828
|
+
%a{pure}
|
|
473
829
|
def yesterday: () -> DateTime
|
|
830
|
+
%a{pure}
|
|
474
831
|
def tomorrow: () -> DateTime
|
|
832
|
+
# `ago` / `since` are `self + Rational(seconds, 86400)` — pure, and
|
|
833
|
+
# (unlike `Date#ago` / `#since`) never route through `in_time_zone`.
|
|
834
|
+
%a{pure}
|
|
475
835
|
def ago: (Numeric seconds) -> DateTime
|
|
836
|
+
%a{pure}
|
|
476
837
|
def since: (Numeric seconds) -> DateTime
|
|
838
|
+
%a{pure}
|
|
477
839
|
def beginning_of_day: () -> DateTime
|
|
840
|
+
%a{pure}
|
|
478
841
|
def end_of_day: () -> DateTime
|
|
842
|
+
%a{pure}
|
|
479
843
|
def beginning_of_hour: () -> DateTime
|
|
844
|
+
%a{pure}
|
|
480
845
|
def end_of_hour: () -> DateTime
|
|
846
|
+
%a{pure}
|
|
481
847
|
def beginning_of_minute: () -> DateTime
|
|
848
|
+
%a{pure}
|
|
482
849
|
def end_of_minute: () -> DateTime
|
|
850
|
+
%a{pure}
|
|
483
851
|
def acts_like_time?: () -> true
|
|
484
852
|
end
|
|
485
853
|
|
|
@@ -491,6 +859,7 @@ end
|
|
|
491
859
|
# main String block above because it's a less-common method whose
|
|
492
860
|
# coverage was added after a second-round survey.)
|
|
493
861
|
class String
|
|
862
|
+
%a{pure}
|
|
494
863
|
def exclude?: (String) -> bool
|
|
495
864
|
end
|
|
496
865
|
|
|
@@ -505,6 +874,9 @@ class ERB
|
|
|
505
874
|
module Util
|
|
506
875
|
# `ERB::Util.html_escape_once(s)` escapes HTML without double-escaping
|
|
507
876
|
# an already-escaped entity. ActionView adds it on top of stdlib ERB.
|
|
877
|
+
# A deterministic regex-substitution transform of the argument; no
|
|
878
|
+
# receiver, no global reads.
|
|
879
|
+
%a{pure}
|
|
508
880
|
def self.html_escape_once: (untyped) -> String
|
|
509
881
|
end
|
|
510
882
|
end
|