Skip to content

Commit 64e1260

Browse files
committed
Add new Rails/MigrationTimestamp cop
This cop enforces that migration file names start with a valid timestamp in the past.
1 parent 1be8d21 commit 64e1260

File tree

5 files changed

+149
-0
lines changed

5 files changed

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

config/default.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,13 @@ Rails/MigrationClassName:
682682
Include:
683683
- db/**/*.rb
684684

685+
Rails/MigrationTimestamp:
686+
Description: 'Checks that migration filenames start with a valid timestamp in the past.'
687+
Enabled: pending
688+
VersionAdded: '<<next>>'
689+
Include:
690+
- db/migrate/**/*.rb
691+
685692
Rails/NegateInclude:
686693
Description: 'Prefer `collection.exclude?(obj)` over `!collection.include?(obj)`.'
687694
StyleGuide: 'https://rails.rubystyle.guide#exclude'
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# frozen_string_literal: true
2+
3+
require 'time'
4+
5+
module RuboCop
6+
module Cop
7+
module Rails
8+
# Checks that migration file names start with a valid timestamp.
9+
#
10+
# @example
11+
# # bad
12+
# # db/migrate/bad.rb
13+
14+
# # bad
15+
# # db/migrate/123_bad.rb
16+
17+
# # bad
18+
# # db/migrate/20171301000000_bad.rb
19+
#
20+
# # good
21+
# # db/migrate/20170101000000_good.rb
22+
#
23+
class MigrationTimestamp < Base
24+
include RangeHelp
25+
26+
MSG = 'Migration file name must start with a valid `YYYYmmddHHMMSS_` timestamp in the past.'
27+
28+
def on_new_investigation
29+
file_path = processed_source.file_path
30+
timestamp = File.basename(file_path).split('_', 2).first
31+
return if valid_timestamp?(timestamp)
32+
33+
add_offense(source_range(processed_source.buffer, 1, 0))
34+
end
35+
36+
private
37+
38+
def valid_timestamp?(timestamp, format: '%Y%m%d%H%M%S')
39+
format_with_utc_suffix = "#{format} %Z"
40+
timestamp_with_utc_suffix = "#{timestamp} UTC"
41+
42+
timestamp &&
43+
# Time.strptime has no way to externally declare what timezone the string is in, so we append it.
44+
(time = Time.strptime(timestamp_with_utc_suffix, format_with_utc_suffix)) &&
45+
# Time.strptime fuzzily accepts invalid dates around boundaries
46+
# | Wrong Days per Month | 24th Hour | 60th Minute | 60th Second
47+
# ---------+----------------------+----------------+----------------+----------------
48+
# Actual | 20000231000000 | 20000101240000 | 20000101006000 | 20000101000060
49+
# Expected | 20000302000000 | 20000102000000 | 20000101010000 | 20000101000100
50+
# We want normalized values, so we can check if Time#strftime matches the original.
51+
time.strftime(format) == timestamp &&
52+
# No timestamps in the future
53+
time <= Time.now.utc
54+
rescue ArgumentError
55+
false
56+
end
57+
end
58+
end
59+
end
60+
end

lib/rubocop/cop/rails_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
require_relative 'rails/mailer_name'
7777
require_relative 'rails/match_route'
7878
require_relative 'rails/migration_class_name'
79+
require_relative 'rails/migration_timestamp'
7980
require_relative 'rails/negate_include'
8081
require_relative 'rails/not_null_column'
8182
require_relative 'rails/order_by_id'
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::Rails::MigrationTimestamp, :config do
4+
it 'registers no offenses if timestamp is valid' do
5+
expect_no_offenses(<<~RUBY, 'db/migrate/20170101000000_good.rb')
6+
# ...
7+
RUBY
8+
end
9+
10+
it 'registers an offense if timestamp is impossible' do
11+
expect_offense(<<~RUBY, 'db/migrate/20002222222222_bad.rb')
12+
# ...
13+
^ Migration file name must start with a valid `YYYYmmddHHMMSS_` timestamp in the past.
14+
RUBY
15+
end
16+
17+
it 'registers an offense if timestamp swaps month and day' do
18+
expect_offense(<<~RUBY, 'db/migrate/20003112000000_bad.rb')
19+
# ...
20+
^ Migration file name must start with a valid `YYYYmmddHHMMSS_` timestamp in the past.
21+
RUBY
22+
end
23+
24+
it 'registers an offense if timestamp day is wrong' do
25+
expect_offense(<<~RUBY, 'db/migrate/20000231000000_bad.rb')
26+
# ...
27+
^ Migration file name must start with a valid `YYYYmmddHHMMSS_` timestamp in the past.
28+
RUBY
29+
end
30+
31+
it 'registers an offense if timestamp hours are invalid' do
32+
expect_offense(<<~RUBY, 'db/migrate/20000101240000_bad.rb')
33+
# ...
34+
^ Migration file name must start with a valid `YYYYmmddHHMMSS_` timestamp in the past.
35+
RUBY
36+
end
37+
38+
it 'registers an offense if timestamp minutes are invalid' do
39+
expect_offense(<<~RUBY, 'db/migrate/20000101006000_bad.rb')
40+
# ...
41+
^ Migration file name must start with a valid `YYYYmmddHHMMSS_` timestamp in the past.
42+
RUBY
43+
end
44+
45+
it 'registers an offense if timestamp seconds are invalid' do
46+
expect_offense(<<~RUBY, 'db/migrate/20000101000060_bad.rb')
47+
# ...
48+
^ Migration file name must start with a valid `YYYYmmddHHMMSS_` timestamp in the past.
49+
RUBY
50+
end
51+
52+
it 'registers an offense if timestamp is invalid' do
53+
expect_offense(<<~RUBY, 'db/migrate/123_bad.rb')
54+
# ...
55+
^ Migration file name must start with a valid `YYYYmmddHHMMSS_` timestamp in the past.
56+
RUBY
57+
end
58+
59+
it 'registers an offense if no timestamp at all' do
60+
expect_offense(<<~RUBY, 'db/migrate/bad.rb')
61+
# ...
62+
^ Migration file name must start with a valid `YYYYmmddHHMMSS_` timestamp in the past.
63+
RUBY
64+
end
65+
66+
it 'registers an offense if the timestamp is in the future' do
67+
timestamp = (Time.now.utc + 5).strftime('%Y%m%d%H%M%S')
68+
expect_offense(<<~RUBY, "db/migrate/#{timestamp}_bad.rb")
69+
# ...
70+
^ Migration file name must start with a valid `YYYYmmddHHMMSS_` timestamp in the past.
71+
RUBY
72+
end
73+
74+
it 'registers no offense if the timestamp is in the past' do
75+
timestamp = (Time.now.utc - 5).strftime('%Y%m%d%H%M%S')
76+
expect_no_offenses(<<~RUBY, "db/migrate/#{timestamp}_good.rb")
77+
# ...
78+
RUBY
79+
end
80+
end

0 commit comments

Comments
 (0)