Skip to content

Commit ee8766f

Browse files
committed
Add new Rails/MigrationTimestamp cop
This cop enforces that migration file names start with a valid timestamp.
1 parent b040d84 commit ee8766f

File tree

5 files changed

+130
-0
lines changed

5 files changed

+130
-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
@@ -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.'
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'
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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 `YYYYmmddHHMMSS_` timestamp.'
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)[/\A\d{14}(?=_)/]
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, format: '%Y%m%d%H%M%S')
41+
timestamp &&
42+
(time = Time.strptime(timestamp, format)) &&
43+
# Time.strptime fuzzily accepts invalid dates around boundaries
44+
# | Wrong Days per Month | 24th Hour | 60th Minute | 60th Second
45+
# ---------+----------------------+----------------+----------------+----------------
46+
# Actual | 20000231000000 | 20000101240000 | 20000101006000 | 20000101000060
47+
# Expected | 20000302000000 | 20000102000000 | 20000101010000 | 20000101000100
48+
# We want normalized values, so we can check if Time#strftime matches the original.
49+
time.strftime(format) == timestamp
50+
rescue ArgumentError
51+
false
52+
end
53+
end
54+
end
55+
end
56+
end

lib/rubocop/cop/rails_cops.rb

Lines changed: 1 addition & 0 deletions
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'
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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 `YYYYmmddHHMMSS_` timestamp.
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 `YYYYmmddHHMMSS_` timestamp.
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 `YYYYmmddHHMMSS_` timestamp.
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 `YYYYmmddHHMMSS_` timestamp.
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 `YYYYmmddHHMMSS_` timestamp.
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 `YYYYmmddHHMMSS_` timestamp.
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 `YYYYmmddHHMMSS_` timestamp.
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 `YYYYmmddHHMMSS_` timestamp.
63+
RUBY
64+
end
65+
end

0 commit comments

Comments
 (0)