Skip to content

Commit

Permalink
Add new Rails/MigrationTimestamp cop
Browse files Browse the repository at this point in the history
This cop enforces that migration file names start with a valid
timestamp.
  • Loading branch information
sambostock committed Jul 4, 2023
1 parent b040d84 commit ee8766f
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 0 deletions.
1 change: 1 addition & 0 deletions changelog/new_add_new_rails_migration_timestamp_cop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* [#1044](https://github.com/rubocop/rubocop-rails/pull/1044): Add new `Rails/MigrationTimestamp` cop. ([@sambostock][])
7 changes: 7 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,13 @@ Rails/MigrationClassName:
Include:
- db/**/*.rb

Rails/MigrationTimestamp:
Description: 'Checks that migration filenames start with a valid timestamp.'
Enabled: pending
VersionAdded: '<<next>>'
Include:
- db/migrate/**/*.rb

Rails/NegateInclude:
Description: 'Prefer `collection.exclude?(obj)` over `!collection.include?(obj)`.'
StyleGuide: 'https://rails.rubystyle.guide#exclude'
Expand Down
56 changes: 56 additions & 0 deletions lib/rubocop/cop/rails/migration_timestamp.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

require 'time'

module RuboCop
module Cop
module Rails
# Checks that migration file names start with a valid timestamp.
#
# @example
# # bad
# # db/migrate/bad.rb

# # bad
# # db/migrate/123_bad.rb

# # bad
# # db/migrate/20171301000000_bad.rb
#
# # good
# # db/migrate/20170101000000_good.rb
#
class MigrationTimestamp < Base
include RangeHelp

MSG = 'Migration file name must start with `YYYYmmddHHMMSS_` timestamp.'

def on_new_investigation
file_path = processed_source.file_path
return unless file_path.include?('db/migrate')

timestamp = File.basename(file_path)[/\A\d{14}(?=_)/]
return if valid_timestamp?(timestamp)

add_offense(source_range(processed_source.buffer, 1, 0))
end

private

def valid_timestamp?(timestamp, format: '%Y%m%d%H%M%S')
timestamp &&
(time = Time.strptime(timestamp, format)) &&
# Time.strptime fuzzily accepts invalid dates around boundaries
# | Wrong Days per Month | 24th Hour | 60th Minute | 60th Second
# ---------+----------------------+----------------+----------------+----------------
# Actual | 20000231000000 | 20000101240000 | 20000101006000 | 20000101000060
# Expected | 20000302000000 | 20000102000000 | 20000101010000 | 20000101000100
# We want normalized values, so we can check if Time#strftime matches the original.
time.strftime(format) == timestamp
rescue ArgumentError
false
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/rubocop/cop/rails_cops.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
require_relative 'rails/mailer_name'
require_relative 'rails/match_route'
require_relative 'rails/migration_class_name'
require_relative 'rails/migration_timestamp'
require_relative 'rails/negate_include'
require_relative 'rails/not_null_column'
require_relative 'rails/order_by_id'
Expand Down
65 changes: 65 additions & 0 deletions spec/rubocop/cop/rails/migration_timestamp_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::Rails::MigrationTimestamp, :config do
it 'registers no offenses if timestamp is valid' do
expect_no_offenses(<<~RUBY, 'db/migrate/20170101000000_good.rb')
# ...
RUBY
end

it 'registers an offense if timestamp is impossible' do
expect_offense(<<~RUBY, 'db/migrate/20002222222222_bad.rb')
# ...
^ Migration file name must start with `YYYYmmddHHMMSS_` timestamp.
RUBY
end

it 'registers an offense if timestamp swaps month and day' do
expect_offense(<<~RUBY, 'db/migrate/20003112000000_bad.rb')
# ...
^ Migration file name must start with `YYYYmmddHHMMSS_` timestamp.
RUBY
end

it 'registers an offense if timestamp day is wrong' do
expect_offense(<<~RUBY, 'db/migrate/20000231000000_bad.rb')
# ...
^ Migration file name must start with `YYYYmmddHHMMSS_` timestamp.
RUBY
end

it 'registers an offense if timestamp hours are invalid' do
expect_offense(<<~RUBY, 'db/migrate/20000101240000_bad.rb')
# ...
^ Migration file name must start with `YYYYmmddHHMMSS_` timestamp.
RUBY
end

it 'registers an offense if timestamp minutes are invalid' do
expect_offense(<<~RUBY, 'db/migrate/20000101006000_bad.rb')
# ...
^ Migration file name must start with `YYYYmmddHHMMSS_` timestamp.
RUBY
end

it 'registers an offense if timestamp seconds are invalid' do
expect_offense(<<~RUBY, 'db/migrate/20000101000060_bad.rb')
# ...
^ Migration file name must start with `YYYYmmddHHMMSS_` timestamp.
RUBY
end

it 'registers an offense if timestamp is invalid' do
expect_offense(<<~RUBY, 'db/migrate/123_bad.rb')
# ...
^ Migration file name must start with `YYYYmmddHHMMSS_` timestamp.
RUBY
end

it 'registers an offense if no timestamp at all' do
expect_offense(<<~RUBY, 'db/migrate/bad.rb')
# ...
^ Migration file name must start with `YYYYmmddHHMMSS_` timestamp.
RUBY
end
end

0 comments on commit ee8766f

Please sign in to comment.