Skip to content

Commit 8e0dec9

Browse files
committed
Add new Rails/RelativeDateGrammar cop
This PR adds new `Rails/RelativeDateGrammar` cop. It checks whether the word orders of a relative dates are grammatically easy to understand. ```ruby # bad tomorrow = Time.current.since(1.day) # good tomorrow = 1.day.since(Time.current) ```
1 parent baf39e6 commit 8e0dec9

File tree

5 files changed

+98
-0
lines changed

5 files changed

+98
-0
lines changed

Diff for: changelog/new_add_rails_relative_date_grammar_cop

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#1106](https://github.com/rubocop/rubocop-rails/pull/1106): Add new `Rails/RelativeDateGrammar` cop. ([@aeroastro][])

Diff for: config/default.yml

+6
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,12 @@ Rails/RelativeDateConstant:
861861
VersionAdded: '0.48'
862862
VersionChanged: '2.13'
863863

864+
Rails/RelativeDateGrammar:
865+
Description: 'Prefer ActiveSupport::Duration as a receiver for a relative date like `1.day.since(Time.current)`.'
866+
Enabled: pending
867+
Safe: false
868+
VersionAdded: '<<next>>'
869+
864870
Rails/RenderInline:
865871
Description: 'Prefer using a template over inline rendering.'
866872
StyleGuide: 'https://rails.rubystyle.guide/#inline-rendering'

Diff for: lib/rubocop/cop/rails/relative_date_grammar.rb

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Rails
6+
# Checks whether the word orders of relative dates are grammatically easy to understand.
7+
#
8+
# @safety
9+
# This cop is unsafe because it avoids strict checking of receivers' types,
10+
# ActiveSupport::Duration and Date(Time) respectively.
11+
#
12+
# @example
13+
# # bad
14+
# tomorrow = Time.current.since(1.day)
15+
#
16+
# # good
17+
# tomorrow = 1.day.since(Time.current)
18+
class RelativeDateGrammar < Base
19+
extend AutoCorrector
20+
21+
MSG = 'Prefer ActiveSupport::Duration#%<relation>s as a receiver ' \
22+
'for relative date like `%<duration>s.%<relation>s(%<date>s)`.'
23+
24+
RELATIVE_DATE_METHODS = %i[since from_now after ago until before].to_set.freeze
25+
DURATION_METHODS = %i[second seconds minute minutes hour hours
26+
day days week weeks month months year years].to_set.freeze
27+
28+
RESTRICT_ON_SEND = RELATIVE_DATE_METHODS.to_a.freeze
29+
30+
def_node_matcher :inverted_relative_date?, <<~PATTERN
31+
(send
32+
$!nil?
33+
$RELATIVE_DATE_METHODS
34+
$(send
35+
!nil?
36+
$DURATION_METHODS
37+
)
38+
)
39+
PATTERN
40+
41+
def on_send(node)
42+
inverted_relative_date?(node) do |date, relation, duration|
43+
message = format(MSG, date: date.source, relation: relation.to_s, duration: duration.source)
44+
add_offense(node, message: message) do |corrector|
45+
autocorrect(corrector, node, date, relation, duration)
46+
end
47+
end
48+
end
49+
50+
private
51+
52+
def autocorrect(corrector, node, date, relation, duration)
53+
new_code = ["#{duration.source}.#{relation}(#{date.source})"]
54+
corrector.replace(node, new_code)
55+
end
56+
end
57+
end
58+
end
59+
end

Diff for: lib/rubocop/cop/rails_cops.rb

+1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
require_relative 'rails/reflection_class_name'
9898
require_relative 'rails/refute_methods'
9999
require_relative 'rails/relative_date_constant'
100+
require_relative 'rails/relative_date_grammar'
100101
require_relative 'rails/render_inline'
101102
require_relative 'rails/render_plain_text'
102103
require_relative 'rails/request_referer'

Diff for: spec/rubocop/cop/rails/relative_date_grammar_spec.rb

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::Rails::RelativeDateGrammar, :config do
4+
it 'accepts ActiveSupport::Duration as a receiver (ActiveSupport::Duration#since)' do
5+
expect_no_offenses(<<~RUBY)
6+
yesterday = 1.day.since(Time.current)
7+
RUBY
8+
end
9+
10+
it 'registers an offense for Date(Time) as a receiver (ActiveSupport::TimeWithZone#ago)' do
11+
expect_offense(<<~RUBY)
12+
last_week = Time.current.ago(1.week)
13+
^^^^^^^^^^^^^^^^^^^^^^^^ Prefer ActiveSupport::Duration#ago as a receiver for relative date like `1.week.ago(Time.current)`.
14+
RUBY
15+
16+
expect_correction(<<~RUBY)
17+
last_week = 1.week.ago(Time.current)
18+
RUBY
19+
end
20+
21+
it 'registers an offense when a receiver is presumably Date(Time)' do
22+
expect_offense(<<~RUBY)
23+
expiration_time = purchase.created_at.since(ticket.expires_in.seconds)
24+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer ActiveSupport::Duration#since as a receiver for relative date like `ticket.expires_in.seconds.since(purchase.created_at)`.
25+
RUBY
26+
27+
expect_correction(<<~RUBY)
28+
expiration_time = ticket.expires_in.seconds.since(purchase.created_at)
29+
RUBY
30+
end
31+
end

0 commit comments

Comments
 (0)