Skip to content

Commit 5a42999

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 b040d84 commit 5a42999

File tree

5 files changed

+152
-0
lines changed

5 files changed

+152
-0
lines changed
+1
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][])

Diff for: config/default.yml

+7
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,13 @@ Rails/MigrationClassName:
656656
Include:
657657
- db/**/*.rb
658658

659+
Rails/MigrationTimestamp:
660+
Description: 'Checks that migration filenames start with a valid timestamp in the past.'
661+
Enabled: pending
662+
VersionAdded: '<<next>>'
663+
Include:
664+
- db/migrate/**/*.rb
665+
659666
Rails/NegateInclude:
660667
Description: 'Prefer `collection.exclude?(obj)` over `!collection.include?(obj)`.'
661668
StyleGuide: 'https://rails.rubystyle.guide#exclude'

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

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
return unless file_path.include?('db/migrate')
31+
32+
timestamp = File.basename(file_path).split('_', 2).first
33+
return if valid_timestamp?(timestamp)
34+
35+
add_offense(source_range(processed_source.buffer, 1, 0))
36+
end
37+
38+
private
39+
40+
def valid_timestamp?(timestamp)
41+
format = '%Y%m%d%H%M%S'
42+
format_with_utc_suffix = '%Y%m%d%H%M%S %Z'
43+
timestamp_with_utc_suffix = "#{timestamp} UTC"
44+
45+
timestamp &&
46+
# Time.strptime has no way to externally declare what timezone the string is in, so we append it.
47+
(time = Time.strptime(timestamp_with_utc_suffix, format_with_utc_suffix)) &&
48+
# Time.strptime fuzzily accepts invalid dates around boundaries
49+
# | Wrong Days per Month | 24th Hour | 60th Minute | 60th Second
50+
# ---------+----------------------+----------------+----------------+----------------
51+
# Actual | 20000231000000 | 20000101240000 | 20000101006000 | 20000101000060
52+
# Expected | 20000302000000 | 20000102000000 | 20000101010000 | 20000101000100
53+
# We want normalized values, so we can check if Time#strftime matches the original.
54+
time.strftime(format) == timestamp &&
55+
# No timestamps in the future
56+
time <= Time.now.utc
57+
rescue ArgumentError
58+
false
59+
end
60+
end
61+
end
62+
end
63+
end

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

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
require_relative 'rails/mailer_name'
7474
require_relative 'rails/match_route'
7575
require_relative 'rails/migration_class_name'
76+
require_relative 'rails/migration_timestamp'
7677
require_relative 'rails/negate_include'
7778
require_relative 'rails/not_null_column'
7879
require_relative 'rails/order_by_id'

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

+80
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)