rigortype 0.3.6 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/data/builtins/ruby_core/date.yml +6 -3
- data/data/builtins/ruby_core/enumerable.yml +1 -0
- data/data/builtins/ruby_core/exception.yml +2 -1
- data/data/builtins/ruby_core/file.yml +28 -14
- data/data/builtins/ruby_core/hash.yml +5 -2
- data/data/builtins/ruby_core/io.yml +33 -16
- data/data/builtins/ruby_core/random.yml +2 -1
- data/data/builtins/ruby_core/re.yml +6 -3
- data/data/builtins/ruby_core/struct.yml +2 -0
- data/data/builtins/ruby_core/time.yml +2 -1
- data/data/gem_overlay/activesupport/core_ext.rbs +409 -11
- data/docs/manual/02-cli-reference.md +16 -7
- data/docs/manual/05-inspecting-types.md +20 -8
- data/docs/manual/07-plugins.md +4 -0
- data/docs/manual/plugins/rigor-actionpack.md +61 -0
- data/docs/manual/plugins/rigor-activerecord.md +74 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
- data/docs/manual/plugins/rigor-sidekiq.md +22 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
- data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
- data/lib/rigor/analysis/check_rules.rb +383 -48
- data/lib/rigor/analysis/crash_signature.rb +116 -0
- data/lib/rigor/analysis/dependency_recorder.rb +18 -3
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +28 -0
- data/lib/rigor/analysis/incremental_session.rb +44 -9
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
- data/lib/rigor/analysis/reachability/graph.rb +14 -5
- data/lib/rigor/analysis/reachability/scan.rb +7 -3
- data/lib/rigor/analysis/result.rb +32 -0
- data/lib/rigor/analysis/run_cache_key.rb +41 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +34 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
- data/lib/rigor/analysis/runner.rb +147 -10
- data/lib/rigor/analysis/worker_session.rb +10 -1
- data/lib/rigor/bleeding_edge.rb +7 -6
- data/lib/rigor/builtins/hkt_builtins.rb +45 -6
- data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
- data/lib/rigor/builtins/static_return_refinements.rb +20 -1
- data/lib/rigor/cache/descriptor.rb +42 -2
- data/lib/rigor/cache/incremental_snapshot.rb +40 -3
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +31 -8
- data/lib/rigor/cache/store.rb +10 -1
- data/lib/rigor/cli/coverage_command.rb +23 -18
- data/lib/rigor/cli/coverage_mutation.rb +13 -17
- data/lib/rigor/cli/coverage_scan.rb +47 -6
- data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
- data/lib/rigor/cli/fused_protection_report.rb +23 -2
- data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
- data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
- data/lib/rigor/cli/mutation_protection_report.rb +31 -2
- data/lib/rigor/cli/sig_gen_command.rb +18 -0
- data/lib/rigor/cli/type_of_command.rb +188 -41
- data/lib/rigor/cli/type_of_renderer.rb +72 -3
- data/lib/rigor/cli/type_scan_command.rb +10 -1
- data/lib/rigor/cli.rb +1 -1
- data/lib/rigor/configuration.rb +7 -4
- data/lib/rigor/effects/plugin_facts.rb +1 -1
- data/lib/rigor/effects/scanner.rb +2 -3
- data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
- data/lib/rigor/environment/lockfile_resolver.rb +15 -5
- data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
- data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
- data/lib/rigor/environment/rbs_loader.rb +525 -46
- data/lib/rigor/environment.rb +38 -15
- data/lib/rigor/inference/acceptance.rb +172 -20
- data/lib/rigor/inference/captured_locals.rb +62 -0
- data/lib/rigor/inference/content_join.rb +347 -0
- data/lib/rigor/inference/def_handle.rb +15 -3
- data/lib/rigor/inference/def_node_resolver.rb +62 -3
- data/lib/rigor/inference/expression_typer.rb +1020 -141
- data/lib/rigor/inference/fork_map.rb +6 -1
- data/lib/rigor/inference/hkt_reducer.rb +12 -1
- data/lib/rigor/inference/hkt_registry.rb +46 -0
- data/lib/rigor/inference/hkt_sugar_translator.rb +93 -0
- data/lib/rigor/inference/index_write_widening.rb +48 -0
- data/lib/rigor/inference/indexed_narrowing.rb +14 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
- data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
- data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
- data/lib/rigor/inference/method_dispatcher.rb +155 -14
- data/lib/rigor/inference/method_parameter_binder.rb +3 -3
- data/lib/rigor/inference/mutation_widening.rb +244 -174
- data/lib/rigor/inference/narrowing.rb +331 -27
- data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
- data/lib/rigor/inference/pre_eval_constants.rb +15 -0
- data/lib/rigor/inference/precision_scanner.rb +5 -2
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +89 -39
- data/lib/rigor/inference/scope_indexer.rb +1493 -140
- data/lib/rigor/inference/statement_evaluator.rb +276 -91
- data/lib/rigor/inference/struct_fold_safety.rb +186 -27
- data/lib/rigor/inference/version_guard.rb +229 -0
- data/lib/rigor/language_server/selection_range_provider.rb +1 -1
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/inflector.rb +14 -5
- data/lib/rigor/plugin/io_boundary.rb +107 -4
- data/lib/rigor/plugin/loader.rb +14 -0
- data/lib/rigor/protection/analysis_guard.rb +60 -0
- data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
- data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
- data/lib/rigor/protection/discovery_seed.rb +1 -1
- data/lib/rigor/protection/measurement_integrity.rb +36 -0
- data/lib/rigor/protection/mutation_scanner.rb +61 -6
- data/lib/rigor/protection/mutator.rb +121 -18
- data/lib/rigor/reflection.rb +182 -30
- data/lib/rigor/scope/discovery_index.rb +39 -1
- data/lib/rigor/scope.rb +385 -4
- data/lib/rigor/sig_gen/classification.rb +12 -1
- data/lib/rigor/sig_gen/generator.rb +254 -5
- data/lib/rigor/sig_gen/renderer.rb +1 -6
- data/lib/rigor/sig_gen/writer.rb +3 -0
- data/lib/rigor/source/constant_path.rb +79 -2
- data/lib/rigor/triage/catalogue.rb +1 -1
- data/lib/rigor/type/combinator.rb +10 -0
- data/lib/rigor/type/maybe.rb +47 -0
- data/lib/rigor/type/refined.rb +1 -2
- data/lib/rigor/type/result.rb +53 -0
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
- data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
- data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
- data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
- data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
- data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
- data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
- data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
- data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
- data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
- data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
- data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
- data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
- data/sig/rigor/analysis/baseline.rbs +1 -1
- data/sig/rigor/environment.rbs +3 -0
- data/sig/rigor/inference.rbs +4 -1
- data/sig/rigor/plugin/io_boundary.rbs +2 -0
- data/sig/rigor/reflection.rbs +3 -1
- data/sig/rigor/scope.rbs +25 -1
- data/sig/rigor/type.rbs +95 -1
- data/sig/rigor.rbs +16 -0
- metadata +69 -3
|
@@ -7,17 +7,46 @@
|
|
|
7
7
|
# which together account for the bulk of the Rails-extension long
|
|
8
8
|
# tail.
|
|
9
9
|
#
|
|
10
|
-
# This file is opt-in: enable by
|
|
11
|
-
# to `.rigor.yml`'s `signature_paths:`:
|
|
10
|
+
# This file is opt-in: enable it by naming the PLUGIN in `.rigor.yml`:
|
|
12
11
|
#
|
|
13
12
|
# # .rigor.yml
|
|
14
|
-
#
|
|
15
|
-
# -
|
|
16
|
-
#
|
|
13
|
+
# plugins:
|
|
14
|
+
# - rigor-activesupport-core-ext
|
|
15
|
+
#
|
|
16
|
+
# Do NOT wire it by pointing `signature_paths:` at this directory. The
|
|
17
|
+
# ADR-72 activesupport gem overlay (`data/gem_overlay/activesupport/`)
|
|
18
|
+
# stands down only when this plugin's ID is in the plugin registry, so
|
|
19
|
+
# on a project whose Gemfile.lock locks activesupport a bare
|
|
20
|
+
# `signature_paths:` entry loads BOTH copies. `Time.current` then has
|
|
21
|
+
# two declarations, `RBS::DefinitionBuilder` raises
|
|
22
|
+
# `DuplicatedMethodDefinitionError`, and the whole `Time` class
|
|
23
|
+
# silently degrades to `Dynamic[top]` — every call into it, a real typo
|
|
24
|
+
# included, stops being witnessed. Same failure mode as #437, reached
|
|
25
|
+
# by a different route.
|
|
17
26
|
#
|
|
18
27
|
# Not exhaustive. ActiveSupport ships many more extensions across
|
|
19
|
-
# Hash / Array / String / Object /
|
|
20
|
-
#
|
|
28
|
+
# Hash / Array / String / Object / Integer / Module than this file
|
|
29
|
+
# covers; PRs welcome to extend. `Time`, `Date` and `DateTime` are the
|
|
30
|
+
# exceptions and are audited exhaustively rather than sampled — they
|
|
31
|
+
# are CORE classes, so RBS knows them fully and they are CLOSED, which
|
|
32
|
+
# makes an omission on one a false positive rather than a missed
|
|
33
|
+
# precision opportunity (`Time` in #658, `Date` / `DateTime` in #670).
|
|
34
|
+
# Twelve names are still left out on `Time` — measured against a real
|
|
35
|
+
# `require "active_support/all"`, ten instance and two singleton — and
|
|
36
|
+
# every one is an `alias_method` artefact of ActiveSupport's own
|
|
37
|
+
# operator overrides: the `plus_with{,out}_duration`,
|
|
38
|
+
# `minus_with{,out}_duration`, `minus_with{,out}_coercion`,
|
|
39
|
+
# `compare_with{,out}_coercion`, `eql_with{,out}_coercion` and
|
|
40
|
+
# `Time.at_with{,out}_coercion` pairs. `Date` and `DateTime` leave out
|
|
41
|
+
# the same six `:nodoc:` artefacts (`plus_with{,out}_duration`,
|
|
42
|
+
# `minus_with{,out}_duration`, `compare_with{,out}_coercion`). All are
|
|
43
|
+
# `:nodoc:` in the source and called by nothing outside ActiveSupport.
|
|
44
|
+
#
|
|
45
|
+
# What all three still leave out is the `Object`- and `Module`-level
|
|
46
|
+
# core_ext that reaches them because it reaches EVERY class —
|
|
47
|
+
# `to_param`, `to_query`, `deep_dup`, `mattr_accessor`, `delegate` and
|
|
48
|
+
# the rest. That is not a `Time` / `Date` / `DateTime` gap and patching
|
|
49
|
+
# it per closed class would be the wrong shape; issue #673 tracks it.
|
|
21
50
|
#
|
|
22
51
|
# ---------------------------------------------------------------
|
|
23
52
|
# Purity (ADR-103 WD10; docs/type-specification/effect-labels.md; issue #388)
|
|
@@ -153,6 +182,9 @@ class NilClass
|
|
|
153
182
|
def try: (*untyped) -> nil
|
|
154
183
|
%a{pure}
|
|
155
184
|
def try!: (*untyped) -> nil
|
|
185
|
+
%a{pure}
|
|
186
|
+
def duplicable?: () -> true
|
|
187
|
+
def instance_values: () -> Hash[String, untyped]
|
|
156
188
|
end
|
|
157
189
|
|
|
158
190
|
class TrueClass
|
|
@@ -300,6 +332,16 @@ class Integer
|
|
|
300
332
|
# …); the clock is read only later, by calling `.ago` / `.since` / etc.
|
|
301
333
|
# on the resulting Duration — attributed on `ActiveSupport::Duration` in
|
|
302
334
|
# `lib/rigor/plugin/activesupport_core_ext/effects.rb`, not here.
|
|
335
|
+
#
|
|
336
|
+
# The `untyped` return is DELIBERATE and stays for now (#534). The real
|
|
337
|
+
# answer is `ActiveSupport::Duration`, and the plugin supplies it from
|
|
338
|
+
# `DURATION_MULTIPLIERS`, whose contribution tier sits above RBS
|
|
339
|
+
# dispatch. `ActiveSupport::Duration` IS now declared in this bundle
|
|
340
|
+
# (further down, before `Time`), protected by the manifest's
|
|
341
|
+
# `open_receivers:` — so naming it here too is no longer blocked the way
|
|
342
|
+
# it was when #534 landed. Issue #660 tracks moving this return type
|
|
343
|
+
# into RBS to match; until then, what this declaration is for is
|
|
344
|
+
# making `1.day` RESOLVE, and the type itself still comes from the plugin.
|
|
303
345
|
%a{pure}
|
|
304
346
|
def second: () -> untyped # ActiveSupport::Duration
|
|
305
347
|
%a{pure}
|
|
@@ -429,6 +471,111 @@ class Float
|
|
|
429
471
|
def gigabytes: () -> Float
|
|
430
472
|
end
|
|
431
473
|
|
|
474
|
+
# ---------------------------------------------------------------
|
|
475
|
+
# ActiveSupport::Duration — issue #632
|
|
476
|
+
# ---------------------------------------------------------------
|
|
477
|
+
|
|
478
|
+
# The multiplier comment on `Integer#seconds` above explains why naming this class in RBS is normally the
|
|
479
|
+
# wrong move: an incomplete signature on a class whose real surface forwards to `method_missing` turns
|
|
480
|
+
# every omitted member into a false `call.undefined-method`. This declaration is the one place that trade
|
|
481
|
+
# is accepted anyway, because the manifest lists `ActiveSupport::Duration` under `open_receivers:` (ADR-26)
|
|
482
|
+
# — the same mechanism `rigor-activerecord` uses for `ActiveRecord::Relation` — so the rule never fires
|
|
483
|
+
# against a Duration receiver regardless of what this class does or doesn't declare. What follows is the
|
|
484
|
+
# reader surface audited against ActiveSupport 8.1.3.1's `lib/active_support/duration.rb`; every method not
|
|
485
|
+
# listed here (the arithmetic operators, `==`/`eql?`/`hash`/`<=>`, `to_s`/`inspect`, …) stays undeclared on
|
|
486
|
+
# purpose — some are already covered by the separate `dynamic_return` rule for `+`/`-`/`*` below, the rest
|
|
487
|
+
# are simply outside this issue's scope and remain silently `Dynamic` rather than a guess.
|
|
488
|
+
module ActiveSupport
|
|
489
|
+
class Duration
|
|
490
|
+
# The `ago` family (issue #659). `since(time = ::Time.current)` and `ago(time = ::Time.current)` are
|
|
491
|
+
# the only two real methods; `from_now` / `after` are `alias`es of the first and `until` / `before`
|
|
492
|
+
# of the second, so all six share one signature — checked, because a mis-transcribed alias arity is
|
|
493
|
+
# exactly the bug #658 found on `Time#at_beginning_of_week`.
|
|
494
|
+
#
|
|
495
|
+
# These were declared once before and REMOVED, and the reason they can return now is that the
|
|
496
|
+
# blocker was elsewhere: every FP-viable return routes into `Time`, and `Time` is a CORE Ruby class,
|
|
497
|
+
# RBS-known and therefore closed, whose Rails instance surface this bundle did not declare. Typing
|
|
498
|
+
# `ago` as `Time` while `to_fs` / `in_time_zone` / `past?` / `middle_of_day` were undeclared turned
|
|
499
|
+
# `1.hour.ago.to_fs(:db)` — ordinary Rails — into a false `call.undefined-method` on `Time`, nine per
|
|
500
|
+
# fixture, `to_fs` alone at ~92 corpus sites. `open_receivers: ["ActiveSupport::Duration"]` does not
|
|
501
|
+
# help there: it protects a Duration receiver, and the diagnostic lands on the `Time` the call
|
|
502
|
+
# RETURNS. #658 declared that surface and #670 the `Date` / `DateTime` twins, so the chains resolve.
|
|
503
|
+
#
|
|
504
|
+
# `() -> Time` is the correct class, not a compromise. Under a zone these answer an
|
|
505
|
+
# `ActiveSupport::TimeWithZone` (verified by calling them), and Rails overrides `TimeWithZone#is_a?`
|
|
506
|
+
# to answer true for `::Time` — so `Time` is the class Rails ITSELF tells callers the value is, and
|
|
507
|
+
# `Time.current` above already answers `Time` for the same reason. A `Time | TimeWithZone` union was
|
|
508
|
+
# measured and rejected: it draws no `undefined-method`, but types every downstream call
|
|
509
|
+
# `Dynamic[top]` (`iso8601`, `beginning_of_day`, `to_date`, `- 1.day`), which is worth no more than
|
|
510
|
+
# leaving them undeclared while still firing a parameter mismatch on the TimeWithZone arm.
|
|
511
|
+
#
|
|
512
|
+
# The with-an-argument form stays `(untyped) -> untyped` on purpose, and this is not caution: the
|
|
513
|
+
# runtime return depends on the argument's class AND on whether the duration carries a date-scale
|
|
514
|
+
# part, because `sum` dispatches per part to `advance` for date scales and to `since` for clock
|
|
515
|
+
# scales. Measured — `(1.month).ago(Date.today)` answers a `Date` where `(30.minutes).ago(Date.today)`
|
|
516
|
+
# answers a `TimeWithZone`, and `(1.day).ago(DateTime.now)` answers a `DateTime`. No overload
|
|
517
|
+
# expresses that without guessing, and a guess here is a false positive on correct code.
|
|
518
|
+
#
|
|
519
|
+
# NONE are `%a{pure}`: all six read the clock (and the zone) through the default argument, which is
|
|
520
|
+
# what stops `created_at > 7.days.ago` folding to a constant. They are already attributed `nondet.time`
|
|
521
|
+
# in `lib/rigor/plugin/activesupport_core_ext/effects.rb` — annotating them here would duplicate an
|
|
522
|
+
# envelope the manifest already owns, and risk the two disagreeing.
|
|
523
|
+
def ago: () -> Time
|
|
524
|
+
| (untyped time) -> untyped
|
|
525
|
+
def until: () -> Time
|
|
526
|
+
| (untyped time) -> untyped
|
|
527
|
+
def before: () -> Time
|
|
528
|
+
| (untyped time) -> untyped
|
|
529
|
+
def since: () -> Time
|
|
530
|
+
| (untyped time) -> untyped
|
|
531
|
+
def from_now: () -> Time
|
|
532
|
+
| (untyped time) -> untyped
|
|
533
|
+
def after: () -> Time
|
|
534
|
+
| (untyped time) -> untyped
|
|
535
|
+
|
|
536
|
+
# `to_i` returns `@value.to_i` — always Integer, whether `@value` itself is Integer (`1.day`) or Float
|
|
537
|
+
# (`1.5.days`). `in_seconds` is a literal alias.
|
|
538
|
+
%a{pure}
|
|
539
|
+
def to_i: () -> Integer
|
|
540
|
+
%a{pure}
|
|
541
|
+
def in_seconds: () -> Integer
|
|
542
|
+
|
|
543
|
+
# `to_f` is a real `ActiveSupport::Delegation.generate` accessor (defined at class-load time, not
|
|
544
|
+
# `method_missing`) — `@value.to_f` is always Float.
|
|
545
|
+
%a{pure}
|
|
546
|
+
def to_f: () -> Float
|
|
547
|
+
|
|
548
|
+
# `in_minutes` / `in_hours` / `in_days` / `in_weeks` / `in_months` / `in_years` are each
|
|
549
|
+
# `in_seconds / SECONDS_PER_*.to_f` — Integer divided by Float is always Float.
|
|
550
|
+
%a{pure}
|
|
551
|
+
def in_minutes: () -> Float
|
|
552
|
+
%a{pure}
|
|
553
|
+
def in_hours: () -> Float
|
|
554
|
+
%a{pure}
|
|
555
|
+
def in_days: () -> Float
|
|
556
|
+
%a{pure}
|
|
557
|
+
def in_weeks: () -> Float
|
|
558
|
+
%a{pure}
|
|
559
|
+
def in_months: () -> Float
|
|
560
|
+
%a{pure}
|
|
561
|
+
def in_years: () -> Float
|
|
562
|
+
|
|
563
|
+
# `iso8601(precision: nil)` delegates to `ISO8601Serializer#serialize`, which always returns a String.
|
|
564
|
+
%a{pure}
|
|
565
|
+
def iso8601: (?precision: Integer?) -> String
|
|
566
|
+
|
|
567
|
+
# `parts` returns a `dup` of the internal parts hash (`{years:, months:, weeks:, days:, hours:,
|
|
568
|
+
# minutes:, seconds:}`), Integer-valued for a duration built from an Integer multiplier but real
|
|
569
|
+
# runtime Float for a fractional one (`1.5.seconds.parts == {seconds: 1.5}`) — `Hash[Symbol, Integer]`
|
|
570
|
+
# was reviewed and rejected: a `Float`-valued `:seconds` entry read against that type would answer
|
|
571
|
+
# `Integer`, and `parts[:seconds].nan?` on it would fire a false `call.undefined-method`, silent on
|
|
572
|
+
# master where `parts` was undeclared. `Integer | Float` costs nothing on the common `parts[:days] * 2`
|
|
573
|
+
# chain (both arms support `*`) and stays honest on the fractional one.
|
|
574
|
+
%a{pure}
|
|
575
|
+
def parts: () -> Hash[Symbol, Integer | Float]
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
|
|
432
579
|
# ---------------------------------------------------------------
|
|
433
580
|
# Time — singleton + instance extensions
|
|
434
581
|
# ---------------------------------------------------------------
|
|
@@ -441,6 +588,34 @@ class Time
|
|
|
441
588
|
def self.zone: () -> untyped # ActiveSupport::TimeZone | nil
|
|
442
589
|
def self.zone=: (String | Symbol | untyped) -> untyped
|
|
443
590
|
|
|
591
|
+
# Issue #658, `core_ext/time/zones.rb` + `core_ext/time/calculations.rb`. The rest of the
|
|
592
|
+
# singleton surface those two files add, none of it typed by rbs.
|
|
593
|
+
#
|
|
594
|
+
# `zone_default` is the `attr_accessor` behind `Time.zone`'s fallback; `find_zone!` raises on a
|
|
595
|
+
# bad zone and `find_zone` rescues to `nil`; `use_zone` swaps the zone around a block and returns
|
|
596
|
+
# the block's value. All four answer or accept an `ActiveSupport::TimeZone`, which this bundle
|
|
597
|
+
# does not declare, so they stay `untyped` rather than guessing a carrier.
|
|
598
|
+
#
|
|
599
|
+
# NONE of them are `%a{pure}`. `zone_default` / `zone_default=` / `use_zone` are process-state
|
|
600
|
+
# reads and writes, recorded in the plugin manifest next to `Time.zone` / `Time.zone=`.
|
|
601
|
+
# `days_in_month` / `days_in_year` default their `year` to `current.year`, so an omitted argument
|
|
602
|
+
# reads the clock — recorded there too. `find_zone!` / `find_zone` read the memoised
|
|
603
|
+
# `ActiveSupport::TimeZone` registry and are left unannotated rather than claimed either way.
|
|
604
|
+
def self.zone_default: () -> untyped
|
|
605
|
+
def self.zone_default=: (untyped) -> untyped
|
|
606
|
+
def self.use_zone: (untyped time_zone) { () -> untyped } -> untyped
|
|
607
|
+
def self.find_zone!: (untyped time_zone) -> untyped
|
|
608
|
+
def self.find_zone: (untyped time_zone) -> untyped
|
|
609
|
+
# `COMMON_YEAR_DAYS_IN_MONTH[month]` answers `nil` for a month outside 1..12, but the declared
|
|
610
|
+
# return stays `Integer`: `Integer?` would make `Time.days_in_month(2, 2024) - 1` — the way every
|
|
611
|
+
# real call site uses it — a nil-arithmetic diagnostic, which is the more expensive error of the
|
|
612
|
+
# two on correct code.
|
|
613
|
+
def self.days_in_month: (Integer month, ?Integer year) -> Integer
|
|
614
|
+
def self.days_in_year: (?Integer year) -> Integer
|
|
615
|
+
# `Time.rfc3339(str)` builds a `Time` from the parsed parts, raising `ArgumentError` when the
|
|
616
|
+
# time or offset component is missing. Distinct from the `rfc3339` INSTANCE alias below.
|
|
617
|
+
def self.rfc3339: (String str) -> Time
|
|
618
|
+
|
|
444
619
|
# `core_ext/time/conversions` — nothing to declare. `Time.parse`, `Time.httpdate` and
|
|
445
620
|
# `Time.iso8601` all come from stdlib `time`, which rbs types; ActiveSupport only re-opens them.
|
|
446
621
|
# Declaring them here raised `RBS::DuplicatedMethodDefinitionError` and collapsed the whole `Time`
|
|
@@ -497,9 +672,11 @@ class Time
|
|
|
497
672
|
%a{pure}
|
|
498
673
|
def at_end_of_day: () -> Time
|
|
499
674
|
# NOT %a{pure}: same `Date.beginning_of_week` default-argument read as
|
|
500
|
-
# `beginning_of_week` / `end_of_week` above.
|
|
501
|
-
|
|
502
|
-
|
|
675
|
+
# `beginning_of_week` / `end_of_week` above. They are `alias`es of those two in
|
|
676
|
+
# `DateAndTime::Calculations`, so they take the same optional `start_day` — declaring them
|
|
677
|
+
# zero-arity made `time.at_beginning_of_week(:sunday)`, correct Rails, an arity diagnostic.
|
|
678
|
+
def at_beginning_of_week: (?Symbol start_day) -> Time
|
|
679
|
+
def at_end_of_week: (?Symbol start_day) -> Time
|
|
503
680
|
%a{pure}
|
|
504
681
|
def at_midnight: () -> Time
|
|
505
682
|
%a{pure}
|
|
@@ -521,6 +698,223 @@ class Time
|
|
|
521
698
|
def advance: (untyped options) -> Time
|
|
522
699
|
%a{pure}
|
|
523
700
|
def all_day: () -> Range[Time]
|
|
701
|
+
|
|
702
|
+
# -------------------------------------------------------------
|
|
703
|
+
# Issue #658 — the rest of the Rails `Time` INSTANCE surface
|
|
704
|
+
# -------------------------------------------------------------
|
|
705
|
+
#
|
|
706
|
+
# `Time` is a CORE class, fully RBS-known from `core/time.rbs`, and therefore CLOSED: a name this
|
|
707
|
+
# file does not declare is reported `call.undefined-method`. So on a closed class an OMISSION is a
|
|
708
|
+
# false positive on correct Rails code just as much as a wrong return type is, and there is no
|
|
709
|
+
# gradual middle — making `Time` an open receiver would suppress the genuine diagnostic
|
|
710
|
+
# project-wide. Everything below was therefore audited name by name against the vendored
|
|
711
|
+
# activesupport-8.1.3.1 sources rather than recalled: `core_ext/date_and_time/calculations.rb`
|
|
712
|
+
# (the module `Time`, `Date` and `DateTime` all include), `core_ext/time/calculations.rb`,
|
|
713
|
+
# `core_ext/time/conversions.rb`, `core_ext/date_and_time/zones.rb` and
|
|
714
|
+
# `core_ext/date_and_time/compatibility.rb`.
|
|
715
|
+
#
|
|
716
|
+
# Every row was also checked against the assembled environment (`DEFAULT_LIBRARIES` + this `sig/`)
|
|
717
|
+
# for a name rbs already declares, because a second full declaration of one raises
|
|
718
|
+
# `RBS::DuplicatedMethodDefinitionError` and collapses the WHOLE class to `Dynamic[top]` — the
|
|
719
|
+
# #437 failure the `Date#to_time` note below records. That is why `to_time`, `xmlschema`,
|
|
720
|
+
# `localtime`, `getlocal`, `utc`, `getutc`, `utc?`, `utc_offset`, `usec`, `nsec`, `subsec`, `to_f`
|
|
721
|
+
# and `to_i` are absent here even though ActiveSupport re-opens some of them: rbs types them already.
|
|
722
|
+
|
|
723
|
+
# `core_ext/date_and_time/calculations.rb` — the shared module. Each of these is `advance(...)` /
|
|
724
|
+
# `change(...)` on the receiver, so on a `Time` receiver each answers `Time`.
|
|
725
|
+
#
|
|
726
|
+
# NOT %a{pure}: the five predicates below compare against the CLOCK — `today?` / `tomorrow?` /
|
|
727
|
+
# `yesterday?` read `::Date.current`, and `past?` / `future?` read `self.class.current`, which is
|
|
728
|
+
# `Time.current` and therefore reads `Time.zone` as well.
|
|
729
|
+
def today?: () -> bool
|
|
730
|
+
def tomorrow?: () -> bool
|
|
731
|
+
def next_day?: () -> bool
|
|
732
|
+
def yesterday?: () -> bool
|
|
733
|
+
def prev_day?: () -> bool
|
|
734
|
+
def past?: () -> bool
|
|
735
|
+
def future?: () -> bool
|
|
736
|
+
# `WEEKEND_DAYS.include?(wday)` — the receiver's own field and a frozen constant.
|
|
737
|
+
%a{pure}
|
|
738
|
+
def on_weekend?: () -> bool
|
|
739
|
+
%a{pure}
|
|
740
|
+
def on_weekday?: () -> bool
|
|
741
|
+
# NOT %a{pure}: `self < date_or_time` goes through ActiveSupport's own `<=>` override
|
|
742
|
+
# (`compare_with_coercion`), which calls `comparable_time` / `to_time` on the ARGUMENT — a
|
|
743
|
+
# dispatch on a value whose real class this audit cannot see, the `try` problem the file header
|
|
744
|
+
# calls out. The parameter is `untyped` for the same reason: `Date`, `DateTime`,
|
|
745
|
+
# `ActiveSupport::TimeWithZone` and `Time` are all accepted at runtime.
|
|
746
|
+
def before?: (untyped date_or_time) -> bool
|
|
747
|
+
def after?: (untyped date_or_time) -> bool
|
|
748
|
+
# `days_ago(n)` … `years_since(n)` are all `advance(unit => ±n)`; the argument is `Numeric` and
|
|
749
|
+
# not `Integer` because `advance` divmods a fractional `days:` / `weeks:` on purpose.
|
|
750
|
+
%a{pure}
|
|
751
|
+
def days_ago: (Numeric days) -> Time
|
|
752
|
+
%a{pure}
|
|
753
|
+
def days_since: (Numeric days) -> Time
|
|
754
|
+
%a{pure}
|
|
755
|
+
def weeks_ago: (Numeric weeks) -> Time
|
|
756
|
+
%a{pure}
|
|
757
|
+
def weeks_since: (Numeric weeks) -> Time
|
|
758
|
+
%a{pure}
|
|
759
|
+
def months_ago: (Numeric months) -> Time
|
|
760
|
+
%a{pure}
|
|
761
|
+
def months_since: (Numeric months) -> Time
|
|
762
|
+
%a{pure}
|
|
763
|
+
def years_ago: (Numeric years) -> Time
|
|
764
|
+
%a{pure}
|
|
765
|
+
def years_since: (Numeric years) -> Time
|
|
766
|
+
# The `at_`-prefixed spellings are `alias`es of the already-declared `beginning_of_*` /
|
|
767
|
+
# `end_of_*`, and inherit their purity verdict one for one.
|
|
768
|
+
%a{pure}
|
|
769
|
+
def at_beginning_of_month: () -> Time
|
|
770
|
+
%a{pure}
|
|
771
|
+
def at_beginning_of_year: () -> Time
|
|
772
|
+
%a{pure}
|
|
773
|
+
def at_end_of_month: () -> Time
|
|
774
|
+
%a{pure}
|
|
775
|
+
def at_end_of_year: () -> Time
|
|
776
|
+
%a{pure}
|
|
777
|
+
def beginning_of_quarter: () -> Time
|
|
778
|
+
%a{pure}
|
|
779
|
+
def at_beginning_of_quarter: () -> Time
|
|
780
|
+
%a{pure}
|
|
781
|
+
def end_of_quarter: () -> Time
|
|
782
|
+
%a{pure}
|
|
783
|
+
def at_end_of_quarter: () -> Time
|
|
784
|
+
# `(month / 3.0).ceil` — `Float#ceil` with no argument is Integer.
|
|
785
|
+
%a{pure}
|
|
786
|
+
def quarter: () -> Integer
|
|
787
|
+
# NOT %a{pure}: `next_week` / `prev_week` / `last_week` / `days_to_week_start` / `all_week`
|
|
788
|
+
# default their `start_day` to `Date.beginning_of_week`, the fiber-local config read
|
|
789
|
+
# (`ActiveSupport::IsolatedExecutionState[:beginning_of_week]`) that already keeps
|
|
790
|
+
# `beginning_of_week` / `end_of_week` above unannotated. `next_weekday` joins them because it
|
|
791
|
+
# calls `next_week(:monday, same_time: true)`, whose body reaches `beginning_of_week` with NO
|
|
792
|
+
# argument and `days_span`, both of which read the same config.
|
|
793
|
+
def next_week: (?Symbol given_day_in_next_week, ?same_time: bool) -> Time
|
|
794
|
+
def prev_week: (?Symbol start_day, ?same_time: bool) -> Time
|
|
795
|
+
def last_week: (?Symbol start_day, ?same_time: bool) -> Time
|
|
796
|
+
def next_weekday: () -> Time
|
|
797
|
+
def days_to_week_start: (?Symbol start_day) -> Integer
|
|
798
|
+
def all_week: (?Symbol start_day) -> Range[Time]
|
|
799
|
+
# `prev_weekday` / `last_weekday` DO stay pure, and the asymmetry with `next_weekday` above is
|
|
800
|
+
# real rather than an oversight: the body is `prev_day.on_weekend? ? copy_time_to(
|
|
801
|
+
# beginning_of_week(:friday)) : prev_day`, and `:friday` is passed explicitly, so no default
|
|
802
|
+
# argument ever reaches `Date.beginning_of_week`.
|
|
803
|
+
%a{pure}
|
|
804
|
+
def prev_weekday: () -> Time
|
|
805
|
+
%a{pure}
|
|
806
|
+
def last_weekday: () -> Time
|
|
807
|
+
# `monday` / `sunday` likewise name their start day explicitly (`beginning_of_week(:monday)` /
|
|
808
|
+
# `end_of_week(:monday)`), so unlike their zero-argument parents they read no config.
|
|
809
|
+
%a{pure}
|
|
810
|
+
def monday: () -> Time
|
|
811
|
+
%a{pure}
|
|
812
|
+
def sunday: () -> Time
|
|
813
|
+
%a{pure}
|
|
814
|
+
def next_quarter: () -> Time
|
|
815
|
+
%a{pure}
|
|
816
|
+
def prev_quarter: () -> Time
|
|
817
|
+
%a{pure}
|
|
818
|
+
def last_quarter: () -> Time
|
|
819
|
+
%a{pure}
|
|
820
|
+
def last_month: () -> Time
|
|
821
|
+
%a{pure}
|
|
822
|
+
def last_year: () -> Time
|
|
823
|
+
%a{pure}
|
|
824
|
+
def all_month: () -> Range[Time]
|
|
825
|
+
%a{pure}
|
|
826
|
+
def all_quarter: () -> Range[Time]
|
|
827
|
+
%a{pure}
|
|
828
|
+
def all_year: () -> Range[Time]
|
|
829
|
+
%a{pure}
|
|
830
|
+
def next_occurring: (Symbol day_of_week) -> Time
|
|
831
|
+
%a{pure}
|
|
832
|
+
def prev_occurring: (Symbol day_of_week) -> Time
|
|
833
|
+
|
|
834
|
+
# `core_ext/time/calculations.rb` — the `Time`-only half.
|
|
835
|
+
#
|
|
836
|
+
# `seconds_since_midnight` ends in `+ (usec / 1.0e+6)`, so it is Float even at exactly midnight;
|
|
837
|
+
# `seconds_until_end_of_day` is `end_of_day.to_i - to_i`, Integer. `sec_fraction` is a plain alias
|
|
838
|
+
# of `subsec`, and carries `subsec`'s own core type rather than a guessed `Rational` — CRuby
|
|
839
|
+
# answers the Integer `0` for a whole-second receiver.
|
|
840
|
+
%a{pure}
|
|
841
|
+
def seconds_since_midnight: () -> Float
|
|
842
|
+
%a{pure}
|
|
843
|
+
def seconds_until_end_of_day: () -> Integer
|
|
844
|
+
%a{pure}
|
|
845
|
+
def sec_fraction: () -> (0 | Rational)
|
|
846
|
+
# `middle_of_day` is `change(hour: 12)`, and `midday` / `noon` / `at_noon` (already declared
|
|
847
|
+
# above) plus `at_midday` / `at_middle_of_day` are its aliases.
|
|
848
|
+
%a{pure}
|
|
849
|
+
def middle_of_day: () -> Time
|
|
850
|
+
%a{pure}
|
|
851
|
+
def at_midday: () -> Time
|
|
852
|
+
%a{pure}
|
|
853
|
+
def at_middle_of_day: () -> Time
|
|
854
|
+
%a{pure}
|
|
855
|
+
def at_beginning_of_hour: () -> Time
|
|
856
|
+
%a{pure}
|
|
857
|
+
def at_end_of_hour: () -> Time
|
|
858
|
+
%a{pure}
|
|
859
|
+
def at_beginning_of_minute: () -> Time
|
|
860
|
+
%a{pure}
|
|
861
|
+
def at_end_of_minute: () -> Time
|
|
862
|
+
# `Time#prev_day` … `#next_year` are `Time`'s OWN redefinitions (`advance(days: -days)` and
|
|
863
|
+
# friends), taking an optional count — not the zero-argument `Date` methods of the same name that
|
|
864
|
+
# rbs's `stdlib/date` types. rbs does not declare them on `Time`, so these are additions.
|
|
865
|
+
%a{pure}
|
|
866
|
+
def prev_day: (?Numeric days) -> Time
|
|
867
|
+
%a{pure}
|
|
868
|
+
def next_day: (?Numeric days) -> Time
|
|
869
|
+
%a{pure}
|
|
870
|
+
def prev_month: (?Numeric months) -> Time
|
|
871
|
+
%a{pure}
|
|
872
|
+
def next_month: (?Numeric months) -> Time
|
|
873
|
+
%a{pure}
|
|
874
|
+
def prev_year: (?Numeric years) -> Time
|
|
875
|
+
%a{pure}
|
|
876
|
+
def next_year: (?Numeric years) -> Time
|
|
877
|
+
|
|
878
|
+
# `core_ext/time/conversions.rb`.
|
|
879
|
+
#
|
|
880
|
+
# NOT %a{pure}: `to_fs` / `to_formatted_s` look the format up in `Time::DATE_FORMATS`, a mutable
|
|
881
|
+
# process-global hash that applications are documented to extend from an initializer, and a
|
|
882
|
+
# custom entry may be an arbitrary lambda that is then CALLED. That is the same reason
|
|
883
|
+
# `Array#to_fs` is unannotated above. Both always answer a String, whatever the formatter
|
|
884
|
+
# returns: the lambda branch ends in `.to_s` and the fallback is `to_s`.
|
|
885
|
+
def to_fs: (?Symbol format) -> String
|
|
886
|
+
def to_formatted_s: (?Symbol format) -> String
|
|
887
|
+
# `formatted_offset` is `utc? && alternate_utc_string || ActiveSupport::TimeZone
|
|
888
|
+
# .seconds_to_utc_offset(utc_offset, colon)`, and `seconds_to_utc_offset` is a `format %` over
|
|
889
|
+
# its two arguments — pure, and String in both branches. `colon` is `boolish` and not `bool`:
|
|
890
|
+
# the body is `colon ? WITH_COLON : WITHOUT_COLON`, plain truthiness, so `bool` would make
|
|
891
|
+
# `t.formatted_offset(1)` a `call.argument-type-mismatch` on code that runs.
|
|
892
|
+
# `alternate_utc_string` is `untyped` because it is returned as-is when the receiver is UTC and
|
|
893
|
+
# callers pass whatever they want to print there.
|
|
894
|
+
%a{pure}
|
|
895
|
+
def formatted_offset: (?boolish colon, ?untyped alternate_utc_string) -> String
|
|
896
|
+
# `rfc3339` is `alias_method :rfc3339, :xmlschema`, so it takes `xmlschema`'s optional
|
|
897
|
+
# `fraction_digits`. rbs types `Time#xmlschema` but not `Time#rfc3339`, so this is an addition
|
|
898
|
+
# rather than a collision.
|
|
899
|
+
%a{pure}
|
|
900
|
+
def rfc3339: (?Integer fraction_digits) -> String
|
|
901
|
+
|
|
902
|
+
# `core_ext/date_and_time/zones.rb`. Answers an `ActiveSupport::TimeWithZone`, which nothing in
|
|
903
|
+
# this bundle declares, so the return is `untyped` rather than a guess — the same answer
|
|
904
|
+
# `DateTime#in_time_zone` gives below. The parameter is `untyped` and not `String | Symbol`
|
|
905
|
+
# because `Time.find_zone!` also accepts an `ActiveSupport::TimeZone`, a `TZInfo::Timezone`, and
|
|
906
|
+
# an offset `Duration` (`Time.find_zone! -5.hours` is the documented spelling).
|
|
907
|
+
#
|
|
908
|
+
# NOT %a{pure}: `in_time_zone(zone = ::Time.zone)` reads `Time.zone` through its own default
|
|
909
|
+
# argument. The label is `global.read` alone and it is recorded in the plugin manifest
|
|
910
|
+
# (`lib/rigor/plugin/activesupport_core_ext/effects.rb`), next to the identical `DateTime` row.
|
|
911
|
+
def in_time_zone: (?untyped zone) -> untyped
|
|
912
|
+
|
|
913
|
+
# `core_ext/date_and_time/compatibility.rb` — `mattr_accessor :utc_to_local_returns_utc_offset_times,
|
|
914
|
+
# instance_writer: false` defines an instance READER on every class that includes the module.
|
|
915
|
+
# Rarely called on an instance, but it is public and `Time` is closed, so the omission would be
|
|
916
|
+
# the false positive. NOT %a{pure}: it reads process-global configuration.
|
|
917
|
+
def utc_to_local_returns_utc_offset_times: () -> bool
|
|
524
918
|
end
|
|
525
919
|
|
|
526
920
|
# ---------------------------------------------------------------
|
|
@@ -528,20 +922,22 @@ end
|
|
|
528
922
|
# ---------------------------------------------------------------
|
|
529
923
|
|
|
530
924
|
class Date
|
|
531
|
-
#
|
|
532
|
-
# already attributed nondet.time/global.read by
|
|
533
|
-
#
|
|
534
|
-
#
|
|
535
|
-
#
|
|
925
|
+
# These three are the WHOLE date-constructing singleton surface `core_ext/date/calculations.rb`'s
|
|
926
|
+
# `class << self` adds. NOT %a{pure}: all three are already attributed nondet.time/global.read by
|
|
927
|
+
# `effects.rb` (#387).
|
|
928
|
+
#
|
|
929
|
+
# Five more used to be declared here — `self.end_of_week`, `self.beginning_of_month`,
|
|
930
|
+
# `self.end_of_month`, `self.beginning_of_year`, `self.end_of_year` — and none of them exist
|
|
931
|
+
# (#762, re-verified against activesupport 8.1.3.1 by `Date.respond_to?`). `Date` only INCLUDES
|
|
932
|
+
# `DateAndTime::Calculations`, so that module's members are instance methods, declared below and
|
|
933
|
+
# nowhere else. Declaring them as singletons made `Date.end_of_month` — a real `NoMethodError` —
|
|
934
|
+
# type-check and answer a `Date`, so everything chained off it was checked against a value that
|
|
935
|
+
# never exists; `DateTime` inherited all five through its own singleton and was wrong the same way.
|
|
936
|
+
# `self.beginning_of_week` is not here either: it exists, but it is the week-start CONFIGURATION
|
|
937
|
+
# reader and is declared with the rest of that group at the bottom of this class.
|
|
536
938
|
def self.current: () -> Date
|
|
537
939
|
def self.yesterday: () -> Date
|
|
538
940
|
def self.tomorrow: () -> Date
|
|
539
|
-
def self.beginning_of_week: () -> Date
|
|
540
|
-
def self.end_of_week: () -> Date
|
|
541
|
-
def self.beginning_of_month: () -> Date
|
|
542
|
-
def self.end_of_month: () -> Date
|
|
543
|
-
def self.beginning_of_year: () -> Date
|
|
544
|
-
def self.end_of_year: () -> Date
|
|
545
941
|
|
|
546
942
|
# `advance(days: -1)` / `advance(days: 1)` — pure y/m/d arithmetic on the
|
|
547
943
|
# receiver (`core_ext/date_and_time/calculations.rb`), unlike the
|
|
@@ -615,6 +1011,232 @@ class Date
|
|
|
615
1011
|
# environment that loads a plugin's `signature_paths`.
|
|
616
1012
|
%a{pure}
|
|
617
1013
|
def to_time: (?Symbol form) -> Time | ...
|
|
1014
|
+
|
|
1015
|
+
# -------------------------------------------------------------
|
|
1016
|
+
# Issue #670 — the rest of the Rails `Date` INSTANCE surface
|
|
1017
|
+
# -------------------------------------------------------------
|
|
1018
|
+
#
|
|
1019
|
+
# `Date` is CLOSED for the same reason `Time` is (rbs's `stdlib/date` types it fully), so an
|
|
1020
|
+
# OMISSION here is a false positive on correct Rails code exactly as much as a wrong return type
|
|
1021
|
+
# is. Audited name by name against the vendored activesupport-8.1.3.1 sources —
|
|
1022
|
+
# `core_ext/date_and_time/calculations.rb`, `core_ext/date/calculations.rb`,
|
|
1023
|
+
# `core_ext/date/conversions.rb`, `core_ext/date_and_time/zones.rb` — and every RETURN was then
|
|
1024
|
+
# confirmed by CALLING it on a real `Date` receiver, not read off the body: the `first_hour` /
|
|
1025
|
+
# `last_hour` helpers in the shared module branch on `acts_like?(:time)`, which is false for `Date`
|
|
1026
|
+
# and true for `DateTime`, so the same method name answers a different class on each.
|
|
1027
|
+
#
|
|
1028
|
+
# Every row was checked against the assembled environment (`DEFAULT_LIBRARIES` + this `sig/`) for a
|
|
1029
|
+
# name rbs already declares on `Date` itself, because a second full declaration raises
|
|
1030
|
+
# `RBS::DuplicatedMethodDefinitionError` and collapses the WHOLE class to `Dynamic[top]` (#437).
|
|
1031
|
+
# Zero collided. That is why `xmlschema` is absent even though ActiveSupport re-opens it (rbs types
|
|
1032
|
+
# it, at the same `() -> String`), and why `inspect` is absent even though `readable_inspect`
|
|
1033
|
+
# replaces it.
|
|
1034
|
+
#
|
|
1035
|
+
# Left undeclared on purpose: the six `:nodoc:` alias-chain internals ActiveSupport's own operator
|
|
1036
|
+
# overrides leave behind (`plus_with{,out}_duration`, `minus_with{,out}_duration`,
|
|
1037
|
+
# `compare_with{,out}_coercion`) — measured by diffing the runtime method table against the
|
|
1038
|
+
# assembled RBS environment, the same way #658 measured `Time`'s twelve. Also undeclared: the
|
|
1039
|
+
# `Object`- and `Module`-level core_ext that lands on `Date` because it lands on EVERY class
|
|
1040
|
+
# (`to_param`, `to_query`, `deep_dup`, `mattr_accessor`, `delegate`, …). Those are not a `Date`
|
|
1041
|
+
# gap and fixing them here would be patchwork — issue #673 tracks them.
|
|
1042
|
+
|
|
1043
|
+
# `core_ext/date_and_time/calculations.rb` — the module `Date`, `DateTime` and `Time` all include.
|
|
1044
|
+
#
|
|
1045
|
+
# NOT %a{pure}: the five predicates below compare against the CLOCK — `today?` / `tomorrow?` /
|
|
1046
|
+
# `yesterday?` read `::Date.current`, and `past?` / `future?` read `self.class.current`, which
|
|
1047
|
+
# reads `Time.zone` as well.
|
|
1048
|
+
def today?: () -> bool
|
|
1049
|
+
def tomorrow?: () -> bool
|
|
1050
|
+
def next_day?: () -> bool
|
|
1051
|
+
def yesterday?: () -> bool
|
|
1052
|
+
def prev_day?: () -> bool
|
|
1053
|
+
def past?: () -> bool
|
|
1054
|
+
def future?: () -> bool
|
|
1055
|
+
# `WEEKEND_DAYS.include?(wday)` — the receiver's own field and a frozen constant.
|
|
1056
|
+
%a{pure}
|
|
1057
|
+
def on_weekend?: () -> bool
|
|
1058
|
+
%a{pure}
|
|
1059
|
+
def on_weekday?: () -> bool
|
|
1060
|
+
# NOT %a{pure}: `self < date_or_time` goes through ActiveSupport's own `<=>` override
|
|
1061
|
+
# (`compare_with_coercion`), which calls `to_datetime` on the ARGUMENT — a dispatch on a value
|
|
1062
|
+
# whose real class this audit cannot see, the `try` problem the file header calls out. The
|
|
1063
|
+
# parameter is `untyped` for the same reason: `Date`, `Time`, `DateTime` and
|
|
1064
|
+
# `ActiveSupport::TimeWithZone` are all accepted at runtime.
|
|
1065
|
+
def before?: (untyped date_or_time) -> bool
|
|
1066
|
+
def after?: (untyped date_or_time) -> bool
|
|
1067
|
+
# `days_ago(n)` … `years_since(n)` are all `advance(unit => ±n)`, and `Date#advance` is pure y/m/d
|
|
1068
|
+
# arithmetic on the receiver. The argument is `Numeric` and not `Integer` because `advance`
|
|
1069
|
+
# divmods a fractional `days:` / `weeks:` on purpose.
|
|
1070
|
+
%a{pure}
|
|
1071
|
+
def days_ago: (Numeric days) -> Date
|
|
1072
|
+
%a{pure}
|
|
1073
|
+
def days_since: (Numeric days) -> Date
|
|
1074
|
+
%a{pure}
|
|
1075
|
+
def weeks_ago: (Numeric weeks) -> Date
|
|
1076
|
+
%a{pure}
|
|
1077
|
+
def weeks_since: (Numeric weeks) -> Date
|
|
1078
|
+
%a{pure}
|
|
1079
|
+
def months_ago: (Numeric months) -> Date
|
|
1080
|
+
%a{pure}
|
|
1081
|
+
def months_since: (Numeric months) -> Date
|
|
1082
|
+
%a{pure}
|
|
1083
|
+
def years_ago: (Numeric years) -> Date
|
|
1084
|
+
%a{pure}
|
|
1085
|
+
def years_since: (Numeric years) -> Date
|
|
1086
|
+
# The `at_`-prefixed spellings are `alias`es of the already-declared `beginning_of_*` / `end_of_*`
|
|
1087
|
+
# and inherit their purity verdict one for one. `end_of_month` reaches
|
|
1088
|
+
# `::Time.days_in_month(month, year)` with BOTH arguments supplied, so it never takes that
|
|
1089
|
+
# method's clock-reading default.
|
|
1090
|
+
%a{pure}
|
|
1091
|
+
def at_beginning_of_month: () -> Date
|
|
1092
|
+
%a{pure}
|
|
1093
|
+
def at_end_of_month: () -> Date
|
|
1094
|
+
%a{pure}
|
|
1095
|
+
def at_beginning_of_year: () -> Date
|
|
1096
|
+
%a{pure}
|
|
1097
|
+
def at_end_of_year: () -> Date
|
|
1098
|
+
%a{pure}
|
|
1099
|
+
def beginning_of_quarter: () -> Date
|
|
1100
|
+
%a{pure}
|
|
1101
|
+
def at_beginning_of_quarter: () -> Date
|
|
1102
|
+
%a{pure}
|
|
1103
|
+
def end_of_quarter: () -> Date
|
|
1104
|
+
%a{pure}
|
|
1105
|
+
def at_end_of_quarter: () -> Date
|
|
1106
|
+
# `(month / 3.0).ceil` — `Float#ceil` with no argument is Integer.
|
|
1107
|
+
%a{pure}
|
|
1108
|
+
def quarter: () -> Integer
|
|
1109
|
+
# NOT %a{pure}: `next_week` / `prev_week` / `last_week` / `days_to_week_start` / `all_week` default
|
|
1110
|
+
# their `start_day` to `Date.beginning_of_week`, a fiber-local config read
|
|
1111
|
+
# (`ActiveSupport::IsolatedExecutionState[:beginning_of_week]`) — the same read that already keeps
|
|
1112
|
+
# `beginning_of_week` / `end_of_week` above unannotated. `next_weekday` joins them because it calls
|
|
1113
|
+
# `next_week(:monday, same_time: true)`, whose body reaches `beginning_of_week` with NO argument.
|
|
1114
|
+
def next_week: (?Symbol given_day_in_next_week, ?same_time: bool) -> Date
|
|
1115
|
+
def prev_week: (?Symbol start_day, ?same_time: bool) -> Date
|
|
1116
|
+
def last_week: (?Symbol start_day, ?same_time: bool) -> Date
|
|
1117
|
+
def next_weekday: () -> Date
|
|
1118
|
+
def days_to_week_start: (?Symbol start_day) -> Integer
|
|
1119
|
+
def all_week: (?Symbol start_day) -> Range[Date]
|
|
1120
|
+
# NOT %a{pure}, and for the SAME reason as `beginning_of_week` / `end_of_week`, which these two are
|
|
1121
|
+
# `alias`es of. Declaring them zero-arity is the concrete bug #658 found on the `Time` twins:
|
|
1122
|
+
# `date.at_beginning_of_week(:sunday)` — correct Rails — then draws `call.wrong-arity`.
|
|
1123
|
+
def at_beginning_of_week: (?Symbol start_day) -> Date
|
|
1124
|
+
def at_end_of_week: (?Symbol start_day) -> Date
|
|
1125
|
+
# `prev_weekday` / `last_weekday` DO stay pure while `next_weekday` above does not, and the
|
|
1126
|
+
# asymmetry is real rather than an oversight: the body reaches `beginning_of_week(:friday)` with
|
|
1127
|
+
# `:friday` passed explicitly, so no default argument ever reaches `Date.beginning_of_week`.
|
|
1128
|
+
%a{pure}
|
|
1129
|
+
def prev_weekday: () -> Date
|
|
1130
|
+
%a{pure}
|
|
1131
|
+
def last_weekday: () -> Date
|
|
1132
|
+
# `monday` / `sunday` likewise name their start day explicitly (`beginning_of_week(:monday)` /
|
|
1133
|
+
# `end_of_week(:monday)`), so unlike their zero-argument parents they read no config.
|
|
1134
|
+
%a{pure}
|
|
1135
|
+
def monday: () -> Date
|
|
1136
|
+
%a{pure}
|
|
1137
|
+
def sunday: () -> Date
|
|
1138
|
+
%a{pure}
|
|
1139
|
+
def next_quarter: () -> Date
|
|
1140
|
+
%a{pure}
|
|
1141
|
+
def prev_quarter: () -> Date
|
|
1142
|
+
%a{pure}
|
|
1143
|
+
def last_quarter: () -> Date
|
|
1144
|
+
%a{pure}
|
|
1145
|
+
def last_month: () -> Date
|
|
1146
|
+
%a{pure}
|
|
1147
|
+
def last_year: () -> Date
|
|
1148
|
+
# `all_month` / `all_quarter` / `all_year` compose two pure endpoints and answer `Range[Date]`.
|
|
1149
|
+
# `all_day` above is the odd one out and stays `Range[Time]`: on `Date` its endpoints go through
|
|
1150
|
+
# `in_time_zone`, which is where the `Date` / `Time` divergence in this family lives.
|
|
1151
|
+
%a{pure}
|
|
1152
|
+
def all_month: () -> Range[Date]
|
|
1153
|
+
%a{pure}
|
|
1154
|
+
def all_quarter: () -> Range[Date]
|
|
1155
|
+
%a{pure}
|
|
1156
|
+
def all_year: () -> Range[Date]
|
|
1157
|
+
%a{pure}
|
|
1158
|
+
def next_occurring: (Symbol day_of_week) -> Date
|
|
1159
|
+
%a{pure}
|
|
1160
|
+
def prev_occurring: (Symbol day_of_week) -> Date
|
|
1161
|
+
|
|
1162
|
+
# `core_ext/date/calculations.rb` — the `Date`-only half.
|
|
1163
|
+
#
|
|
1164
|
+
# `change` takes a POSITIONAL options hash (`def change(options)`), not keywords, which is why it
|
|
1165
|
+
# is spelled `(untyped options)` like `advance` above rather than `(**untyped)`: that spelling
|
|
1166
|
+
# accepts both `d.change(day: 1)` and `d.change({ day: 1 })`, and only the second is valid against
|
|
1167
|
+
# a keyword-only signature.
|
|
1168
|
+
%a{pure}
|
|
1169
|
+
def change: (untyped options) -> Date
|
|
1170
|
+
|
|
1171
|
+
# NONE of the seven below are `%a{pure}`: every one is `in_time_zone.xxx`, and
|
|
1172
|
+
# `in_time_zone(zone = ::Time.zone)` reads `Time.zone` when no explicit zone is given. `in` is the
|
|
1173
|
+
# `alias :in, :since` of the already-declared `Date#since`, so it inherits its `-> Time` and its
|
|
1174
|
+
# verdict; `middle_of_day` and its five aliases join the `beginning_of_day` family above. This is
|
|
1175
|
+
# the one place `Date` and `Time` diverge on the exact same method names.
|
|
1176
|
+
def in: (Numeric seconds) -> Time
|
|
1177
|
+
def middle_of_day: () -> Time
|
|
1178
|
+
def midday: () -> Time
|
|
1179
|
+
def noon: () -> Time
|
|
1180
|
+
def at_midday: () -> Time
|
|
1181
|
+
def at_noon: () -> Time
|
|
1182
|
+
def at_middle_of_day: () -> Time
|
|
1183
|
+
|
|
1184
|
+
# `core_ext/date/conversions.rb`.
|
|
1185
|
+
#
|
|
1186
|
+
# NOT %a{pure}: `to_fs` / `to_formatted_s` look the format up in `Date::DATE_FORMATS`, a mutable
|
|
1187
|
+
# process-global hash applications are documented to extend from an initializer, and a custom entry
|
|
1188
|
+
# may be an arbitrary lambda that is then CALLED. Both always answer a String whatever the
|
|
1189
|
+
# formatter returns: the lambda branch ends in `.to_s` and the fallback is `to_s`.
|
|
1190
|
+
def to_fs: (?Symbol format) -> String
|
|
1191
|
+
def to_formatted_s: (?Symbol format) -> String
|
|
1192
|
+
# `readable_inspect` is `strftime("%a, %d %b %Y")` and `default_inspect` is the `alias_method` of
|
|
1193
|
+
# the ORIGINAL `Date#inspect` — both pure on a `Date` receiver. Neither is annotated, and that is
|
|
1194
|
+
# deliberate rather than an omission: `DateTime#readable_inspect` is a DIFFERENT body
|
|
1195
|
+
# (`to_fs(:rfc822)`, which reads the mutable `Time::DATE_FORMATS`) and is NOT pure, so annotating
|
|
1196
|
+
# the `Date` row would push a purity claim onto `DateTime` through inheritance that the audit does
|
|
1197
|
+
# not support. Under-claiming an envelope costs precision; over-claiming one is unsound.
|
|
1198
|
+
def readable_inspect: () -> String
|
|
1199
|
+
def default_inspect: () -> String
|
|
1200
|
+
|
|
1201
|
+
# `core_ext/date_and_time/zones.rb`. Answers an `ActiveSupport::TimeWithZone`, which nothing in
|
|
1202
|
+
# this bundle declares, so the return is `untyped` rather than a guess — the same answer the `Time`
|
|
1203
|
+
# and `DateTime` rows give. The parameter is `untyped` and not `String | Symbol` because
|
|
1204
|
+
# `Time.find_zone!` also accepts an `ActiveSupport::TimeZone`, a `TZInfo::Timezone` and an offset
|
|
1205
|
+
# `Duration` (`Time.find_zone! -5.hours` is the documented spelling).
|
|
1206
|
+
#
|
|
1207
|
+
# NOT %a{pure}: `in_time_zone(zone = ::Time.zone)` reads `Time.zone` through its own default
|
|
1208
|
+
# argument. The label is `global.read` alone and it is recorded in the plugin manifest
|
|
1209
|
+
# (`lib/rigor/plugin/activesupport_core_ext/effects.rb`), next to the `Time` and `DateTime` rows.
|
|
1210
|
+
def in_time_zone: (?untyped zone) -> untyped
|
|
1211
|
+
|
|
1212
|
+
# `core_ext/date/calculations.rb`'s `class << self` — the week-start configuration surface. `Date`
|
|
1213
|
+
# is closed, so these omissions were false positives on an ordinary `config/initializers` line.
|
|
1214
|
+
#
|
|
1215
|
+
# `self.beginning_of_week` is the READER of this group and belongs to it, not to the date
|
|
1216
|
+
# constructors at the top of the class. It answers a **Symbol** — the week start, `:monday` by
|
|
1217
|
+
# default:
|
|
1218
|
+
#
|
|
1219
|
+
# def beginning_of_week
|
|
1220
|
+
# ::ActiveSupport::IsolatedExecutionState[:beginning_of_week] || beginning_of_week_default || :monday
|
|
1221
|
+
# end
|
|
1222
|
+
#
|
|
1223
|
+
# It was declared `() -> Date` until #762, up with `self.current` and friends, which is where the
|
|
1224
|
+
# wrong reading came from. That was a false-positive generator rather than lost precision, because
|
|
1225
|
+
# `Date` is closed in both directions: every correct Symbol use of the value (`== :monday`, `to_s`,
|
|
1226
|
+
# feeding it back to the instance `beginning_of_week(start_day)`) looked wrong, while an incorrect
|
|
1227
|
+
# `Date` use of it looked right — and `find_beginning_of_week!` below returns that same value and
|
|
1228
|
+
# already said `Symbol`.
|
|
1229
|
+
#
|
|
1230
|
+
# NONE are annotated: `beginning_of_week` / `beginning_of_week=` / `beginning_of_week_default` /
|
|
1231
|
+
# `beginning_of_week_default=` read and write `ActiveSupport::IsolatedExecutionState` and a
|
|
1232
|
+
# class-level `attr_accessor`. `find_beginning_of_week!` is a pure guard over a frozen constant,
|
|
1233
|
+
# but it is left unannotated to match the `Time` singletons above, none of which claim an envelope
|
|
1234
|
+
# either.
|
|
1235
|
+
def self.beginning_of_week: () -> Symbol
|
|
1236
|
+
def self.beginning_of_week=: (Symbol week_start) -> Symbol
|
|
1237
|
+
def self.beginning_of_week_default: () -> Symbol?
|
|
1238
|
+
def self.beginning_of_week_default=: (Symbol? week_start) -> Symbol?
|
|
1239
|
+
def self.find_beginning_of_week!: (Symbol week_start) -> Symbol
|
|
618
1240
|
end
|
|
619
1241
|
|
|
620
1242
|
# ---------------------------------------------------------------
|
|
@@ -811,16 +1433,53 @@ end
|
|
|
811
1433
|
# ---------------------------------------------------------------
|
|
812
1434
|
|
|
813
1435
|
class DateTime
|
|
1436
|
+
# -------------------------------------------------------------
|
|
1437
|
+
# Issue #670 — why this block is mostly OVERRIDES of `Date` rows
|
|
1438
|
+
# -------------------------------------------------------------
|
|
1439
|
+
#
|
|
1440
|
+
# `DateTime < Date`, so every row the `Date` block above declares is INHERITED here, and for most
|
|
1441
|
+
# of `DateAndTime::Calculations` the inherited return is wrong: the shared module's `first_hour` /
|
|
1442
|
+
# `last_hour` helpers branch on `acts_like?(:time)`, which is false on `Date` and true on
|
|
1443
|
+
# `DateTime`, and the `advance` / `change` primitives they compose are each class's own. So
|
|
1444
|
+
# `DateTime.now.beginning_of_month` answers a `DateTime` at runtime while the inherited declaration
|
|
1445
|
+
# said `Date` — a wrong-return false positive that was live on master before this block grew, and
|
|
1446
|
+
# the reason declaring the missing surface on `Date` ALONE would have converted the
|
|
1447
|
+
# `undefined-method` false positives into wrong-return ones rather than fixing them.
|
|
1448
|
+
#
|
|
1449
|
+
# Every return below was confirmed by CALLING it on a real `DateTime` receiver against
|
|
1450
|
+
# activesupport-8.1.3.1, not read off the body. Rows the audit found already CORRECT through
|
|
1451
|
+
# inheritance are deliberately absent, and the non-obvious ones are worth naming:
|
|
1452
|
+
# `today?` / `past?` / `before?` / `on_weekend?` (bool), `quarter` / `days_to_week_start`
|
|
1453
|
+
# (Integer), `to_fs` / `to_formatted_s` / `readable_inspect` / `default_inspect` (String) and
|
|
1454
|
+
# `acts_like_date?` (true) answer the same type on both classes, so the `Date` row stands.
|
|
1455
|
+
#
|
|
1456
|
+
# The sharpest of them is at the singleton: `DateTime.yesterday` and `DateTime.tomorrow` really do
|
|
1457
|
+
# answer a `Date`, not a `DateTime` — their bodies are a hardcoded `::Date.current.yesterday` — so
|
|
1458
|
+
# the inherited `Date` rows are RIGHT and overriding them would have been the false positive.
|
|
1459
|
+
# `DateTime.current` is the one that is wrong inherited, and it is overridden below.
|
|
1460
|
+
|
|
814
1461
|
# `utc` is pure Rational arithmetic on the receiver's own offset
|
|
815
|
-
# (`core_ext/date_time/calculations.rb`), no zone lookup.
|
|
1462
|
+
# (`core_ext/date_time/calculations.rb`), no zone lookup. `getgm` / `getutc` / `gmtime` are its
|
|
1463
|
+
# `alias_method`s and inherit the verdict.
|
|
816
1464
|
%a{pure}
|
|
817
1465
|
def utc: () -> Time
|
|
1466
|
+
%a{pure}
|
|
1467
|
+
def getgm: () -> Time
|
|
1468
|
+
%a{pure}
|
|
1469
|
+
def getutc: () -> Time
|
|
1470
|
+
%a{pure}
|
|
1471
|
+
def gmtime: () -> Time
|
|
818
1472
|
# NOTE: `DateTime#to_time` is NOT declared here — stdlib `date` already types it.
|
|
819
1473
|
# NOT %a{pure}: `in_time_zone(zone = ::Time.zone)` reads `Time.zone`
|
|
820
1474
|
# when no explicit zone is passed (`core_ext/date_and_time/zones.rb`).
|
|
821
1475
|
# Not previously covered by #387's `effect_attributions:`; the plugin
|
|
822
1476
|
# manifest gains this row in the same change (`global.read`).
|
|
823
|
-
|
|
1477
|
+
#
|
|
1478
|
+
# The parameter widened from `String | Symbol` to `untyped` in #670, for the reason the `Time` and
|
|
1479
|
+
# `Date` rows already carry: `Time.find_zone!` also accepts an `ActiveSupport::TimeZone`, a
|
|
1480
|
+
# `TZInfo::Timezone` and an offset `Duration`, so `dt.in_time_zone(-5.hours)` — the documented
|
|
1481
|
+
# spelling — drew `call.argument-type-mismatch` on code that runs.
|
|
1482
|
+
def in_time_zone: (?untyped zone) -> untyped
|
|
824
1483
|
# `yesterday` / `tomorrow` are inherited from `DateAndTime::Calculations`
|
|
825
1484
|
# via `Date` (`DateTime < Date`), resolved against `DateTime`'s own
|
|
826
1485
|
# `advance` — pure, same reasoning as `Date#yesterday` / `#tomorrow`
|
|
@@ -849,6 +1508,200 @@ class DateTime
|
|
|
849
1508
|
def end_of_minute: () -> DateTime
|
|
850
1509
|
%a{pure}
|
|
851
1510
|
def acts_like_time?: () -> true
|
|
1511
|
+
|
|
1512
|
+
# -------------------------------------------------------------
|
|
1513
|
+
# Issue #670 — the rest of `core_ext/date_time/*`
|
|
1514
|
+
# -------------------------------------------------------------
|
|
1515
|
+
|
|
1516
|
+
# `core_ext/date_time/calculations.rb`. `advance` and `change` are `DateTime`'s OWN redefinitions,
|
|
1517
|
+
# so they answer `DateTime` where the inherited `Date` rows said `Date` — and because the whole
|
|
1518
|
+
# `DateAndTime::Calculations` family below is built out of these two, every one of those rows
|
|
1519
|
+
# inherits the same correction. Both are pure: `change` ends in `::DateTime.civil(...)` over the
|
|
1520
|
+
# receiver's own fields and `advance` composes `to_date.advance` with it. `change` takes a
|
|
1521
|
+
# POSITIONAL options hash, spelled `(untyped options)` for the reason the `Date#change` row gives.
|
|
1522
|
+
%a{pure}
|
|
1523
|
+
def advance: (untyped options) -> DateTime
|
|
1524
|
+
%a{pure}
|
|
1525
|
+
def change: (untyped options) -> DateTime
|
|
1526
|
+
# `in` is `alias :in, :since`. Unlike `Date#in`, which routes through `in_time_zone`, `DateTime`'s
|
|
1527
|
+
# is `self + Rational(seconds, 86400)` — pure, and never leaves the receiver.
|
|
1528
|
+
%a{pure}
|
|
1529
|
+
def in: (Numeric seconds) -> DateTime
|
|
1530
|
+
# The `midnight` / `middle_of_day` / `end_of_day` alias families. All are `change(hour: n)` on
|
|
1531
|
+
# `DateTime`, which is what makes them pure here and NOT pure on `Date`, where the same names go
|
|
1532
|
+
# through `in_time_zone`.
|
|
1533
|
+
%a{pure}
|
|
1534
|
+
def midnight: () -> DateTime
|
|
1535
|
+
%a{pure}
|
|
1536
|
+
def at_midnight: () -> DateTime
|
|
1537
|
+
%a{pure}
|
|
1538
|
+
def at_beginning_of_day: () -> DateTime
|
|
1539
|
+
%a{pure}
|
|
1540
|
+
def at_end_of_day: () -> DateTime
|
|
1541
|
+
%a{pure}
|
|
1542
|
+
def middle_of_day: () -> DateTime
|
|
1543
|
+
%a{pure}
|
|
1544
|
+
def midday: () -> DateTime
|
|
1545
|
+
%a{pure}
|
|
1546
|
+
def noon: () -> DateTime
|
|
1547
|
+
%a{pure}
|
|
1548
|
+
def at_midday: () -> DateTime
|
|
1549
|
+
%a{pure}
|
|
1550
|
+
def at_noon: () -> DateTime
|
|
1551
|
+
%a{pure}
|
|
1552
|
+
def at_middle_of_day: () -> DateTime
|
|
1553
|
+
%a{pure}
|
|
1554
|
+
def at_beginning_of_hour: () -> DateTime
|
|
1555
|
+
%a{pure}
|
|
1556
|
+
def at_end_of_hour: () -> DateTime
|
|
1557
|
+
%a{pure}
|
|
1558
|
+
def at_beginning_of_minute: () -> DateTime
|
|
1559
|
+
%a{pure}
|
|
1560
|
+
def at_end_of_minute: () -> DateTime
|
|
1561
|
+
# `seconds_since_midnight` is `sec + (min * 60) + (hour * 3600)` — Integer, and NOT the Float
|
|
1562
|
+
# `Time#seconds_since_midnight` answers (that one ends in `+ (usec / 1.0e+6)`). A same-name row
|
|
1563
|
+
# copied across from the `Time` block would have been wrong here. `subsec` is a plain alias of
|
|
1564
|
+
# `sec_fraction`, which rbs already types on `DateTime`, and answers `Rational` on every receiver
|
|
1565
|
+
# — unlike `Time#subsec`, where CRuby gives the Integer `0` for a whole-second value.
|
|
1566
|
+
%a{pure}
|
|
1567
|
+
def seconds_since_midnight: () -> Integer
|
|
1568
|
+
%a{pure}
|
|
1569
|
+
def seconds_until_end_of_day: () -> Integer
|
|
1570
|
+
%a{pure}
|
|
1571
|
+
def subsec: () -> Rational
|
|
1572
|
+
%a{pure}
|
|
1573
|
+
def to_f: () -> Float
|
|
1574
|
+
%a{pure}
|
|
1575
|
+
def to_i: () -> Integer
|
|
1576
|
+
%a{pure}
|
|
1577
|
+
def usec: () -> Integer
|
|
1578
|
+
%a{pure}
|
|
1579
|
+
def nsec: () -> Integer
|
|
1580
|
+
%a{pure}
|
|
1581
|
+
def utc?: () -> bool
|
|
1582
|
+
%a{pure}
|
|
1583
|
+
def utc_offset: () -> Integer
|
|
1584
|
+
# NOT %a{pure}: `localtime(utc_offset = nil)` ends in `Time#getlocal(utc_offset)`, and with no
|
|
1585
|
+
# argument `getlocal` resolves against the PROCESS time zone (`ENV['TZ']`) — a global read. The
|
|
1586
|
+
# parameter is `untyped` because it accepts both an Integer offset and a `"+09:00"` String.
|
|
1587
|
+
def localtime: (?untyped utc_offset) -> Time
|
|
1588
|
+
def getlocal: (?untyped utc_offset) -> Time
|
|
1589
|
+
|
|
1590
|
+
# `core_ext/date_and_time/calculations.rb`, re-declared here because `first_hour` / `last_hour`
|
|
1591
|
+
# answer `beginning_of_day` / `end_of_day` on a `DateTime` receiver where they are the identity on
|
|
1592
|
+
# a `Date` one. Purity matches the `Date` rows one for one — the divergence is in the RETURN, not
|
|
1593
|
+
# the envelope — except that here the whole family is genuinely pure, since it composes
|
|
1594
|
+
# `DateTime`'s own `change` rather than `Date`'s zone-routing accessors.
|
|
1595
|
+
%a{pure}
|
|
1596
|
+
def days_ago: (Numeric days) -> DateTime
|
|
1597
|
+
%a{pure}
|
|
1598
|
+
def days_since: (Numeric days) -> DateTime
|
|
1599
|
+
%a{pure}
|
|
1600
|
+
def weeks_ago: (Numeric weeks) -> DateTime
|
|
1601
|
+
%a{pure}
|
|
1602
|
+
def weeks_since: (Numeric weeks) -> DateTime
|
|
1603
|
+
%a{pure}
|
|
1604
|
+
def months_ago: (Numeric months) -> DateTime
|
|
1605
|
+
%a{pure}
|
|
1606
|
+
def months_since: (Numeric months) -> DateTime
|
|
1607
|
+
%a{pure}
|
|
1608
|
+
def years_ago: (Numeric years) -> DateTime
|
|
1609
|
+
%a{pure}
|
|
1610
|
+
def years_since: (Numeric years) -> DateTime
|
|
1611
|
+
%a{pure}
|
|
1612
|
+
def beginning_of_month: () -> DateTime
|
|
1613
|
+
%a{pure}
|
|
1614
|
+
def at_beginning_of_month: () -> DateTime
|
|
1615
|
+
%a{pure}
|
|
1616
|
+
def end_of_month: () -> DateTime
|
|
1617
|
+
%a{pure}
|
|
1618
|
+
def at_end_of_month: () -> DateTime
|
|
1619
|
+
%a{pure}
|
|
1620
|
+
def beginning_of_year: () -> DateTime
|
|
1621
|
+
%a{pure}
|
|
1622
|
+
def at_beginning_of_year: () -> DateTime
|
|
1623
|
+
%a{pure}
|
|
1624
|
+
def end_of_year: () -> DateTime
|
|
1625
|
+
%a{pure}
|
|
1626
|
+
def at_end_of_year: () -> DateTime
|
|
1627
|
+
%a{pure}
|
|
1628
|
+
def beginning_of_quarter: () -> DateTime
|
|
1629
|
+
%a{pure}
|
|
1630
|
+
def at_beginning_of_quarter: () -> DateTime
|
|
1631
|
+
%a{pure}
|
|
1632
|
+
def end_of_quarter: () -> DateTime
|
|
1633
|
+
%a{pure}
|
|
1634
|
+
def at_end_of_quarter: () -> DateTime
|
|
1635
|
+
%a{pure}
|
|
1636
|
+
def next_quarter: () -> DateTime
|
|
1637
|
+
%a{pure}
|
|
1638
|
+
def prev_quarter: () -> DateTime
|
|
1639
|
+
%a{pure}
|
|
1640
|
+
def last_quarter: () -> DateTime
|
|
1641
|
+
%a{pure}
|
|
1642
|
+
def last_month: () -> DateTime
|
|
1643
|
+
%a{pure}
|
|
1644
|
+
def last_year: () -> DateTime
|
|
1645
|
+
%a{pure}
|
|
1646
|
+
def prev_weekday: () -> DateTime
|
|
1647
|
+
%a{pure}
|
|
1648
|
+
def last_weekday: () -> DateTime
|
|
1649
|
+
%a{pure}
|
|
1650
|
+
def monday: () -> DateTime
|
|
1651
|
+
%a{pure}
|
|
1652
|
+
def sunday: () -> DateTime
|
|
1653
|
+
%a{pure}
|
|
1654
|
+
def next_occurring: (Symbol day_of_week) -> DateTime
|
|
1655
|
+
%a{pure}
|
|
1656
|
+
def prev_occurring: (Symbol day_of_week) -> DateTime
|
|
1657
|
+
# `all_day` diverges from the `Date` row in more than its element type: on `Date` its endpoints go
|
|
1658
|
+
# through `in_time_zone` and it is `Range[Time]` and NOT pure, while here it composes the two pure
|
|
1659
|
+
# `change(hour:)` accessors above.
|
|
1660
|
+
%a{pure}
|
|
1661
|
+
def all_day: () -> Range[DateTime]
|
|
1662
|
+
%a{pure}
|
|
1663
|
+
def all_month: () -> Range[DateTime]
|
|
1664
|
+
%a{pure}
|
|
1665
|
+
def all_quarter: () -> Range[DateTime]
|
|
1666
|
+
%a{pure}
|
|
1667
|
+
def all_year: () -> Range[DateTime]
|
|
1668
|
+
# NOT %a{pure}: the `Date.beginning_of_week` default-argument read, exactly as on `Date` and
|
|
1669
|
+
# `Time`. The `at_`-prefixed pair are `alias`es and take the same optional `start_day` — declaring
|
|
1670
|
+
# them zero-arity is the `call.wrong-arity` bug #658 found on the `Time` twins.
|
|
1671
|
+
def beginning_of_week: (?Symbol start_day) -> DateTime
|
|
1672
|
+
def end_of_week: (?Symbol start_day) -> DateTime
|
|
1673
|
+
def at_beginning_of_week: (?Symbol start_day) -> DateTime
|
|
1674
|
+
def at_end_of_week: (?Symbol start_day) -> DateTime
|
|
1675
|
+
def next_week: (?Symbol given_day_in_next_week, ?same_time: bool) -> DateTime
|
|
1676
|
+
def prev_week: (?Symbol start_day, ?same_time: bool) -> DateTime
|
|
1677
|
+
def last_week: (?Symbol start_day, ?same_time: bool) -> DateTime
|
|
1678
|
+
def next_weekday: () -> DateTime
|
|
1679
|
+
def all_week: (?Symbol start_day) -> Range[DateTime]
|
|
1680
|
+
|
|
1681
|
+
# `core_ext/date_time/conversions.rb`. `formatted_offset` is
|
|
1682
|
+
# `ActiveSupport::TimeZone.seconds_to_utc_offset(utc_offset, colon)`, a `format %` over two
|
|
1683
|
+
# arguments — pure, and String in both branches. `colon` is `boolish` and not `bool` because the
|
|
1684
|
+
# body is plain truthiness (`colon ? WITH_COLON : WITHOUT_COLON`); declared `bool` it made
|
|
1685
|
+
# `dt.formatted_offset(1)` an argument-type diagnostic on code that runs, which is the review
|
|
1686
|
+
# finding #658 recorded for the `Time` row.
|
|
1687
|
+
%a{pure}
|
|
1688
|
+
def formatted_offset: (?boolish colon, ?untyped alternate_utc_string) -> String
|
|
1689
|
+
|
|
1690
|
+
# `core_ext/date_and_time/compatibility.rb` — `mattr_accessor
|
|
1691
|
+
# :utc_to_local_returns_utc_offset_times, instance_writer: false` defines an instance READER on
|
|
1692
|
+
# every class that includes the module. `DateTime` includes it (via
|
|
1693
|
+
# `core_ext/date_time/compatibility.rb`) and `Date` does NOT, so this row belongs here and not in
|
|
1694
|
+
# the `Date` block — verified against the runtime method table, not assumed from the file layout.
|
|
1695
|
+
# NOT %a{pure}: it reads process-global configuration.
|
|
1696
|
+
def utc_to_local_returns_utc_offset_times: () -> bool
|
|
1697
|
+
|
|
1698
|
+
# `DateTime.current` is `Time.zone.now.to_datetime`. Without this row it inherits `Date.current`'s
|
|
1699
|
+
# `() -> Date` — wrong, and the singleton-level instance of this issue's whole thesis.
|
|
1700
|
+
# NOT %a{pure}: already attributed `nondet.time` / `global.read` in `effects.rb` (#387).
|
|
1701
|
+
def self.current: () -> DateTime
|
|
1702
|
+
# `civil_from_format(:local, 2012)` reads `::Time.local(...).utc_offset`, i.e. the process time
|
|
1703
|
+
# zone, so it is left unannotated like the `Time` singletons.
|
|
1704
|
+
def self.civil_from_format: (untyped utc_or_local, Integer year, ?Integer month, ?Integer day, ?Integer hour, ?Integer min, ?Integer sec) -> DateTime
|
|
852
1705
|
end
|
|
853
1706
|
|
|
854
1707
|
# ---------------------------------------------------------------
|