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.
Files changed (213) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/date.yml +6 -3
  4. data/data/builtins/ruby_core/enumerable.yml +1 -0
  5. data/data/builtins/ruby_core/exception.yml +2 -1
  6. data/data/builtins/ruby_core/file.yml +28 -14
  7. data/data/builtins/ruby_core/hash.yml +5 -2
  8. data/data/builtins/ruby_core/io.yml +33 -16
  9. data/data/builtins/ruby_core/random.yml +2 -1
  10. data/data/builtins/ruby_core/re.yml +6 -3
  11. data/data/builtins/ruby_core/struct.yml +2 -0
  12. data/data/builtins/ruby_core/time.yml +2 -1
  13. data/data/gem_overlay/activesupport/core_ext.rbs +409 -11
  14. data/docs/manual/02-cli-reference.md +16 -7
  15. data/docs/manual/05-inspecting-types.md +20 -8
  16. data/docs/manual/07-plugins.md +4 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +61 -0
  18. data/docs/manual/plugins/rigor-activerecord.md +74 -1
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
  20. data/docs/manual/plugins/rigor-sidekiq.md +22 -0
  21. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
  22. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
  23. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
  24. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
  25. data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
  26. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
  28. data/lib/rigor/analysis/check_rules.rb +383 -48
  29. data/lib/rigor/analysis/crash_signature.rb +116 -0
  30. data/lib/rigor/analysis/dependency_recorder.rb +18 -3
  31. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  32. data/lib/rigor/analysis/incremental.rb +28 -0
  33. data/lib/rigor/analysis/incremental_session.rb +44 -9
  34. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
  35. data/lib/rigor/analysis/reachability/graph.rb +14 -5
  36. data/lib/rigor/analysis/reachability/scan.rb +7 -3
  37. data/lib/rigor/analysis/result.rb +32 -0
  38. data/lib/rigor/analysis/run_cache_key.rb +41 -1
  39. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
  40. data/lib/rigor/analysis/runner/pool_coordinator.rb +34 -0
  41. data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
  42. data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
  43. data/lib/rigor/analysis/runner.rb +147 -10
  44. data/lib/rigor/analysis/worker_session.rb +10 -1
  45. data/lib/rigor/bleeding_edge.rb +7 -6
  46. data/lib/rigor/builtins/hkt_builtins.rb +45 -6
  47. data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
  48. data/lib/rigor/builtins/static_return_refinements.rb +20 -1
  49. data/lib/rigor/cache/descriptor.rb +42 -2
  50. data/lib/rigor/cache/incremental_snapshot.rb +40 -3
  51. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +31 -8
  52. data/lib/rigor/cache/store.rb +10 -1
  53. data/lib/rigor/cli/coverage_command.rb +23 -18
  54. data/lib/rigor/cli/coverage_mutation.rb +13 -17
  55. data/lib/rigor/cli/coverage_scan.rb +47 -6
  56. data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
  57. data/lib/rigor/cli/fused_protection_report.rb +23 -2
  58. data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
  59. data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
  60. data/lib/rigor/cli/mutation_protection_report.rb +31 -2
  61. data/lib/rigor/cli/sig_gen_command.rb +18 -0
  62. data/lib/rigor/cli/type_of_command.rb +188 -41
  63. data/lib/rigor/cli/type_of_renderer.rb +72 -3
  64. data/lib/rigor/cli/type_scan_command.rb +10 -1
  65. data/lib/rigor/cli.rb +1 -1
  66. data/lib/rigor/configuration.rb +7 -4
  67. data/lib/rigor/effects/plugin_facts.rb +1 -1
  68. data/lib/rigor/effects/scanner.rb +2 -3
  69. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
  70. data/lib/rigor/environment/lockfile_resolver.rb +15 -5
  71. data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
  72. data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
  73. data/lib/rigor/environment/rbs_loader.rb +525 -46
  74. data/lib/rigor/environment.rb +38 -15
  75. data/lib/rigor/inference/acceptance.rb +172 -20
  76. data/lib/rigor/inference/captured_locals.rb +62 -0
  77. data/lib/rigor/inference/content_join.rb +347 -0
  78. data/lib/rigor/inference/def_handle.rb +15 -3
  79. data/lib/rigor/inference/def_node_resolver.rb +62 -3
  80. data/lib/rigor/inference/expression_typer.rb +1020 -141
  81. data/lib/rigor/inference/fork_map.rb +6 -1
  82. data/lib/rigor/inference/hkt_reducer.rb +12 -1
  83. data/lib/rigor/inference/hkt_registry.rb +46 -0
  84. data/lib/rigor/inference/hkt_sugar_translator.rb +93 -0
  85. data/lib/rigor/inference/index_write_widening.rb +48 -0
  86. data/lib/rigor/inference/indexed_narrowing.rb +14 -0
  87. data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
  88. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
  89. data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
  90. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
  91. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
  92. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
  93. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
  94. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
  95. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
  96. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
  97. data/lib/rigor/inference/method_dispatcher.rb +155 -14
  98. data/lib/rigor/inference/method_parameter_binder.rb +3 -3
  99. data/lib/rigor/inference/mutation_widening.rb +244 -174
  100. data/lib/rigor/inference/narrowing.rb +331 -27
  101. data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
  102. data/lib/rigor/inference/pre_eval_constants.rb +15 -0
  103. data/lib/rigor/inference/precision_scanner.rb +5 -2
  104. data/lib/rigor/inference/project_patched_scanner.rb +2 -1
  105. data/lib/rigor/inference/rbs_type_translator.rb +89 -39
  106. data/lib/rigor/inference/scope_indexer.rb +1493 -140
  107. data/lib/rigor/inference/statement_evaluator.rb +276 -91
  108. data/lib/rigor/inference/struct_fold_safety.rb +186 -27
  109. data/lib/rigor/inference/version_guard.rb +229 -0
  110. data/lib/rigor/language_server/selection_range_provider.rb +1 -1
  111. data/lib/rigor/plugin/base.rb +3 -2
  112. data/lib/rigor/plugin/inflector.rb +14 -5
  113. data/lib/rigor/plugin/io_boundary.rb +107 -4
  114. data/lib/rigor/plugin/loader.rb +14 -0
  115. data/lib/rigor/protection/analysis_guard.rb +60 -0
  116. data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
  117. data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
  118. data/lib/rigor/protection/discovery_seed.rb +1 -1
  119. data/lib/rigor/protection/measurement_integrity.rb +36 -0
  120. data/lib/rigor/protection/mutation_scanner.rb +61 -6
  121. data/lib/rigor/protection/mutator.rb +121 -18
  122. data/lib/rigor/reflection.rb +182 -30
  123. data/lib/rigor/scope/discovery_index.rb +39 -1
  124. data/lib/rigor/scope.rb +385 -4
  125. data/lib/rigor/sig_gen/classification.rb +12 -1
  126. data/lib/rigor/sig_gen/generator.rb +254 -5
  127. data/lib/rigor/sig_gen/renderer.rb +1 -6
  128. data/lib/rigor/sig_gen/writer.rb +3 -0
  129. data/lib/rigor/source/constant_path.rb +79 -2
  130. data/lib/rigor/triage/catalogue.rb +1 -1
  131. data/lib/rigor/type/combinator.rb +10 -0
  132. data/lib/rigor/type/maybe.rb +47 -0
  133. data/lib/rigor/type/refined.rb +1 -2
  134. data/lib/rigor/type/result.rb +53 -0
  135. data/lib/rigor/type.rb +2 -0
  136. data/lib/rigor/version.rb +1 -1
  137. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
  138. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
  139. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
  140. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
  141. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
  142. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
  143. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
  144. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
  145. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
  146. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
  147. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
  148. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
  149. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
  150. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
  151. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
  152. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
  153. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
  154. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
  156. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
  157. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
  158. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
  159. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
  160. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
  161. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
  162. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
  163. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
  164. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
  165. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
  166. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
  167. data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
  168. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
  169. data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
  170. data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
  171. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
  172. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
  173. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
  174. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
  175. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
  176. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
  177. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
  178. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
  179. data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
  180. data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
  181. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
  182. data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
  183. data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
  184. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
  185. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
  186. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  187. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
  188. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
  189. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
  190. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
  191. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
  192. data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
  193. data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
  194. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
  195. data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
  196. data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
  197. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
  198. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
  199. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
  200. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
  201. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
  202. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
  203. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
  204. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
  205. data/sig/rigor/analysis/baseline.rbs +1 -1
  206. data/sig/rigor/environment.rbs +3 -0
  207. data/sig/rigor/inference.rbs +4 -1
  208. data/sig/rigor/plugin/io_boundary.rbs +2 -0
  209. data/sig/rigor/reflection.rbs +3 -1
  210. data/sig/rigor/scope.rbs +25 -1
  211. data/sig/rigor/type.rbs +95 -1
  212. data/sig/rigor.rbs +16 -0
  213. 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 adding the gem's `sig/` directory
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
- # signature_paths:
15
- # - sig
16
- # - path/to/rigor-activesupport-core-ext/sig
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 / Date / Time / Integer / Module
20
- # than this file covers; PRs welcome to extend.
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
- def at_beginning_of_week: () -> Time
502
- def at_end_of_week: () -> Time
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
- # 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.
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
- def in_time_zone: (?String | Symbol zone) -> untyped
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
  # ---------------------------------------------------------------