rigortype 0.3.2 → 0.3.4
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 +843 -0
- data/data/effects/registry.yml +67 -0
- data/docs/handbook/07-rbs-and-extended.md +106 -5
- data/docs/manual/02-cli-reference.md +311 -1
- data/docs/manual/03-configuration.md +84 -1
- data/docs/manual/04-diagnostics.md +14 -2
- data/docs/manual/16-rbs-extended-annotations.md +77 -0
- data/docs/manual/18-removing-dead-code.md +326 -0
- data/docs/manual/README.md +5 -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/always_truthy_condition_collector.rb +8 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +53 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +28 -2
- data/lib/rigor/analysis/check_rules/rule_walk.rb +7 -0
- data/lib/rigor/analysis/check_rules.rb +62 -22
- 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 +163 -1
- data/lib/rigor/analysis/run_cache_key.rb +16 -0
- data/lib/rigor/analysis/runner/declaration_position.rb +54 -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 +68 -10
- data/lib/rigor/analysis/runner.rb +420 -10
- data/lib/rigor/analysis/severity_stamp.rb +2 -1
- data/lib/rigor/analysis/worker_session.rb +58 -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 +131 -0
- data/lib/rigor/cli/effects_diff_renderer.rb +123 -0
- data/lib/rigor/cli/effects_explain_renderer.rb +68 -0
- data/lib/rigor/cli/effects_renderer.rb +54 -0
- data/lib/rigor/cli/effects_report.rb +53 -0
- data/lib/rigor/cli/effects_snapshot_command.rb +256 -0
- data/lib/rigor/cli/unused_command.rb +288 -0
- data/lib/rigor/cli.rb +16 -0
- data/lib/rigor/configuration/severity_profile.rb +23 -0
- data/lib/rigor/configuration.rb +296 -14
- data/lib/rigor/effects/attribution.rb +76 -0
- data/lib/rigor/effects/catalog.rb +275 -0
- data/lib/rigor/effects/collector.rb +182 -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 +93 -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 +160 -0
- data/lib/rigor/effects/framework_units.rb +167 -0
- data/lib/rigor/effects/identity.rb +104 -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 +335 -0
- data/lib/rigor/effects/propagator.rb +275 -0
- data/lib/rigor/effects/registry.rb +193 -0
- data/lib/rigor/effects/scanner.rb +290 -0
- data/lib/rigor/effects/signature_sources.rb +74 -0
- data/lib/rigor/effects/snapshot.rb +380 -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 +38 -0
- data/lib/rigor/effects/unit_scan.rb +572 -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/anonymous_meta_class.rb +81 -0
- data/lib/rigor/inference/expression_typer.rb +74 -17
- data/lib/rigor/inference/fork_map.rb +45 -11
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +147 -17
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +97 -2
- data/lib/rigor/inference/method_dispatcher.rb +41 -10
- data/lib/rigor/inference/optimistic_origin.rb +68 -3
- data/lib/rigor/inference/pre_eval_constants.rb +189 -0
- data/lib/rigor/inference/scope_indexer.rb +134 -31
- data/lib/rigor/inference/singleton_object_constant.rb +63 -0
- data/lib/rigor/inference/statement_evaluator.rb +25 -12
- data/lib/rigor/plugin/base.rb +24 -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 +103 -3
- data/lib/rigor/plugin/node_rule_walk.rb +6 -0
- data/lib/rigor/plugin/registry.rb +48 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/protection/closure_kill_oracle.rb +20 -1
- data/lib/rigor/rbs_extended/envelope_scanner.rb +160 -0
- data/lib/rigor/rbs_extended.rb +168 -0
- data/lib/rigor/reflection.rb +131 -30
- data/lib/rigor/scope.rb +62 -2
- data/lib/rigor/source/node_walker.rb +12 -0
- data/lib/rigor/type/anonymous_class_name.rb +40 -0
- data/lib/rigor/type/nominal.rb +5 -2
- data/lib/rigor/type/singleton.rb +4 -1
- 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 +385 -28
- 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-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +122 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +56 -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/scope.rbs +4 -0
- data/sig/rigor.rbs +21 -1
- data/skills/rigor-unused-adjudicate/SKILL.md +90 -0
- metadata +83 -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,16 +575,30 @@ 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]
|
|
601
|
+
%a{pure}
|
|
333
602
|
def to_time: (?Symbol form) -> Time
|
|
334
603
|
end
|
|
335
604
|
|
|
@@ -340,41 +609,61 @@ end
|
|
|
340
609
|
class Array[unchecked out Elem]
|
|
341
610
|
# `Array.wrap(x)` is the dominant Rails idiom: `nil → []`,
|
|
342
611
|
# `Array x → x`, `else → [x]`.
|
|
612
|
+
%a{pure}
|
|
343
613
|
def self.wrap: (untyped) -> Array[untyped]
|
|
344
614
|
|
|
345
615
|
# `core_ext/array/access`
|
|
616
|
+
%a{pure}
|
|
346
617
|
def from: (Integer position) -> Array[Elem]
|
|
618
|
+
%a{pure}
|
|
347
619
|
def to: (Integer position) -> Array[Elem]
|
|
620
|
+
%a{pure}
|
|
348
621
|
def second: () -> Elem?
|
|
622
|
+
%a{pure}
|
|
349
623
|
def third: () -> Elem?
|
|
624
|
+
%a{pure}
|
|
350
625
|
def fourth: () -> Elem?
|
|
626
|
+
%a{pure}
|
|
351
627
|
def fifth: () -> Elem?
|
|
628
|
+
%a{pure}
|
|
352
629
|
def forty_two: () -> Elem?
|
|
353
630
|
|
|
354
631
|
# `core_ext/array/grouping`
|
|
632
|
+
%a{pure}
|
|
355
633
|
def in_groups_of: (Integer number, ?untyped fill_with) -> Array[Array[Elem]]
|
|
356
634
|
| (Integer number, ?untyped fill_with) { (Array[Elem]) -> void } -> Array[Elem]
|
|
635
|
+
%a{pure}
|
|
357
636
|
def in_groups: (Integer number, ?untyped fill_with) -> Array[Array[Elem]]
|
|
358
637
|
| (Integer number, ?untyped fill_with) { (Array[Elem]) -> void } -> Array[Elem]
|
|
638
|
+
%a{pure}
|
|
359
639
|
def split: (?untyped value) -> Array[Array[Elem]]
|
|
360
640
|
| () { (Elem) -> bool } -> Array[Array[Elem]]
|
|
361
641
|
|
|
362
|
-
# `core_ext/array/conversions`
|
|
642
|
+
# `core_ext/array/conversions`. NEITHER `to_sentence` nor `to_fs` /
|
|
643
|
+
# `to_formatted_s` is `%a{pure}`: `to_sentence` calls `I18n.translate`
|
|
644
|
+
# for its connector words unless `locale: false` is passed explicitly
|
|
645
|
+
# (a request-scoped global read); `to_fs(:db)` calls `#id` on every
|
|
646
|
+
# element, a dispatch to an unknown-at-this-declaration method the same
|
|
647
|
+
# way `Object#try` does. `to_xml` takes a caller-supplied `:builder` and
|
|
648
|
+
# an optional block, both opaque callables — skipped for the same reason.
|
|
363
649
|
def to_sentence: (?two_words_connector: String, ?last_word_connector: String, ?words_connector: String, ?locale: Symbol?) -> String
|
|
364
650
|
def to_formatted_s: (?Symbol format) -> String
|
|
365
651
|
def to_fs: (?Symbol format) -> String
|
|
366
652
|
def to_xml: (**untyped) -> String
|
|
367
653
|
|
|
368
654
|
# `core_ext/array/inquiry`
|
|
655
|
+
%a{pure}
|
|
369
656
|
def inquiry: () -> untyped # ActiveSupport::ArrayInquirer
|
|
370
657
|
|
|
371
658
|
# `core_ext/array/extract`
|
|
372
|
-
def extract!: () { (Elem) -> bool } -> Array[Elem]
|
|
659
|
+
def extract!: () { (Elem) -> bool } -> Array[Elem] # NOT %a{pure}: bang, mutates in place.
|
|
373
660
|
|
|
374
661
|
# `core_ext/object/blank` / `core_ext/enumerable` — both
|
|
375
662
|
# ActiveSupport additions, shipped on Array specifically.
|
|
663
|
+
%a{pure}
|
|
376
664
|
def compact_blank: () -> Array[Elem]
|
|
377
|
-
def compact_blank!: () -> self
|
|
665
|
+
def compact_blank!: () -> self # NOT %a{pure}: bang (`delete_if`), mutates in place.
|
|
666
|
+
%a{pure}
|
|
378
667
|
def exclude?: (Elem) -> bool
|
|
379
668
|
end
|
|
380
669
|
|
|
@@ -383,20 +672,35 @@ end
|
|
|
383
672
|
# ---------------------------------------------------------------
|
|
384
673
|
|
|
385
674
|
module Enumerable[unchecked out Elem]
|
|
675
|
+
%a{pure}
|
|
386
676
|
def index_by: () { (Elem) -> untyped } -> Hash[untyped, Elem]
|
|
677
|
+
%a{pure}
|
|
387
678
|
def index_with: (?untyped default) { (Elem) -> untyped } -> Hash[Elem, untyped]
|
|
388
679
|
| (untyped default) -> Hash[Elem, untyped]
|
|
680
|
+
%a{pure}
|
|
389
681
|
def exclude?: (Elem) -> bool
|
|
682
|
+
%a{pure}
|
|
390
683
|
def including: (*Elem) -> Array[Elem]
|
|
684
|
+
%a{pure}
|
|
391
685
|
def excluding: (*Elem) -> Array[Elem]
|
|
686
|
+
%a{pure}
|
|
392
687
|
def without: (*Elem) -> Array[Elem]
|
|
688
|
+
%a{pure}
|
|
393
689
|
def pluck: (Symbol | String) -> Array[untyped]
|
|
394
690
|
| (*Symbol | String) -> Array[Array[untyped]]
|
|
691
|
+
%a{pure}
|
|
395
692
|
def pick: (Symbol | String) -> untyped
|
|
396
693
|
| (*Symbol | String) -> Array[untyped]?
|
|
694
|
+
%a{pure}
|
|
397
695
|
def maximum: (Symbol | String) -> untyped
|
|
696
|
+
%a{pure}
|
|
398
697
|
def minimum: (Symbol | String) -> untyped
|
|
698
|
+
# Raises `SoleItemExpectedError` rather than returning on 0-or-2+ items;
|
|
699
|
+
# raising is not an effect label (AGENTS.md / design note), so this is
|
|
700
|
+
# still pure.
|
|
701
|
+
%a{pure}
|
|
399
702
|
def sole: () -> Elem
|
|
703
|
+
%a{pure}
|
|
400
704
|
def compact_blank: () -> Array[Elem]
|
|
401
705
|
end
|
|
402
706
|
|
|
@@ -405,25 +709,39 @@ end
|
|
|
405
709
|
# ---------------------------------------------------------------
|
|
406
710
|
|
|
407
711
|
class Hash[unchecked out K, unchecked out V]
|
|
408
|
-
# `core_ext/hash/keys`
|
|
712
|
+
# `core_ext/hash/keys` — bang variants mutate in place, so only the
|
|
713
|
+
# non-bang half is `%a{pure}`. `assert_valid_keys` raises rather than
|
|
714
|
+
# mutating (raising is not an effect label).
|
|
715
|
+
%a{pure}
|
|
409
716
|
def symbolize_keys: () -> Hash[Symbol, V]
|
|
410
717
|
def symbolize_keys!: () -> self
|
|
718
|
+
%a{pure}
|
|
411
719
|
def deep_symbolize_keys: () -> Hash[untyped, untyped]
|
|
412
720
|
def deep_symbolize_keys!: () -> self
|
|
721
|
+
%a{pure}
|
|
413
722
|
def stringify_keys: () -> Hash[String, V]
|
|
414
723
|
def stringify_keys!: () -> self
|
|
724
|
+
%a{pure}
|
|
415
725
|
def deep_stringify_keys: () -> Hash[untyped, untyped]
|
|
416
726
|
def deep_stringify_keys!: () -> self
|
|
727
|
+
%a{pure}
|
|
417
728
|
def assert_valid_keys: (*K | Array[K]) -> self
|
|
729
|
+
%a{pure}
|
|
418
730
|
def deep_transform_keys: () { (K) -> K } -> Hash[K, untyped]
|
|
419
731
|
def deep_transform_keys!: () { (K) -> K } -> self
|
|
732
|
+
%a{pure}
|
|
420
733
|
def deep_transform_values: () { (V) -> untyped } -> Hash[K, untyped]
|
|
421
734
|
def deep_transform_values!: () { (V) -> untyped } -> self
|
|
422
735
|
|
|
423
|
-
# `core_ext/hash/deep_dup`
|
|
736
|
+
# `core_ext/hash/deep_dup` — allocates a deep copy, never touches the
|
|
737
|
+
# receiver.
|
|
738
|
+
%a{pure}
|
|
424
739
|
def deep_dup: () -> Hash[K, V]
|
|
425
740
|
|
|
426
|
-
# `core_ext/hash/deep_merge`
|
|
741
|
+
# `core_ext/hash/deep_merge` — the block receives `(key, this_val,
|
|
742
|
+
# other_val)` and returns the merged value; neither `deep_merge` nor its
|
|
743
|
+
# block mutates either hash (containment covers the block itself).
|
|
744
|
+
%a{pure}
|
|
427
745
|
def deep_merge: (Hash[K, V]) -> Hash[K, V]
|
|
428
746
|
| (Hash[K, V]) { (K, V, V) -> V } -> Hash[K, V]
|
|
429
747
|
def deep_merge!: (Hash[K, V]) -> self
|
|
@@ -433,15 +751,24 @@ class Hash[unchecked out K, unchecked out V]
|
|
|
433
751
|
# Ruby 3.0+; `except!` is ActiveSupport-only. ActiveSupport
|
|
434
752
|
# also aliases `Hash#without` to `Hash#except`, used by
|
|
435
753
|
# `Mastodon`-shaped `options.without('type').merge(...)` chains.
|
|
436
|
-
def except!: (*K) -> self
|
|
754
|
+
def except!: (*K) -> self # NOT %a{pure}: bang, mutates via `delete`.
|
|
755
|
+
%a{pure}
|
|
437
756
|
def without: (*K) -> Hash[K, V]
|
|
438
757
|
|
|
439
|
-
# `core_ext/hash/conversions`
|
|
758
|
+
# `core_ext/hash/conversions` — NOT `%a{pure}`: `to_query` / `to_param`
|
|
759
|
+
# recurse into `value.to_query` / `value.to_param` on every value, an
|
|
760
|
+
# unknown-at-this-declaration dispatch the same way `Object#try` and
|
|
761
|
+
# `Array#to_fs(:db)` are (a value's own `to_param` override is invisible
|
|
762
|
+
# here); `to_xml` takes a caller-supplied `:builder` and an optional
|
|
763
|
+
# block, both opaque callables.
|
|
440
764
|
def to_query: (?String namespace) -> String
|
|
441
765
|
def to_param: (?String namespace) -> String
|
|
442
766
|
def to_xml: (**untyped) -> String
|
|
443
767
|
|
|
444
|
-
# `core_ext/hash/indifferent_access`
|
|
768
|
+
# `core_ext/hash/indifferent_access` — wraps `self` in a new
|
|
769
|
+
# `HashWithIndifferentAccess`; the design note § 11.2 names this row
|
|
770
|
+
# `%a{pure}` explicitly.
|
|
771
|
+
%a{pure}
|
|
445
772
|
def with_indifferent_access: () -> untyped
|
|
446
773
|
|
|
447
774
|
# `core_ext/hash/conversions`
|
|
@@ -449,17 +776,19 @@ class Hash[unchecked out K, unchecked out V]
|
|
|
449
776
|
def self.from_trusted_xml: (String) -> Hash[String, untyped]
|
|
450
777
|
|
|
451
778
|
# `core_ext/object/blank` / `core_ext/hash`
|
|
779
|
+
%a{pure}
|
|
452
780
|
def compact_blank: () -> Hash[K, V]
|
|
453
|
-
def compact_blank!: () -> self
|
|
781
|
+
def compact_blank!: () -> self # NOT %a{pure}: bang (`delete_if`), mutates in place.
|
|
454
782
|
|
|
455
783
|
# `core_ext/hash/reverse_merge`
|
|
784
|
+
%a{pure}
|
|
456
785
|
def reverse_merge: (Hash[K, V]) -> Hash[K, V]
|
|
457
|
-
def reverse_merge!: (Hash[K, V]) -> self
|
|
786
|
+
def reverse_merge!: (Hash[K, V]) -> self # NOT %a{pure}: bang (`replace`), mutates in place.
|
|
458
787
|
|
|
459
788
|
# `core_ext/hash/slice` — `Hash#except` is in core RBS (Ruby 3.0+);
|
|
460
789
|
# `Hash#slice` is in core RBS (Ruby 2.5+); the bang variants are
|
|
461
790
|
# ActiveSupport-only.
|
|
462
|
-
def slice!: (*K) -> Hash[K, V]
|
|
791
|
+
def slice!: (*K) -> Hash[K, V] # NOT %a{pure}: bang (`replace`), mutates in place.
|
|
463
792
|
end
|
|
464
793
|
|
|
465
794
|
# ---------------------------------------------------------------
|
|
@@ -467,19 +796,43 @@ end
|
|
|
467
796
|
# ---------------------------------------------------------------
|
|
468
797
|
|
|
469
798
|
class DateTime
|
|
799
|
+
# `utc` is pure Rational arithmetic on the receiver's own offset
|
|
800
|
+
# (`core_ext/date_time/calculations.rb`), no zone lookup.
|
|
801
|
+
%a{pure}
|
|
470
802
|
def utc: () -> Time
|
|
471
803
|
# NOTE: `DateTime#to_time` is NOT declared here — stdlib `date` already types it.
|
|
804
|
+
# NOT %a{pure}: `in_time_zone(zone = ::Time.zone)` reads `Time.zone`
|
|
805
|
+
# when no explicit zone is passed (`core_ext/date_and_time/zones.rb`).
|
|
806
|
+
# Not previously covered by #387's `effect_attributions:`; the plugin
|
|
807
|
+
# manifest gains this row in the same change (`global.read`).
|
|
472
808
|
def in_time_zone: (?String | Symbol zone) -> untyped
|
|
809
|
+
# `yesterday` / `tomorrow` are inherited from `DateAndTime::Calculations`
|
|
810
|
+
# via `Date` (`DateTime < Date`), resolved against `DateTime`'s own
|
|
811
|
+
# `advance` — pure, same reasoning as `Date#yesterday` / `#tomorrow`
|
|
812
|
+
# above and unlike `Date.self.yesterday` / `.tomorrow`.
|
|
813
|
+
%a{pure}
|
|
473
814
|
def yesterday: () -> DateTime
|
|
815
|
+
%a{pure}
|
|
474
816
|
def tomorrow: () -> DateTime
|
|
817
|
+
# `ago` / `since` are `self + Rational(seconds, 86400)` — pure, and
|
|
818
|
+
# (unlike `Date#ago` / `#since`) never route through `in_time_zone`.
|
|
819
|
+
%a{pure}
|
|
475
820
|
def ago: (Numeric seconds) -> DateTime
|
|
821
|
+
%a{pure}
|
|
476
822
|
def since: (Numeric seconds) -> DateTime
|
|
823
|
+
%a{pure}
|
|
477
824
|
def beginning_of_day: () -> DateTime
|
|
825
|
+
%a{pure}
|
|
478
826
|
def end_of_day: () -> DateTime
|
|
827
|
+
%a{pure}
|
|
479
828
|
def beginning_of_hour: () -> DateTime
|
|
829
|
+
%a{pure}
|
|
480
830
|
def end_of_hour: () -> DateTime
|
|
831
|
+
%a{pure}
|
|
481
832
|
def beginning_of_minute: () -> DateTime
|
|
833
|
+
%a{pure}
|
|
482
834
|
def end_of_minute: () -> DateTime
|
|
835
|
+
%a{pure}
|
|
483
836
|
def acts_like_time?: () -> true
|
|
484
837
|
end
|
|
485
838
|
|
|
@@ -491,6 +844,7 @@ end
|
|
|
491
844
|
# main String block above because it's a less-common method whose
|
|
492
845
|
# coverage was added after a second-round survey.)
|
|
493
846
|
class String
|
|
847
|
+
%a{pure}
|
|
494
848
|
def exclude?: (String) -> bool
|
|
495
849
|
end
|
|
496
850
|
|
|
@@ -505,6 +859,9 @@ class ERB
|
|
|
505
859
|
module Util
|
|
506
860
|
# `ERB::Util.html_escape_once(s)` escapes HTML without double-escaping
|
|
507
861
|
# an already-escaped entity. ActionView adds it on top of stdlib ERB.
|
|
862
|
+
# A deterministic regex-substitution transform of the argument; no
|
|
863
|
+
# receiver, no global reads.
|
|
864
|
+
%a{pure}
|
|
508
865
|
def self.html_escape_once: (untyped) -> String
|
|
509
866
|
end
|
|
510
867
|
end
|