Skip to content

Commit 9e2486f

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` (autocorrected) - `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 dea8939 commit 9e2486f

File tree

3 files changed

+235
-0
lines changed

3 files changed

+235
-0
lines changed

lib/rubocop/cop/rails/timecop.rb

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Rails
6+
class Timecop < Cop
7+
FREEZE_MESSAGE = 'Use `freeze_time` instead of `Timecop.freeze`'
8+
FREEZE_WITH_ARGUMENTS_MESSAGE = 'Use `travel` or `travel_to` instead of `Timecop.freeze`'
9+
RETURN_MESSAGE = 'Use `travel_back` instead of `Timecop.return`'
10+
TRAVEL_MESSAGE = 'Use `travel` or `travel_to` instead of `Timecop.travel`. If you need time to keep flowing, ' \
11+
'simulate it by travelling again.'
12+
MSG = 'Use `ActiveSupport::Testing::TimeHelpers` instead of `Timecop`'
13+
14+
FREEZE_TIME = 'freeze_time'
15+
TRAVEL_BACK = 'travel_back'
16+
17+
TIMECOP_PATTERN_STRING = <<~PATTERN
18+
(const {nil? (:cbase)} :Timecop)
19+
PATTERN
20+
21+
def_node_matcher :timecop, TIMECOP_PATTERN_STRING
22+
23+
def_node_matcher :timecop_send, <<~PATTERN
24+
(send
25+
#{TIMECOP_PATTERN_STRING} ${:freeze :return :travel}
26+
$...
27+
)
28+
PATTERN
29+
30+
def on_const(node)
31+
return unless timecop(node)
32+
33+
timecop_send(node.parent) do |message, arguments|
34+
return on_timecop_send(node.parent, message, arguments)
35+
end
36+
37+
add_offense(node)
38+
end
39+
40+
def autocorrect(node)
41+
lambda do |corrector|
42+
timecop_send(node) do |message, arguments|
43+
case message
44+
when :freeze
45+
autocorrect_freeze(corrector, node, arguments)
46+
when :return
47+
autocorrect_return(corrector, node, arguments)
48+
end
49+
end
50+
end
51+
end
52+
53+
private
54+
55+
def on_timecop_send(node, message, arguments)
56+
case message
57+
when :freeze
58+
on_timecop_freeze(node, arguments)
59+
when :return
60+
on_timecop_return(node, arguments)
61+
when :travel
62+
on_timecop_travel(node, arguments)
63+
else
64+
add_offense(node)
65+
end
66+
end
67+
68+
def on_timecop_freeze(node, arguments)
69+
if arguments.empty?
70+
add_offense(node, message: FREEZE_MESSAGE)
71+
else
72+
add_offense(node, message: FREEZE_WITH_ARGUMENTS_MESSAGE)
73+
end
74+
end
75+
76+
def on_timecop_return(node, _arguments)
77+
add_offense(node, message: RETURN_MESSAGE)
78+
end
79+
80+
def on_timecop_travel(node, _arguments)
81+
add_offense(node, message: TRAVEL_MESSAGE)
82+
end
83+
84+
def autocorrect_freeze(corrector, node, arguments)
85+
return unless arguments.empty?
86+
87+
corrector.replace(receiver_and_message_range(node), FREEZE_TIME)
88+
end
89+
90+
def autocorrect_return(corrector, node, _arguments)
91+
corrector.replace(receiver_and_message_range(node), TRAVEL_BACK)
92+
end
93+
94+
def receiver_and_message_range(node)
95+
# FIXME: There is probably a better way to do this
96+
# Just trying to get the range of `Timecop.method_name`, without args, or block, or anything
97+
node.location.expression.with(end_pos: node.location.selector.end_pos)
98+
end
99+
end
100+
end
101+
end
102+
end

lib/rubocop/cop/rails_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ module Cop
5555
require_relative 'rails/scope_args'
5656
require_relative 'rails/skips_model_validations'
5757
require_relative 'rails/time_zone'
58+
require_relative 'rails/timecop'
5859
require_relative 'rails/uniq_before_pluck'
5960
require_relative 'rails/unknown_env'
6061
require_relative 'rails/validation'
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe(RuboCop::Cop::Rails::Timecop, :config) do
4+
subject(:cop) { described_class.new(config) }
5+
6+
describe 'Timecop.freeze' do
7+
context 'without a block' do
8+
context 'without arguments' do
9+
it 'adds an offense' do
10+
expect_offense(<<~RUBY)
11+
Timecop.freeze
12+
^^^^^^^^^^^^^^ Use `freeze_time` instead of `Timecop.freeze`
13+
RUBY
14+
end
15+
16+
it 'autocorrects to `freeze_time`' do
17+
expect(autocorrect_source('Timecop.freeze')).to(eq('freeze_time'))
18+
end
19+
end
20+
21+
context 'with arguments' do
22+
it 'adds an offense' do
23+
expect_offense(<<~RUBY)
24+
Timecop.freeze(123)
25+
^^^^^^^^^^^^^^^^^^^ Use `travel` or `travel_to` instead of `Timecop.freeze`
26+
RUBY
27+
end
28+
29+
it 'does not autocorrect' do
30+
source = 'Timecop.freeze(123)'
31+
32+
expect(autocorrect_source(source)).to(eq(source))
33+
end
34+
end
35+
end
36+
37+
context 'with a block' do
38+
context 'without arguments' do
39+
it 'adds an offense' do
40+
expect_offense(<<~RUBY)
41+
Timecop.freeze { }
42+
^^^^^^^^^^^^^^ Use `freeze_time` instead of `Timecop.freeze`
43+
RUBY
44+
end
45+
46+
it 'autocorrects to `freeze_time`' do
47+
expect(autocorrect_source('Timecop.freeze { }')).to(eq('freeze_time { }'))
48+
end
49+
end
50+
51+
context 'with arguments' do
52+
it 'adds an offense' do
53+
expect_offense(<<~RUBY)
54+
Timecop.freeze(123) { }
55+
^^^^^^^^^^^^^^^^^^^ Use `travel` or `travel_to` instead of `Timecop.freeze`
56+
RUBY
57+
end
58+
59+
# FIXME: Is this how NOT autocorrecting something should be tested?
60+
it 'does not autocorrect' do
61+
source = 'Timecop.freeze(123) { }'
62+
63+
expect(autocorrect_source(source)).to(eq(source))
64+
end
65+
end
66+
end
67+
end
68+
69+
describe 'Timecop.return' do
70+
context 'without a block' do
71+
it 'adds an offense' do
72+
expect_offense(<<~RUBY)
73+
Timecop.return
74+
^^^^^^^^^^^^^^ Use `travel_back` instead of `Timecop.return`
75+
RUBY
76+
end
77+
78+
it 'autocorrects to `travel_back`' do
79+
expect(autocorrect_source('Timecop.return')).to(eq('travel_back'))
80+
end
81+
end
82+
83+
context 'with a block' do
84+
it 'adds an offense' do
85+
expect_offense(<<~RUBY)
86+
Timecop.return { }
87+
^^^^^^^^^^^^^^ Use `travel_back` instead of `Timecop.return`
88+
RUBY
89+
end
90+
91+
it 'autocorrects to `travel_back`' do
92+
expect(autocorrect_source('Timecop.return { }')).to(eq('travel_back { }'))
93+
end
94+
end
95+
end
96+
97+
describe 'Timecop.travel' do
98+
it 'adds an offense' do
99+
expect_offense(<<~RUBY)
100+
Timecop.travel(123) { }
101+
^^^^^^^^^^^^^^^^^^^ Use `travel` or `travel_to` instead of `Timecop.travel`. If you need time to keep flowing, simulate it by travelling again.
102+
RUBY
103+
end
104+
end
105+
106+
describe 'Timecop.*' do
107+
it 'adds an offense' do
108+
expect_offense(<<~RUBY)
109+
Timecop.foo
110+
^^^^^^^ Use `ActiveSupport::Testing::TimeHelpers` instead of `Timecop`
111+
RUBY
112+
end
113+
end
114+
115+
describe 'Timecop' do
116+
it 'adds an offense' do
117+
expect_offense(<<~RUBY)
118+
Timecop.foo
119+
^^^^^^^ Use `ActiveSupport::Testing::TimeHelpers` instead of `Timecop`
120+
RUBY
121+
end
122+
end
123+
124+
describe '::Timecop' do
125+
it 'adds an offense' do
126+
expect_offense(<<~RUBY)
127+
::Timecop.foo
128+
^^^^^^^^^ Use `ActiveSupport::Testing::TimeHelpers` instead of `Timecop`
129+
RUBY
130+
end
131+
end
132+
end

0 commit comments

Comments
 (0)