Skip to content

Commit 2e46a36

Browse files
authored
Merge pull request #571 from pirj/time-arithmetic
Add `Rails/DurationArithmetic` cop
2 parents 847d0b8 + 0161972 commit 2e46a36

File tree

8 files changed

+178
-0
lines changed

8 files changed

+178
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,3 +482,4 @@
482482
[@skryukov]: https://github.com/skryukov
483483
[@johnsyweb]: https://github.com/johnsyweb
484484
[@theunraveler]: https://github.com/theunraveler
485+
[@pirj]: https://github.com/pirj
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#571](https://github.com/rubocop/rubocop-rails/issues/571): Add `Rails/DurationArithmetic` cop. ([@pirj][])

config/default.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,12 @@ Rails/DelegateAllowBlank:
245245
Enabled: true
246246
VersionAdded: '0.44'
247247

248+
Rails/DurationArithmetic:
249+
Description: 'Do not use duration as arithmetic operand with `Time.current`.'
250+
StyleGuide: 'https://rails.rubystyle.guide#duration-arithmetic'
251+
Enabled: pending
252+
VersionAdded: '2.13'
253+
248254
Rails/DynamicFindBy:
249255
Description: 'Use `find_by` instead of dynamic `find_by_*`.'
250256
StyleGuide: 'https://rails.rubystyle.guide#find_by'

docs/modules/ROOT/pages/cops.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ based on the https://rails.rubystyle.guide/[Rails Style Guide].
3939
* xref:cops_rails.adoc#railsdefaultscope[Rails/DefaultScope]
4040
* xref:cops_rails.adoc#railsdelegate[Rails/Delegate]
4141
* xref:cops_rails.adoc#railsdelegateallowblank[Rails/DelegateAllowBlank]
42+
* xref:cops_rails.adoc#railsdurationarithmetic[Rails/DurationArithmetic]
4243
* xref:cops_rails.adoc#railsdynamicfindby[Rails/DynamicFindBy]
4344
* xref:cops_rails.adoc#railseagerevaluationlogmessage[Rails/EagerEvaluationLogMessage]
4445
* xref:cops_rails.adoc#railsenumhash[Rails/EnumHash]

docs/modules/ROOT/pages/cops_rails.adoc

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,6 +1206,42 @@ delegate :foo, to: :bar, allow_blank: true
12061206
delegate :foo, to: :bar, allow_nil: true
12071207
----
12081208

1209+
== Rails/DurationArithmetic
1210+
1211+
|===
1212+
| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed
1213+
1214+
| Pending
1215+
| Yes
1216+
| Yes
1217+
| 2.13
1218+
| -
1219+
|===
1220+
1221+
This cop checks if a duration is added to or subtracted from `Time.current`.
1222+
1223+
=== Examples
1224+
1225+
[source,ruby]
1226+
----
1227+
# bad
1228+
Time.current - 1.minute
1229+
Time.current + 2.days
1230+
1231+
# good - using relative would make it harder to express and read
1232+
Date.yesterday + 3.days
1233+
created_at - 1.minute
1234+
3.days - 1.hour
1235+
1236+
# good
1237+
1.minute.ago
1238+
2.days.from_now
1239+
----
1240+
1241+
=== References
1242+
1243+
* https://rails.rubystyle.guide#duration-arithmetic
1244+
12091245
== Rails/DynamicFindBy
12101246

12111247
|===
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Rails
6+
# This cop checks if a duration is added to or subtracted from `Time.current`.
7+
#
8+
# @example
9+
# # bad
10+
# Time.current - 1.minute
11+
# Time.current + 2.days
12+
#
13+
# # good - using relative would make it harder to express and read
14+
# Date.yesterday + 3.days
15+
# created_at - 1.minute
16+
# 3.days - 1.hour
17+
#
18+
# # good
19+
# 1.minute.ago
20+
# 2.days.from_now
21+
class DurationArithmetic < Base
22+
extend AutoCorrector
23+
24+
MSG = 'Do not add or subtract duration.'
25+
26+
RESTRICT_ON_SEND = %i[+ -].freeze
27+
28+
DURATIONS = Set[:second, :seconds, :minute, :minutes, :hour, :hours,
29+
:day, :days, :week, :weeks, :fortnight, :fortnights]
30+
31+
# @!method duration_arithmetic_argument?(node)
32+
# Match duration subtraction or addition with current time.
33+
#
34+
# @example source that matches
35+
# Time.current - 1.hour
36+
#
37+
# @example source that matches
38+
# ::Time.zone.now + 1.hour
39+
#
40+
# @param node [RuboCop::AST::Node]
41+
# @yield operator and duration
42+
def_node_matcher :duration_arithmetic_argument?, <<~PATTERN
43+
(send #time_current? ${ :+ :- } $#duration?)
44+
PATTERN
45+
46+
# @!method duration?(node)
47+
# Match a literal Duration
48+
#
49+
# @example source that matches
50+
# 1.hour
51+
#
52+
# @example source that matches
53+
# 9.5.weeks
54+
#
55+
# @param node [RuboCop::AST::Node]
56+
# @return [Boolean] true if matches
57+
def_node_matcher :duration?, '(send { int float (send nil _) } DURATIONS)'
58+
59+
# @!method time_current?(node)
60+
# Match Time.current
61+
#
62+
# @example source that matches
63+
# Time.current
64+
#
65+
# @example source that matches
66+
# ::Time.zone.now
67+
#
68+
# @param node [RuboCop::AST::Node]
69+
# @return [Boolean] true if matches
70+
def_node_matcher :time_current?, <<~PATTERN
71+
{
72+
(send (const _ :Time) :current)
73+
(send (send (const _ :Time) :zone) :now)
74+
}
75+
PATTERN
76+
77+
def on_send(node)
78+
duration_arithmetic_argument?(node) do |*operation|
79+
add_offense(node) do |corrector|
80+
corrector.replace(node.source_range, corrected_source(*operation))
81+
end
82+
end
83+
end
84+
85+
private
86+
87+
def corrected_source(operator, duration)
88+
if operator == :-
89+
"#{duration.source}.ago"
90+
else
91+
"#{duration.source}.from_now"
92+
end
93+
end
94+
end
95+
end
96+
end
97+
end

lib/rubocop/cop/rails_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
require_relative 'rails/default_scope'
2929
require_relative 'rails/delegate'
3030
require_relative 'rails/delegate_allow_blank'
31+
require_relative 'rails/duration_arithmetic'
3132
require_relative 'rails/dynamic_find_by'
3233
require_relative 'rails/eager_evaluation_log_message'
3334
require_relative 'rails/enum_hash'
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::Rails::DurationArithmetic, :config do
4+
it 'registers an offense and corrects' do
5+
expect_offense(<<~RUBY)
6+
Time.zone.now - 1.minute
7+
^^^^^^^^^^^^^^^^^^^^^^^^ Do not add or subtract duration.
8+
Time.current + 2.days
9+
^^^^^^^^^^^^^^^^^^^^^ Do not add or subtract duration.
10+
::Time.current + 1.hour
11+
^^^^^^^^^^^^^^^^^^^^^^^ Do not add or subtract duration.
12+
RUBY
13+
14+
expect_correction(<<~RUBY)
15+
1.minute.ago
16+
2.days.from_now
17+
1.hour.from_now
18+
RUBY
19+
end
20+
21+
it 'does not register an offense for two duration operands' do
22+
expect_no_offenses(<<~RUBY)
23+
3.days - 1.hour
24+
3.days + 1.hour
25+
RUBY
26+
end
27+
28+
it 'does not register an offense if the left operand is non current time' do
29+
expect_no_offenses(<<~RUBY)
30+
5.hours + Time.current # will raise
31+
Date.yesterday + 3.days
32+
created_at - 1.minute
33+
RUBY
34+
end
35+
end

0 commit comments

Comments
 (0)