Skip to content

Commit 3fa9c7c

Browse files
committed
[Fix #221] Make Rails/UniqueValidationWithoutIndex aware of add_index
Fixes #221. This PR makes `Rails/UniqueValidationWithoutIndex` aware of `add_index` in db/schema.rb. Rails 4.0 and Rails 4.1 support will drop shortly, but Rails 4.2 may be a bit ahead. There may be changes to these support after the bug fix release of RoboCop Rails 2.5 series.
1 parent 3ba2b30 commit 3fa9c7c

File tree

5 files changed

+108
-9
lines changed

5 files changed

+108
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* [#213](https://github.com/rubocop-hq/rubocop-rails/pull/213): Fix a false positive for `Rails/UniqueValidationWithoutIndex` when using conditions. ([@sunny][])
88
* [#215](https://github.com/rubocop-hq/rubocop-rails/issues/215): Fix a false positive for `Rails/UniqueValidationWithoutIndex` when using Expression Indexes. ([@koic][])
99
* [#214](https://github.com/rubocop-hq/rubocop-rails/issues/214): Fix an error for `Rails/UniqueValidationWithoutIndex`when a table has no column definition. ([@koic][])
10+
* [#221](https://github.com/rubocop-hq/rubocop-rails/issues/221): Make `Rails/UniqueValidationWithoutIndex` aware of `add_index` in db/schema.rb. ([@koic][])
1011

1112
## 2.5.0 (2020-03-24)
1213

lib/rubocop/cop/rails/unique_validation_without_index.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ def with_index?(node)
5151
names = column_names(node)
5252
return true unless names
5353

54-
table.indices.any? do |index|
54+
# Compatibility for Rails 4.2.
55+
add_indicies = schema.add_indicies_by(table_name: table_name(klass))
56+
57+
(table.indices + add_indicies).any? do |index|
5558
index.unique &&
5659
(index.columns.to_set == names ||
5760
include_column_names_in_expression_index?(index, names))

lib/rubocop/rails/schema_loader/schema.rb

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ module Rails
55
module SchemaLoader
66
# Represent db/schema.rb
77
class Schema
8-
attr_reader :tables
8+
attr_reader :tables, :add_indicies
99

1010
def initialize(ast)
1111
@tables = []
12+
@add_indicies = []
13+
1214
build!(ast)
1315
end
1416

@@ -18,6 +20,12 @@ def table_by(name:)
1820
end
1921
end
2022

23+
def add_indicies_by(table_name:)
24+
add_indicies.select do |add_index|
25+
add_index.table_name == table_name
26+
end
27+
end
28+
2129
private
2230

2331
def build!(ast)
@@ -26,6 +34,11 @@ def build!(ast)
2634
each_table(ast) do |table_def|
2735
@tables << Table.new(table_def)
2836
end
37+
38+
# Compatibility for Rails 4.2.
39+
each_add_index(ast) do |add_index_def|
40+
@add_indicies << AddIndex.new(add_index_def)
41+
end
2942
end
3043

3144
def each_table(ast)
@@ -40,6 +53,14 @@ def each_table(ast)
4053
yield ast.body
4154
end
4255
end
56+
57+
def each_add_index(ast)
58+
ast.body.children.each do |node|
59+
next if !node&.send_type? || !node.method?(:add_index)
60+
61+
yield(node)
62+
end
63+
end
4364
end
4465

4566
# Represent a table
@@ -121,21 +142,19 @@ class Index
121142
attr_reader :name, :columns, :expression, :unique
122143

123144
def initialize(node)
124-
node.first_argument
125-
@columns, @expression = build_columns_or_expr(node)
145+
@columns, @expression = build_columns_or_expr(node.first_argument)
126146
@unique = nil
127147

128148
analyze_keywords!(node)
129149
end
130150

131151
private
132152

133-
def build_columns_or_expr(node)
134-
arg = node.first_argument
135-
if arg.array_type?
136-
[arg.values.map(&:value), nil]
153+
def build_columns_or_expr(columns)
154+
if columns.array_type?
155+
[columns.values.map(&:value), nil]
137156
else
138-
[[], arg.value]
157+
[[], columns.value]
139158
end
140159
end
141160

@@ -153,6 +172,19 @@ def analyze_keywords!(node)
153172
end
154173
end
155174
end
175+
176+
# Represent an `add_index`
177+
class AddIndex < Index
178+
attr_reader :table_name
179+
180+
def initialize(node)
181+
@table_name = node.first_argument.value
182+
@columns, @expression = build_columns_or_expr(node.arguments[1])
183+
@unique = nil
184+
185+
analyze_keywords!(node)
186+
end
187+
end
156188
end
157189
end
158190
end

spec/rubocop/cop/rails/unique_validation_without_index_spec.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,5 +464,46 @@ class Email < ApplicationRecord
464464
end
465465
end
466466
end
467+
468+
context 'when db/schema.rb has been dumped using `add_index` for index' do
469+
context 'when the table does not have any indices' do
470+
let(:schema) { <<~RUBY }
471+
ActiveRecord::Schema.define(version: 2020_02_02_075409) do
472+
create_table "users", force: :cascade do |t|
473+
t.string "account", null: false
474+
end
475+
add_index "users", "account", name: "index_users_on_account"
476+
end
477+
RUBY
478+
479+
it 'registers an offense' do
480+
expect_offense(<<~RUBY)
481+
class User
482+
validates :account, uniqueness: true
483+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Uniqueness validation should be with a unique index.
484+
end
485+
RUBY
486+
end
487+
end
488+
489+
context 'with a unique index' do
490+
let(:schema) { <<~RUBY }
491+
ActiveRecord::Schema.define(version: 2020_02_02_075409) do
492+
create_table "users", force: :cascade do |t|
493+
t.string "account", null: false
494+
end
495+
add_index "users", ["account"], name: "index_users_on_account", unique: true
496+
end
497+
RUBY
498+
499+
it 'does not register an offense' do
500+
expect_no_offenses(<<~RUBY)
501+
class User
502+
validates :account, uniqueness: true
503+
end
504+
RUBY
505+
end
506+
end
507+
end
467508
end
468509
end

spec/rubocop/rails/schema_loader_spec.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,28 @@
8585
expect(table.indices.first.name).to eq 'index_title_lower_unique'
8686
expect(table.indices.first.unique).to be true
8787
end
88+
89+
context 'when an index in users table specified by `add_index`' do
90+
let(:schema) { <<~RUBY }
91+
ActiveRecord::Schema.define(version: 2020_02_02_075409) do
92+
create_table "users", force: :cascade do |t|
93+
t.string "account", null: false
94+
end
95+
add_index "users", ["account"], name: "index_users_on_account", unique: true
96+
add_index "users", ["email"], name: "index_users_on_email", unique: true
97+
add_index "books", ["isbn"], name: "index_books_on_isbn", unique: true
98+
end
99+
RUBY
100+
101+
it 'has an `add_index` for users table' do
102+
add_indicies = loaded_schema.add_indicies_by(table_name: 'users')
103+
expect(add_indicies.size).to eq 2
104+
expect(add_indicies.first.name).to eq 'index_users_on_account'
105+
expect(add_indicies.first.table_name).to eq 'users'
106+
expect(add_indicies.first.columns).to eq ['account']
107+
expect(add_indicies.first.unique).to be true
108+
end
109+
end
88110
end
89111

90112
context 'when the current directory is Rails.root' do

0 commit comments

Comments
 (0)