Skip to content

Commit

Permalink
Add new Rails/DateAndTimeColumnName cop
Browse files Browse the repository at this point in the history
  • Loading branch information
fatkodima committed Dec 26, 2024
1 parent 048d2c7 commit d7f6c33
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 0 deletions.
1 change: 1 addition & 0 deletions changelog/new_add_new_date_and_time_column_name_cop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* [#335](https://github.com/rubocop/rubocop-rails/pull/335): Add new `Rails/DateAndTimeColumnName` cop. ([@fatkodima][])
11 changes: 11 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,17 @@ Rails/Date:
- flexible
AllowToTime: true

Rails/DateAndTimeColumnName:
Description: >-
Enforces date/time columns naming conventions.
Specifically, `date` column should end with `_on` suffix,
`datetime` and `timestamp` - with `_at` suffix,
`time` - with `_time` suffix.
Enabled: pending
VersionAdded: '<<next>>'
Include:
- db/**/*.rb

Rails/DefaultScope:
Description: 'Avoid use of `default_scope`.'
Enabled: false
Expand Down
61 changes: 61 additions & 0 deletions lib/rubocop/cop/rails/date_and_time_column_name.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Rails
# Enforces date/time columns naming conventions.
# Specifically, `date` column should end with `_on` suffix,
# `datetime` and `timestamp` - with `_at` suffix,
# `time` - with `_time` suffix.
#
# @example
# # bad
# add_column :orders, :created_on, :datetime
# t.column :visited_at, :date
# t.time :start_at
#
# # good
# add_column :orders, :created_at, :datetime
# t.column :visited_on, :date
# t.time :start_time
#
class DateAndTimeColumnName < Base
prepend MigrationsHelper

MSG = 'Name `%<type>s` columns with `%<suffix>s` suffixes.'
TYPE_TO_SUFFIX = { datetime: '_at', timestamp: '_at', date: '_on', time: '_time' }.freeze

def on_send(node)
return unless in_migration?(node)

column_name, type = column_name_and_type(node)
return unless column_name

suffix = TYPE_TO_SUFFIX[type]
return if !suffix || column_name.value.to_s.end_with?(suffix)

message = format(MSG, type: type, suffix: suffix)
add_offense(column_name, message: message)
end

private

def column_name_and_type(node)
case node.method_name
when :add_column
_table_name, column_name, type, = *node.arguments
type = type.value
when :column
column_name, type, = *node.arguments
type = type.value
when *TYPE_TO_SUFFIX.keys
column_name = node.first_argument
type = node.method_name
end

[column_name, type]
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 @@ -35,6 +35,7 @@
require_relative 'rails/create_table_with_timestamps'
require_relative 'rails/dangerous_column_names'
require_relative 'rails/date'
require_relative 'rails/date_and_time_column_name'
require_relative 'rails/default_scope'
require_relative 'rails/delegate'
require_relative 'rails/delegate_allow_blank'
Expand Down
157 changes: 157 additions & 0 deletions spec/rubocop/cop/rails/date_and_time_column_name_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::Rails::DateAndTimeColumnName, :config do
described_class::TYPE_TO_SUFFIX.each do |type, suffix|
context 'when `add_column` method' do
it "registers an offense for `#{type}` column without `#{suffix}` suffix" do
source = <<~RUBY
class AddColumnNameToTableName < ActiveRecord::Migration[6.0]
def change
add_column :table_name, :column_name_without_suffix, :#{type}
end
end
RUBY

offenses = inspect_source(source)
expect(offenses.count).to eq(1)
end

it "does not register an offense for `#{type}` column with `#{suffix}` suffix" do
expect_no_offenses(<<~RUBY)
class AddColumnNameToTableName < ActiveRecord::Migration[6.0]
def change
add_column :table_name, :column_name_#{suffix}, :#{type}
end
end
RUBY
end
end

context 'when `column` method' do
it "registers an offense for `#{type}` column without `#{suffix}` suffix" do
source = <<~RUBY
class AddColumnNameToTableName < ActiveRecord::Migration[6.0]
def change
create_table :table_name do |t|
t.column :column_name_without_suffix, :#{type}
end
end
end
RUBY

offenses = inspect_source(source)
expect(offenses.count).to eq(1)
end

it "does not register an offense for `#{type}` column with `#{suffix}` suffix" do
expect_no_offenses(<<~RUBY)
class AddColumnNameToTableName < ActiveRecord::Migration[6.0]
def change
create_table :table_name do |t|
t.column :column_name_#{suffix}, :#{type}
end
end
end
RUBY
end

it 'does not register an offense when non date column is used' do
expect_no_offenses(<<~RUBY)
class AddColumnNameToTableName < ActiveRecord::Migration[6.0]
def change
create_table :table_name do |t|
t.column :column_name, :integer
end
end
end
RUBY
end
end

context 'when `t.<type>` method' do
it "registers an offense for `#{type}` column without `#{suffix}` suffix" do
source = <<~RUBY
class AddColumnNameToTableName < ActiveRecord::Migration[6.0]
def change
create_table :table_name do |t|
t.#{type} :column_name_without_suffix
end
end
end
RUBY

offenses = inspect_source(source)
expect(offenses.count).to eq(1)
end

it "does not register an offense for `#{type}` column with `#{suffix}` suffix" do
expect_no_offenses(<<~RUBY)
class AddColumnNameToTableName < ActiveRecord::Migration[6.0]
def change
create_table :table_name do |t|
t.#{type} :column_name_#{suffix}
end
end
end
RUBY
end

it 'does not register an offense when non date column is used' do
expect_no_offenses(<<~RUBY)
class AddColumnNameToTableName < ActiveRecord::Migration[6.0]
def change
create_table :table_name do |t|
t.integer :column_name
end
end
end
RUBY
end
end
end

it 'does not register an offense when `add_column` with non date column is used' do
expect_no_offenses(<<~RUBY)
class AddColumnNameToTableName < ActiveRecord::Migration[6.0]
def change
add_column :table_name, :column_name, :integer
end
end
RUBY
end

it 'does not register an offense when not inside migration' do
expect_no_offenses(<<~RUBY)
class Foo
add_column :table_name, :column_name, :datetime
end
RUBY
end

context 'MigratedSchemaVersion is used' do
let(:config) do
RuboCop::Config.new('AllCops' => { 'MigratedSchemaVersion' => '20211007000001' })
end

it 'registers an offense for newer migrations' do
expect_offense(<<~RUBY, 'db/migrate/20211007000002_add_created_on_to_orders.rb')
class AddCreatedOnToOrders < ActiveRecord::Migration[6.0]
def change
add_column :orders, :created_on, :datetime
^^^^^^^^^^^ Name `datetime` columns with `_at` suffixes.
end
end
RUBY
end

it 'does not register an offense for older migrations' do
expect_no_offenses(<<~RUBY, 'db/migrate/20211007000001_add_column_to_table_name.rb')
class AddCreatedOnToOrders < ActiveRecord::Migration[6.0]
def change
add_column :orders, :created_on, :datetime
end
end
RUBY
end
end
end

0 comments on commit d7f6c33

Please sign in to comment.