-
-
Notifications
You must be signed in to change notification settings - Fork 263
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add new
Rails/RelativeDateGrammar
cop
This PR adds new `Rails/RelativeDateGrammar` cop. It checks whether the word orders of relative dates are grammatically easy to understand. This check includes detecting undefined methods on Date(Time) objects. ```ruby # bad tomorrow = Time.current.since(1.day) # good tomorrow = 1.day.since(Time.current) ```
- Loading branch information
Showing
5 changed files
with
99 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
* [#1106](https://github.com/rubocop/rubocop-rails/pull/1106): Add new `Rails/RelativeDateGrammar` cop. ([@aeroastro][]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# frozen_string_literal: true | ||
|
||
module RuboCop | ||
module Cop | ||
module Rails | ||
# Checks whether the word orders of relative dates are grammatically easy to understand. | ||
# This check includes detecting undefined methods on Date(Time) objects. | ||
# | ||
# @safety | ||
# This cop is unsafe because it avoids strict checking of receivers' types, | ||
# ActiveSupport::Duration and Date(Time) respectively. | ||
# | ||
# @example | ||
# # bad | ||
# tomorrow = Time.current.since(1.day) | ||
# | ||
# # good | ||
# tomorrow = 1.day.since(Time.current) | ||
class RelativeDateGrammar < Base | ||
extend AutoCorrector | ||
|
||
MSG = 'Use ActiveSupport::Duration#%<relation>s as a receiver ' \ | ||
'for relative date like `%<duration>s.%<relation>s(%<date>s)`.' | ||
|
||
RELATIVE_DATE_METHODS = %i[since from_now after ago until before].to_set.freeze | ||
DURATION_METHODS = %i[second seconds minute minutes hour hours | ||
day days week weeks month months year years].to_set.freeze | ||
|
||
RESTRICT_ON_SEND = RELATIVE_DATE_METHODS.to_a.freeze | ||
|
||
def_node_matcher :inverted_relative_date?, <<~PATTERN | ||
(send | ||
$!nil? | ||
$RELATIVE_DATE_METHODS | ||
$(send | ||
!nil? | ||
$DURATION_METHODS | ||
) | ||
) | ||
PATTERN | ||
|
||
def on_send(node) | ||
inverted_relative_date?(node) do |date, relation, duration| | ||
message = format(MSG, date: date.source, relation: relation.to_s, duration: duration.source) | ||
add_offense(node, message: message) do |corrector| | ||
autocorrect(corrector, node, date, relation, duration) | ||
end | ||
end | ||
end | ||
|
||
private | ||
|
||
def autocorrect(corrector, node, date, relation, duration) | ||
new_code = ["#{duration.source}.#{relation}(#{date.source})"] | ||
corrector.replace(node, new_code) | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# frozen_string_literal: true | ||
|
||
RSpec.describe RuboCop::Cop::Rails::RelativeDateGrammar, :config do | ||
it 'accepts ActiveSupport::Duration as a receiver (ActiveSupport::Duration#since)' do | ||
expect_no_offenses(<<~RUBY) | ||
yesterday = 1.day.since(Time.current) | ||
RUBY | ||
end | ||
|
||
it 'registers an offense for Date(Time) as a receiver (ActiveSupport::TimeWithZone#ago)' do | ||
expect_offense(<<~RUBY) | ||
last_week = Time.current.ago(1.week) | ||
^^^^^^^^^^^^^^^^^^^^^^^^ Use ActiveSupport::Duration#ago as a receiver for relative date like `1.week.ago(Time.current)`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
last_week = 1.week.ago(Time.current) | ||
RUBY | ||
end | ||
|
||
it 'registers an offense when a receiver is presumably Date(Time)' do | ||
expect_offense(<<~RUBY) | ||
expiration_time = purchase.created_at.since(ticket.expires_in.seconds) | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use ActiveSupport::Duration#since as a receiver for relative date like `ticket.expires_in.seconds.since(purchase.created_at)`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
expiration_time = ticket.expires_in.seconds.since(purchase.created_at) | ||
RUBY | ||
end | ||
end |