Skip to content

Commit cc5b2b2

Browse files
committed
Add Timecop cop
This cop makes `Timecop` illegal, in favour of `ActiveSupport::Testing::TimeHelpers`. Specifically, - `Timecop.freeze` should be replaced with `freeze_time` (autocorrected) - `Timecop.freeze(...)` should be replaced with `travel` or `travel_to` - `Timecop.return` should be replaced with `travel_back` or `unfreeze_time` (autocorrected) - `Timecop.scale` & `Timecop.travel` should be replaced with `travel` or `travel_to`. - Explicitly travelling again should be used instead of relying on time continuing to flow - `Timecop` should not appear anywhere
1 parent 9975a46 commit cc5b2b2

File tree

5 files changed

+384
-0
lines changed

5 files changed

+384
-0
lines changed

changelog/new_add_timecop_cop.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#38](https://github.com/rubocop/rubocop-rails/pull/38): Add `Timecop` cop. ([@sambostock][])

config/default.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,6 +1098,12 @@ Rails/TimeZoneAssignment:
10981098
- spec/**/*.rb
10991099
- test/**/*.rb
11001100

1101+
Rails/Timecop:
1102+
Description: 'Prefer `ActiveSupport::Testing::TimeHelpers` over `Timecop`.'
1103+
Enabled: pending
1104+
VersionAdded: <<next>>
1105+
SafeAutoCorrect: false
1106+
11011107
Rails/ToFormattedS:
11021108
Description: 'Checks for consistent uses of `to_fs` or `to_formatted_s`.'
11031109
StyleGuide: 'https://rails.rubystyle.guide/#prefer-to-fs'

lib/rubocop/cop/rails/timecop.rb

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Rails
6+
# Disallows all usage of `Timecop`, in favour of
7+
# `ActiveSupport::Testing::TimeHelpers`.
8+
#
9+
# ## Migration
10+
# `Timecop.freeze` should be replaced with `freeze_time` when used
11+
# without arguments. Where a `duration` has been passed to `freeze`, it
12+
# should be replaced with `travel`. Likewise, where a `time` has been
13+
# passed to `freeze`, it should be replaced with `travel_to`.
14+
#
15+
# `Timecop.scale` should be replaced by explicitly calling `travel` or
16+
# `travel_to` with the expected `durations` or `times`, respectively,
17+
# rather than relying on allowing time to continue to flow.
18+
#
19+
# `Timecop.return` should be replaced with `travel_back`, when used
20+
# without a block. `travel_back` does not accept a block, so where
21+
# `return` is used with a block, it should be replaced by explicitly
22+
# calling `freeze_time` with a block, and passing the `time` to
23+
# temporarily return to.
24+
#
25+
# `Timecop.travel` should be replaced by `travel` or `travel_to` when
26+
# passed a `duration` or `time`, respectively. As with `Timecop.scale`,
27+
# rather than relying on time continuing to flow, it should be travelled
28+
# to explicitly.
29+
#
30+
# All other usages of `Timecop` are similarly disallowed.
31+
#
32+
# ## RSpec Caveats
33+
#
34+
# Note that if using RSpec, `TimeHelpers` are not included by default,
35+
# and must be manually included by updating `rails_helper` accordingly:
36+
#
37+
# ```ruby
38+
# RSpec.configure do |config|
39+
# config.include ActiveSupport::Testing::TimeHelpers
40+
# end
41+
# ```
42+
#
43+
# Moreover, because `TimeHelpers` relies on Minitest teardown hooks,
44+
# `rails_helper` must be required (instead of `spec_helper`), or a
45+
# similar adapter layer must be in effect.
46+
#
47+
# @example
48+
# # bad
49+
# Timecop
50+
#
51+
# # bad
52+
# Timecop.freeze
53+
# Timecop.freeze(duration)
54+
# Timecop.freeze(time)
55+
#
56+
# # good
57+
# freeze_time
58+
# travel(duration)
59+
# travel_to(time)
60+
#
61+
# # bad
62+
# Timecop.freeze { assert true }
63+
# Timecop.freeze(duration) { assert true }
64+
# Timecop.freeze(time) { assert true }
65+
#
66+
# # good
67+
# freeze_time { assert true }
68+
# travel(duration) { assert true }
69+
# travel_to(time) { assert true }
70+
#
71+
# # bad
72+
# Timecop.travel(duration)
73+
# Timecop.travel(time)
74+
#
75+
# # good
76+
# travel(duration)
77+
# travel_to(time)
78+
#
79+
# # bad
80+
# Timecop.return
81+
# Timecop.return { assert true }
82+
#
83+
# # good
84+
# travel_back
85+
# travel_to(time) { assert true }
86+
#
87+
# # bad
88+
# Timecop.scale(factor)
89+
# Timecop.scale(factor) { assert true }
90+
#
91+
# # good
92+
# travel(duration)
93+
# travel_to(time)
94+
# travel(duration) { assert true }
95+
# travel_to(time) { assert true }
96+
class Timecop < Base
97+
extend AutoCorrector
98+
99+
FREEZE_MESSAGE = 'Use `%<replacement>s` instead of `Timecop.freeze`'
100+
FREEZE_WITH_ARGUMENTS_MESSAGE = 'Use `travel` or `travel_to` instead of `Timecop.freeze`'
101+
RETURN_MESSAGE = 'Use `%<replacement>s` instead of `Timecop.return`'
102+
FLOW_ADDENDUM = 'If you need time to keep flowing, simulate it by travelling again.'
103+
TRAVEL_MESSAGE = "Use `travel` or `travel_to` instead of `Timecop.travel`. #{FLOW_ADDENDUM}"
104+
SCALE_MESSAGE = "Use `travel` or `travel_to` instead of `Timecop.scale`. #{FLOW_ADDENDUM}"
105+
MSG = 'Use `ActiveSupport::Testing::TimeHelpers` instead of `Timecop`'
106+
107+
def_node_matcher :timecop_const?, <<~PATTERN
108+
(const {nil? cbase} :Timecop)
109+
PATTERN
110+
111+
def_node_matcher :timecop_send, <<~PATTERN
112+
(send
113+
#timecop_const? ${:freeze :return :scale :travel}
114+
$...
115+
)
116+
PATTERN
117+
118+
def on_const(node)
119+
return unless timecop_const?(node)
120+
121+
timecop_send(node.parent) do |message, arguments|
122+
return on_timecop_send(node.parent, message, arguments)
123+
end
124+
125+
add_offense(node)
126+
end
127+
128+
private
129+
130+
def on_timecop_send(node, message, arguments)
131+
case message
132+
when :freeze then on_timecop_freeze(node, arguments)
133+
when :return then on_timecop_return(node, arguments)
134+
when :scale then on_timecop_scale(node, arguments)
135+
when :travel then on_timecop_travel(node, arguments)
136+
else add_offense(node)
137+
end
138+
end
139+
140+
def on_timecop_freeze(node, arguments)
141+
if arguments.empty?
142+
add_offense(node, message: format(FREEZE_MESSAGE, replacement: preferred_freeze_replacement)) do |corrector|
143+
autocorrect_freeze(corrector, node, arguments)
144+
end
145+
else
146+
add_offense(node, message: FREEZE_WITH_ARGUMENTS_MESSAGE)
147+
end
148+
end
149+
150+
def on_timecop_return(node, arguments)
151+
add_offense(node, message: format(RETURN_MESSAGE, replacement: preferred_return_replacement)) do |corrector|
152+
autocorrect_return(corrector, node, arguments)
153+
end
154+
end
155+
156+
def on_timecop_scale(node, _arguments)
157+
add_offense(node, message: SCALE_MESSAGE)
158+
end
159+
160+
def on_timecop_travel(node, _arguments)
161+
add_offense(node, message: TRAVEL_MESSAGE)
162+
end
163+
164+
def autocorrect_freeze(corrector, node, arguments)
165+
return unless arguments.empty?
166+
167+
corrector.replace(receiver_and_message_range(node), preferred_freeze_replacement)
168+
end
169+
170+
def autocorrect_return(corrector, node, _arguments)
171+
return if given_block?(node)
172+
173+
corrector.replace(receiver_and_message_range(node), preferred_return_replacement)
174+
end
175+
176+
def given_block?(node)
177+
node.send_type? && node.parent && node.parent.block_type? && node.parent.send_node == node
178+
end
179+
180+
def receiver_and_message_range(node)
181+
node.source_range.with(end_pos: node.location.selector.end_pos)
182+
end
183+
184+
def preferred_freeze_replacement
185+
return 'travel_to(Time.now)' if target_rails_version < 5.2
186+
187+
'freeze_time'
188+
end
189+
190+
def preferred_return_replacement
191+
return 'travel_back' if target_rails_version < 6.0
192+
193+
'unfreeze_time'
194+
end
195+
end
196+
end
197+
end
198+
end

lib/rubocop/cop/rails_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
require_relative 'rails/to_s_with_argument'
128128
require_relative 'rails/top_level_hash_with_indifferent_access'
129129
require_relative 'rails/transaction_exit_statement'
130+
require_relative 'rails/timecop'
130131
require_relative 'rails/uniq_before_pluck'
131132
require_relative 'rails/unique_validation_without_index'
132133
require_relative 'rails/unknown_env'

0 commit comments

Comments
 (0)