sevencop 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +6 -0
- data/Gemfile.lock +13 -1
- data/README.md +17 -4
- data/config/default.yml +134 -0
- data/lib/rubocop/cop/sevencop/factory_bot_association_option.rb +1 -1
- data/lib/rubocop/cop/sevencop/factory_bot_association_style.rb +31 -31
- data/lib/rubocop/cop/sevencop/rails_inferred_spec_type.rb +1 -1
- data/lib/rubocop/cop/sevencop/rails_migration_add_check_constraint.rb +111 -0
- data/lib/rubocop/cop/sevencop/rails_migration_add_column_with_default_value.rb +229 -0
- data/lib/rubocop/cop/sevencop/rails_migration_add_foreign_key.rb +166 -0
- data/lib/rubocop/cop/sevencop/rails_migration_add_index_concurrently.rb +164 -0
- data/lib/rubocop/cop/sevencop/rails_migration_batch_in_batches.rb +95 -0
- data/lib/rubocop/cop/sevencop/rails_migration_batch_in_transaction.rb +83 -0
- data/lib/rubocop/cop/sevencop/rails_migration_batch_with_throttling.rb +108 -0
- data/lib/rubocop/cop/sevencop/rails_migration_change_column.rb +113 -0
- data/lib/rubocop/cop/sevencop/rails_migration_change_column_null.rb +128 -0
- data/lib/rubocop/cop/sevencop/rails_migration_create_table_force.rb +89 -0
- data/lib/rubocop/cop/sevencop/rails_migration_jsonb.rb +131 -0
- data/lib/rubocop/cop/sevencop/rails_migration_remove_column.rb +258 -0
- data/lib/rubocop/cop/sevencop/rails_migration_rename_column.rb +81 -0
- data/lib/rubocop/cop/sevencop/rails_migration_rename_table.rb +79 -0
- data/lib/rubocop/cop/sevencop/rails_migration_reserved_word_mysql.rb +2 -19
- data/lib/rubocop/cop/sevencop/rails_migration_unique_index_columns_count.rb +92 -0
- data/lib/sevencop/config_loader.rb +11 -10
- data/lib/sevencop/cop_concerns/batch_processing.rb +32 -0
- data/lib/sevencop/cop_concerns/column_type_method.rb +26 -0
- data/lib/sevencop/cop_concerns/disable_ddl_transaction.rb +49 -0
- data/lib/sevencop/cop_concerns.rb +3 -0
- data/lib/sevencop/rubocop_extension.rb +6 -1
- data/lib/sevencop/version.rb +1 -1
- data/lib/sevencop.rb +15 -0
- data/sevencop.gemspec +1 -0
- metadata +34 -2
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Sevencop
|
6
|
+
# Disable transaction in batch processing.
|
7
|
+
#
|
8
|
+
# To avoid locking the table.
|
9
|
+
#
|
10
|
+
# @safety
|
11
|
+
# There are some cases where transaction is really needed.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# # bad
|
15
|
+
# class AddSomeColumnToUsersThenBackfillSomeColumn < ActiveRecord::Migration[7.0]
|
16
|
+
# def change
|
17
|
+
# add_column :users, :some_column, :text
|
18
|
+
# User.update_all(some_column: 'some value')
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# # good
|
23
|
+
# class AddSomeColumnToUsers < ActiveRecord::Migration[7.0]
|
24
|
+
# def change
|
25
|
+
# add_column :users, :some_column, :text
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# class BackfillSomeColumnToUsers < ActiveRecord::Migration[7.0]
|
30
|
+
# disable_ddl_transaction!
|
31
|
+
#
|
32
|
+
# def up
|
33
|
+
# User.unscoped.in_batches do |relation|
|
34
|
+
# relation.update_all(some_column: 'some value')
|
35
|
+
# sleep(0.01)
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
class RailsMigrationBatchInTransaction < RuboCop::Cop::Base
|
40
|
+
extend AutoCorrector
|
41
|
+
|
42
|
+
include ::Sevencop::CopConcerns::BatchProcessing
|
43
|
+
include ::Sevencop::CopConcerns::DisableDdlTransaction
|
44
|
+
|
45
|
+
MSG = 'Disable transaction in batch processing.'
|
46
|
+
|
47
|
+
RESTRICT_ON_SEND = %i[
|
48
|
+
delete_all
|
49
|
+
update_all
|
50
|
+
].freeze
|
51
|
+
|
52
|
+
# @param node [RuboCop::AST::SendNode]
|
53
|
+
# @return [void]
|
54
|
+
def on_send(node)
|
55
|
+
return unless wrong?(node)
|
56
|
+
|
57
|
+
add_offense(node) do |corrector|
|
58
|
+
autocorrect(corrector, node)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# @param corrector [RuboCop::Cop::Corrector]
|
65
|
+
# @param node [RuboCop::AST::SendNode]
|
66
|
+
# @return [void]
|
67
|
+
def autocorrect(
|
68
|
+
corrector,
|
69
|
+
node
|
70
|
+
)
|
71
|
+
insert_disable_ddl_transaction(corrector, node)
|
72
|
+
end
|
73
|
+
|
74
|
+
# @param node [RuboCop::AST::SendNode]
|
75
|
+
# @return [Boolean]
|
76
|
+
def wrong?(node)
|
77
|
+
batch_processing?(node) &&
|
78
|
+
!within_disable_ddl_transaction?(node)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Sevencop
|
6
|
+
# Use throttling in batch processing.
|
7
|
+
#
|
8
|
+
# @safety
|
9
|
+
# There are some cases where we should not do throttling,
|
10
|
+
# or the throttling might be already done in a way that we cannot detect.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# # bad
|
14
|
+
# class BackfillSomeColumn < ActiveRecord::Migration[7.0]
|
15
|
+
# disable_ddl_transaction!
|
16
|
+
#
|
17
|
+
# def change
|
18
|
+
# User.in_batches do |relation|
|
19
|
+
# relation.update_all(some_column: 'some value')
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# # good
|
25
|
+
# class BackfillSomeColumnToUsers < ActiveRecord::Migration[7.0]
|
26
|
+
# disable_ddl_transaction!
|
27
|
+
#
|
28
|
+
# def up
|
29
|
+
# User.in_batches do |relation|
|
30
|
+
# relation.update_all(some_column: 'some value')
|
31
|
+
# sleep(0.01)
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
class RailsMigrationBatchWithThrottling < RuboCop::Cop::Base
|
36
|
+
extend AutoCorrector
|
37
|
+
|
38
|
+
include ::Sevencop::CopConcerns::BatchProcessing
|
39
|
+
|
40
|
+
MSG = 'Use throttling in batch processing.'
|
41
|
+
|
42
|
+
RESTRICT_ON_SEND = %i[
|
43
|
+
delete_all
|
44
|
+
update_all
|
45
|
+
].freeze
|
46
|
+
|
47
|
+
# @param node [RuboCop::AST::SendNode]
|
48
|
+
# @return [void]
|
49
|
+
def on_send(node)
|
50
|
+
return unless wrong?(node)
|
51
|
+
|
52
|
+
add_offense(node) do |corrector|
|
53
|
+
autocorrect(corrector, node)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# @!method sleep?(node)
|
60
|
+
# @param node [RuboCop::AST::Node]
|
61
|
+
# @return [Boolean]
|
62
|
+
def_node_matcher :sleep?, <<~PATTERN
|
63
|
+
(send
|
64
|
+
nil?
|
65
|
+
:sleep
|
66
|
+
...
|
67
|
+
)
|
68
|
+
PATTERN
|
69
|
+
|
70
|
+
# @param corrector [RuboCop::Cop::Corrector]
|
71
|
+
# @param node [RuboCop::AST::SendNode]
|
72
|
+
# @return [void]
|
73
|
+
def autocorrect(
|
74
|
+
corrector,
|
75
|
+
node
|
76
|
+
)
|
77
|
+
corrector.insert_after(
|
78
|
+
node,
|
79
|
+
"\n#{' ' * node.location.column}sleep(0.01)"
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
# @param node [RuboCop::AST::Node]
|
84
|
+
# @return [Boolean]
|
85
|
+
def in_block?(node)
|
86
|
+
node.parent&.block_type? ||
|
87
|
+
(node.parent&.begin_type? && node.parent.parent&.block_type?)
|
88
|
+
end
|
89
|
+
|
90
|
+
# @param node [RuboCop::AST::SendNode]
|
91
|
+
# @return [Boolean]
|
92
|
+
def with_throttling?(node)
|
93
|
+
(node.left_siblings + node.right_siblings).any? do |sibling|
|
94
|
+
sleep?(sibling)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# @param node [RuboCop::AST::SendNode]
|
99
|
+
# @return [Boolean]
|
100
|
+
def wrong?(node)
|
101
|
+
batch_processing?(node) &&
|
102
|
+
in_block?(node) &&
|
103
|
+
!with_throttling?(node)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Sevencop
|
6
|
+
# Avoid changing column type that is in use.
|
7
|
+
#
|
8
|
+
# Changing the type of a column causes the entire table to be rewritten.
|
9
|
+
# During this time, reads and writes are blocked in Postgres, and writes are blocked in MySQL and MariaDB.
|
10
|
+
#
|
11
|
+
# Some changes don’t require a table rewrite and are safe in PostgreSQL:
|
12
|
+
#
|
13
|
+
# Type | Safe Changes
|
14
|
+
# --- | ---
|
15
|
+
# `cidr` | Changing to `inet`
|
16
|
+
# `citext` | Changing to `text` if not indexed, changing to `string` with no `:limit` if not indexed
|
17
|
+
# `datetime` | Increasing or removing `:precision`, changing to `timestamptz` when session time zone is UTC in Postgres 12+
|
18
|
+
# `decimal` | Increasing `:precision` at same `:scale`, removing `:precision` and `:scale`
|
19
|
+
# `interval` | Increasing or removing `:precision`
|
20
|
+
# `numeric` | Increasing `:precision` at same `:scale`, removing `:precision` and `:scale`
|
21
|
+
# `string` | Increasing or removing `:limit`, changing to `text`, changing `citext` if not indexed
|
22
|
+
# `text` | Changing to `string` with no `:limit`, changing to `citext` if not indexed
|
23
|
+
# `time` | Increasing or removing `:precision`
|
24
|
+
# `timestamptz` | Increasing or removing `:limit`, changing to `datetime` when session time zone is UTC in Postgres 12+
|
25
|
+
#
|
26
|
+
# And some in MySQL and MariaDB:
|
27
|
+
#
|
28
|
+
# Type | Safe Changes
|
29
|
+
# --- | ---
|
30
|
+
# `string` | Increasing `:limit` from under 255 up to 255, increasing `:limit` from over 255 to the max
|
31
|
+
#
|
32
|
+
# A safer approach is to:
|
33
|
+
#
|
34
|
+
# 1. Create a new column
|
35
|
+
# 2. Write to both columns
|
36
|
+
# 3. Backfill data from the old column to the new column
|
37
|
+
# 4. Move reads from the old column to the new column
|
38
|
+
# 5. Stop writing to the old column
|
39
|
+
# 6. Drop the old column
|
40
|
+
#
|
41
|
+
# @safety
|
42
|
+
# Only meaningful if the table is in use and the type change is really unsafe as described above.
|
43
|
+
#
|
44
|
+
# @example
|
45
|
+
# # bad
|
46
|
+
# class ChangeUsersSomeColumnType < ActiveRecord::Migration[7.0]
|
47
|
+
# def change
|
48
|
+
# change_column :users, :some_column, :new_type
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# # good
|
53
|
+
# class AddUsersAnotherColumn < ActiveRecord::Migration[7.0]
|
54
|
+
# def change
|
55
|
+
# add_column :users, :another_column, :new_type
|
56
|
+
# end
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# class RemoveUsersSomeColumn < ActiveRecord::Migration[7.0]
|
60
|
+
# def change
|
61
|
+
# remove_column :users, :some_column
|
62
|
+
# end
|
63
|
+
# end
|
64
|
+
class RailsMigrationChangeColumn < RuboCop::Cop::Base
|
65
|
+
MSG = 'Avoid changing column type that is in use.'
|
66
|
+
|
67
|
+
RESTRICT_ON_SEND = %i[
|
68
|
+
change
|
69
|
+
change_column
|
70
|
+
].freeze
|
71
|
+
|
72
|
+
# @param node [RuboCop::AST::SendNode]
|
73
|
+
# @return [void]
|
74
|
+
def on_send(node)
|
75
|
+
return unless bad?(node)
|
76
|
+
|
77
|
+
add_offense(node)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
# @!method change?(node)
|
83
|
+
# @param node [RuboCop::AST::SendNode]
|
84
|
+
# @return [Boolean]
|
85
|
+
def_node_matcher :change?, <<~PATTERN
|
86
|
+
(send
|
87
|
+
lvar
|
88
|
+
:change
|
89
|
+
...
|
90
|
+
)
|
91
|
+
PATTERN
|
92
|
+
|
93
|
+
# @!method change_column?(node)
|
94
|
+
# @param node [RuboCop::AST::SendNode]
|
95
|
+
# @return [Boolean]
|
96
|
+
def_node_matcher :change_column?, <<~PATTERN
|
97
|
+
(send
|
98
|
+
nil?
|
99
|
+
:change_column
|
100
|
+
...
|
101
|
+
)
|
102
|
+
PATTERN
|
103
|
+
|
104
|
+
# @param node [RuboCop::AST::SendNode]
|
105
|
+
# @return [Boolean]
|
106
|
+
def bad?(node)
|
107
|
+
change?(node) ||
|
108
|
+
change_column?(node)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Sevencop
|
6
|
+
# Avoid simply setting `NOT NULL` constraint on an existing column in PostgreSQL.
|
7
|
+
#
|
8
|
+
# It blocks reads and writes while every row is checked.
|
9
|
+
# In PostgreSQL 12+, you can safely set `NOT NULL` constraint if corresponding check constraint exists.
|
10
|
+
#
|
11
|
+
# @safety
|
12
|
+
# Only meaningful in PostgreSQL 12+.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# # bad
|
16
|
+
# class SetNotNullColumnConstraintToUsersName < ActiveRecord::Migration[7.0]
|
17
|
+
# def change
|
18
|
+
# change_column_null :users, :name, false
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# # good
|
23
|
+
# class SetNotNullCheckConstraintToUsersName < ActiveRecord::Migration[7.0]
|
24
|
+
# def change
|
25
|
+
# add_check_constraint :users, 'name IS NOT NULL', name: 'users_name_is_not_null', validate: false
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# class ReplaceNotNullConstraintOnUsersName < ActiveRecord::Migration[7.0]
|
30
|
+
# def change
|
31
|
+
# validate_constraint :users, name: 'users_name_is_not_null'
|
32
|
+
# change_column_null :users, :name, false
|
33
|
+
# remove_check_constraint :users, name: 'users_name_is_not_null'
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
class RailsMigrationChangeColumnNull < RuboCop::Cop::Base
|
37
|
+
extend AutoCorrector
|
38
|
+
|
39
|
+
MSG = 'Avoid simply setting `NOT NULL` constraint on an existing column in PostgreSQL.'
|
40
|
+
|
41
|
+
RESTRICT_ON_SEND = %i[
|
42
|
+
change_column_null
|
43
|
+
].freeze
|
44
|
+
|
45
|
+
# @param node [RuboCop::AST::SendNode]
|
46
|
+
# @return [void]
|
47
|
+
def on_send(node)
|
48
|
+
return if in_second_migration?(node)
|
49
|
+
|
50
|
+
add_offense(node) do |corrector|
|
51
|
+
autocorrect(corrector, node)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# @!method parse_table_name_and_column_name(node)
|
58
|
+
# @param node [RuboCop::AST::SendNode]
|
59
|
+
# @return [Array<Symbol>, nil]
|
60
|
+
def_node_matcher :parse_table_name_and_column_name, <<~PATTERN
|
61
|
+
(send
|
62
|
+
nil?
|
63
|
+
_
|
64
|
+
({str sym} $_)
|
65
|
+
({str sym} $_)
|
66
|
+
...
|
67
|
+
)
|
68
|
+
PATTERN
|
69
|
+
|
70
|
+
# @!method remove_check_constraint?(node)
|
71
|
+
# @param node [RuboCop::AST::SendNode]
|
72
|
+
# @return [Boolean]
|
73
|
+
def_node_matcher :remove_check_constraint?, <<~PATTERN
|
74
|
+
(send nil? :remove_check_constraint ...)
|
75
|
+
PATTERN
|
76
|
+
|
77
|
+
# @!method validate_constraint?(node)
|
78
|
+
# @param node [RuboCop::AST::SendNode]
|
79
|
+
# @return [Boolean]
|
80
|
+
def_node_matcher :validate_constraint?, <<~PATTERN
|
81
|
+
(send nil? :validate_constraint ...)
|
82
|
+
PATTERN
|
83
|
+
|
84
|
+
# @param corrector [RuboCop::Cop::Corrector]
|
85
|
+
# @param node [RuboCop::AST::SendNode]
|
86
|
+
# @return [void]
|
87
|
+
def autocorrect(
|
88
|
+
corrector,
|
89
|
+
node
|
90
|
+
)
|
91
|
+
table_name, column_name = parse_table_name_and_column_name(node)
|
92
|
+
corrector.replace(
|
93
|
+
node,
|
94
|
+
format(
|
95
|
+
"add_check_constraint :%<table>s, '%<column>s IS NOT NULL', name: '%<constraint>s', validate: false",
|
96
|
+
column: column_name,
|
97
|
+
constraint: "#{table_name}_#{column_name}_is_not_null",
|
98
|
+
table: table_name
|
99
|
+
)
|
100
|
+
)
|
101
|
+
end
|
102
|
+
|
103
|
+
# @param node [RuboCop::AST::SendNode]
|
104
|
+
# @return [Boolean]
|
105
|
+
def called_after_validate_constraint?(node)
|
106
|
+
node.left_siblings.any? do |sibling|
|
107
|
+
validate_constraint?(sibling)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# @param node [RuboCop::AST::SendNode]
|
112
|
+
# @return [Boolean]
|
113
|
+
def called_before_remove_check_constraint?(node)
|
114
|
+
node.right_siblings.any? do |sibling|
|
115
|
+
remove_check_constraint?(sibling)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# @param node [RuboCop::AST::SendNode]
|
120
|
+
# @return [Boolean]
|
121
|
+
def in_second_migration?(node)
|
122
|
+
called_after_validate_constraint?(node) ||
|
123
|
+
called_before_remove_check_constraint?(node)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Sevencop
|
6
|
+
# Create tables without `force: true` option.
|
7
|
+
#
|
8
|
+
# The `force: true` option can drop an existing table.
|
9
|
+
# If you indend to drop an existing table, explicitly call `drop_table` first.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# # bad
|
13
|
+
# class CreateUsers < ActiveRecord::Migration[7.0]
|
14
|
+
# def change
|
15
|
+
# create_table :users, force: true
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# # good
|
20
|
+
# class CreateUsers < ActiveRecord::Migration[7.0]
|
21
|
+
# def change
|
22
|
+
# create_table :users
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
class RailsMigrationCreateTableForce < RuboCop::Cop::Base
|
26
|
+
extend AutoCorrector
|
27
|
+
|
28
|
+
include RangeHelp
|
29
|
+
|
30
|
+
MSG = 'Create tables without `force: true` option.'
|
31
|
+
|
32
|
+
RESTRICT_ON_SEND = %i[
|
33
|
+
create_table
|
34
|
+
].freeze
|
35
|
+
|
36
|
+
# @param node [RuboCop::AST::SendNode]
|
37
|
+
# @return [void]
|
38
|
+
def on_send(node)
|
39
|
+
option_node = option_force_true_from_create_table(node)
|
40
|
+
return unless option_node
|
41
|
+
|
42
|
+
add_offense(option_node) do |corrector|
|
43
|
+
autocorrect(corrector, option_node)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# @!method option_force_true_from_create_table(node)
|
50
|
+
# @param node [RuboCop::AST::SendNode]
|
51
|
+
# @return [RuboCop::AST::PairNode, nil]
|
52
|
+
def_node_matcher :option_force_true_from_create_table, <<~PATTERN
|
53
|
+
(send
|
54
|
+
nil?
|
55
|
+
:create_table
|
56
|
+
_
|
57
|
+
(hash
|
58
|
+
<
|
59
|
+
$(pair
|
60
|
+
(sym :force)
|
61
|
+
true
|
62
|
+
)
|
63
|
+
...
|
64
|
+
>
|
65
|
+
)
|
66
|
+
)
|
67
|
+
PATTERN
|
68
|
+
|
69
|
+
# @param corrector [RuboCop::Cop::Corrector]
|
70
|
+
# @param node [RuboCop::AST::PairNode]
|
71
|
+
# @return [void]
|
72
|
+
def autocorrect(
|
73
|
+
corrector,
|
74
|
+
node
|
75
|
+
)
|
76
|
+
corrector.remove(
|
77
|
+
range_with_surrounding_comma(
|
78
|
+
range_with_surrounding_space(
|
79
|
+
node.location.expression,
|
80
|
+
side: :left
|
81
|
+
),
|
82
|
+
:left
|
83
|
+
)
|
84
|
+
)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Sevencop
|
6
|
+
# Prefer `jsonb` to `json`.
|
7
|
+
#
|
8
|
+
# In PostgreSQL, there is no equality operator for the json column type,
|
9
|
+
# which can cause errors for existing `SELECT DISTINCT` queries in your application.
|
10
|
+
#
|
11
|
+
# @safety
|
12
|
+
# Only meaningful in PostgreSQL.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# # bad
|
16
|
+
# add_column :users, :properties, :json
|
17
|
+
#
|
18
|
+
# # good
|
19
|
+
# add_column :users, :properties, :jsonb
|
20
|
+
class RailsMigrationJsonb < RuboCop::Cop::Base
|
21
|
+
extend AutoCorrector
|
22
|
+
|
23
|
+
MSG = 'Prefer `jsonb` to `json`.'
|
24
|
+
|
25
|
+
RESTRICT_ON_SEND = %i[
|
26
|
+
add_column
|
27
|
+
change
|
28
|
+
change_column
|
29
|
+
json
|
30
|
+
].freeze
|
31
|
+
|
32
|
+
# @param node [RuboCop::AST::SendNode]
|
33
|
+
# @return [void]
|
34
|
+
def on_send(node)
|
35
|
+
json_range = json_range_from_target_send_node(node)
|
36
|
+
return unless json_range
|
37
|
+
|
38
|
+
add_offense(json_range) do |corrector|
|
39
|
+
corrector.replace(json_range, 'jsonb')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# @!method json_type_node_from_add_column(node)
|
46
|
+
# @param node [RuboCop::AST::SendNode]
|
47
|
+
# @return [RuboCop::AST::SymNode, nil]
|
48
|
+
def_node_matcher :json_type_node_from_add_column, <<~PATTERN
|
49
|
+
(send
|
50
|
+
nil?
|
51
|
+
_
|
52
|
+
_
|
53
|
+
_
|
54
|
+
$(sym :json)
|
55
|
+
)
|
56
|
+
PATTERN
|
57
|
+
alias json_type_node_from_change_column json_type_node_from_add_column
|
58
|
+
|
59
|
+
# @!method json_type_node_from_change(node)
|
60
|
+
# @param node [RuboCop::AST::SendNode]
|
61
|
+
# @return [RuboCop::AST::SymNode, nil]
|
62
|
+
def_node_matcher :json_type_node_from_change, <<~PATTERN
|
63
|
+
(send
|
64
|
+
lvar
|
65
|
+
_
|
66
|
+
_
|
67
|
+
$(sym :json)
|
68
|
+
)
|
69
|
+
PATTERN
|
70
|
+
|
71
|
+
# @!method json_type_node_from_json(node)
|
72
|
+
# @param node [RuboCop::AST::SendNode]
|
73
|
+
# @return [RuboCop::AST::SendNode, nil]
|
74
|
+
def_node_matcher :json_type_node_from_json, <<~PATTERN
|
75
|
+
$(send
|
76
|
+
lvar
|
77
|
+
_
|
78
|
+
...
|
79
|
+
)
|
80
|
+
PATTERN
|
81
|
+
|
82
|
+
# @param corrector [RuboCop::Cop::Corrector]
|
83
|
+
# @param node [RuboCop::AST::SendNode, RuboCop::AST::SymNode]
|
84
|
+
# @return [void]
|
85
|
+
def autocorrect(
|
86
|
+
corrector,
|
87
|
+
node
|
88
|
+
)
|
89
|
+
corrector.replace(node, 'jsonb')
|
90
|
+
end
|
91
|
+
|
92
|
+
# @param node [RuboCop::AST::SendNode]
|
93
|
+
# @return [RuboCop::AST::SymNode, nil]
|
94
|
+
def json_node_from_target_send_node(node)
|
95
|
+
case node.method_name
|
96
|
+
when :add_column
|
97
|
+
json_type_node_from_add_column(node)
|
98
|
+
when :change
|
99
|
+
json_type_node_from_change(node)
|
100
|
+
when :change_column
|
101
|
+
json_type_node_from_change_column(node)
|
102
|
+
when :json
|
103
|
+
json_type_node_from_json(node)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# @param node [RuboCop::AST::SendNode, RuboCop::AST::SymNode]
|
108
|
+
# @return [Parser::Source::Range]
|
109
|
+
def json_range_from_json_node(node)
|
110
|
+
case node.type
|
111
|
+
when :send
|
112
|
+
node.location.selector
|
113
|
+
when :sym
|
114
|
+
node.location.expression.with(
|
115
|
+
begin_pos: node.location.expression.begin_pos + 1
|
116
|
+
)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# @param node [RuboCop::AST::SendNode]
|
121
|
+
# @return [Parser::Source::Range]
|
122
|
+
def json_range_from_target_send_node(node)
|
123
|
+
json_node = json_node_from_target_send_node(node)
|
124
|
+
return unless json_node
|
125
|
+
|
126
|
+
json_range_from_json_node(json_node)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|