sevencop 0.21.0 → 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -0
  3. data/Gemfile.lock +13 -1
  4. data/README.md +17 -4
  5. data/config/default.yml +134 -0
  6. data/lib/rubocop/cop/sevencop/factory_bot_association_option.rb +1 -1
  7. data/lib/rubocop/cop/sevencop/factory_bot_association_style.rb +31 -31
  8. data/lib/rubocop/cop/sevencop/rails_inferred_spec_type.rb +1 -1
  9. data/lib/rubocop/cop/sevencop/rails_migration_add_check_constraint.rb +111 -0
  10. data/lib/rubocop/cop/sevencop/rails_migration_add_column_with_default_value.rb +229 -0
  11. data/lib/rubocop/cop/sevencop/rails_migration_add_foreign_key.rb +166 -0
  12. data/lib/rubocop/cop/sevencop/rails_migration_add_index_concurrently.rb +164 -0
  13. data/lib/rubocop/cop/sevencop/rails_migration_batch_in_batches.rb +95 -0
  14. data/lib/rubocop/cop/sevencop/rails_migration_batch_in_transaction.rb +83 -0
  15. data/lib/rubocop/cop/sevencop/rails_migration_batch_with_throttling.rb +108 -0
  16. data/lib/rubocop/cop/sevencop/rails_migration_change_column.rb +113 -0
  17. data/lib/rubocop/cop/sevencop/rails_migration_change_column_null.rb +128 -0
  18. data/lib/rubocop/cop/sevencop/rails_migration_create_table_force.rb +89 -0
  19. data/lib/rubocop/cop/sevencop/rails_migration_jsonb.rb +131 -0
  20. data/lib/rubocop/cop/sevencop/rails_migration_remove_column.rb +258 -0
  21. data/lib/rubocop/cop/sevencop/rails_migration_rename_column.rb +81 -0
  22. data/lib/rubocop/cop/sevencop/rails_migration_rename_table.rb +79 -0
  23. data/lib/rubocop/cop/sevencop/rails_migration_reserved_word_mysql.rb +2 -19
  24. data/lib/rubocop/cop/sevencop/rails_migration_unique_index_columns_count.rb +92 -0
  25. data/lib/sevencop/config_loader.rb +11 -10
  26. data/lib/sevencop/cop_concerns/batch_processing.rb +32 -0
  27. data/lib/sevencop/cop_concerns/column_type_method.rb +26 -0
  28. data/lib/sevencop/cop_concerns/disable_ddl_transaction.rb +49 -0
  29. data/lib/sevencop/cop_concerns.rb +3 -0
  30. data/lib/sevencop/rubocop_extension.rb +6 -1
  31. data/lib/sevencop/version.rb +1 -1
  32. data/lib/sevencop.rb +15 -0
  33. data/sevencop.gemspec +1 -0
  34. metadata +34 -2
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Sevencop
6
+ # Add the column without a default value then change the default.
7
+ #
8
+ # In earlier versions of Postgres, MySQL, and MariaDB,
9
+ # adding a column with a default value to an existing table causes the entire table to be rewritten.
10
+ # During this time, reads and writes are blocked in Postgres, and writes are blocked in MySQL and MariaDB.
11
+ #
12
+ # @safety
13
+ # Only meaningful in earlier versions of Postgres, MySQL, and MariaDB.
14
+ #
15
+ # @example
16
+ # # bad
17
+ # class AddSomeColumnToUsers < ActiveRecord::Migration[7.0]
18
+ # def change
19
+ # add_column :users, :some_column, :string, default: 'some value'
20
+ # end
21
+ # end
22
+ #
23
+ # # good
24
+ # class AddSomeColumnToUsers < ActiveRecord::Migration[7.0]
25
+ # def change
26
+ # add_column :users, :some_column, :string
27
+ # change_column_default :users, :some_column, 'some value'
28
+ # end
29
+ # end
30
+ class RailsMigrationAddColumnWithDefaultValue < RuboCop::Cop::Base
31
+ extend AutoCorrector
32
+
33
+ include RangeHelp
34
+ include ::Sevencop::CopConcerns::ColumnTypeMethod
35
+
36
+ MSG = 'Add the column without a default value then change the default.'
37
+
38
+ RESTRICT_ON_SEND = [
39
+ :add_column,
40
+ *COLUMN_TYPE_METHOD_NAMES
41
+ ].freeze
42
+
43
+ # @param node [RuboCop::AST::SendNode]
44
+ # @return [void]
45
+ def on_send(node)
46
+ return unless target_method?(node)
47
+
48
+ default_option_node = non_nil_default_option_node_from(node)
49
+ return unless default_option_node
50
+
51
+ add_offense(default_option_node) do |corrector|
52
+ autocorrect(
53
+ corrector,
54
+ default_option_node: default_option_node,
55
+ send_node: node
56
+ )
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ # @!method add_column?(node)
63
+ # @param node [RuboCop::AST::SendNode]
64
+ # @return [Boolean]
65
+ def_node_matcher :add_column?, <<~PATTERN
66
+ (send
67
+ nil?
68
+ :add_column
69
+ ...
70
+ )
71
+ PATTERN
72
+
73
+ # @!method column_type_method?(node)
74
+ # @param node [RuboCop::AST::SendNode]
75
+ # @return [Boolean]
76
+ def_node_matcher :column_type_method?, <<~PATTERN
77
+ (send
78
+ lvar
79
+ COLUMN_TYPE_METHOD_NAMES
80
+ ...
81
+ )
82
+ PATTERN
83
+
84
+ # @!method non_nil_default_option_node_from(node)
85
+ # @param node [RuboCop::AST::SendNode]
86
+ # @return [RuboCop::AST::PairNode, nil]
87
+ def_node_matcher :non_nil_default_option_node_from, <<~PATTERN
88
+ (send
89
+ _
90
+ _
91
+ ...
92
+ (hash
93
+ <
94
+ $(pair
95
+ (sym :default)
96
+ !nil
97
+ )
98
+ >
99
+ ...
100
+ )
101
+ )
102
+ PATTERN
103
+
104
+ # @param corrector [RuboCop::Cop::Corrector]
105
+ # @param default_option_node [RuboCop::AST::PairNode]
106
+ # @param send_node [RuboCop::AST::SendNode]
107
+ # @return [void]
108
+ def autocorrect(
109
+ corrector,
110
+ default_option_node:,
111
+ send_node:
112
+ )
113
+ remove_pair(
114
+ corrector,
115
+ default_option_node
116
+ )
117
+ insert_change_column_default(
118
+ corrector,
119
+ default_option_node: default_option_node,
120
+ send_node: send_node
121
+ )
122
+ end
123
+
124
+ # @param node [RuboCop::AST::SendNode]
125
+ # @return [RuboCop::AST::SymNode]
126
+ def find_column_node_from(node)
127
+ case node.method_name
128
+ when :add_column
129
+ node.arguments[1]
130
+ else
131
+ node.first_argument
132
+ end
133
+ end
134
+
135
+ # @param node [RuboCop::AST::SendNode]
136
+ # @return [RuboCop::AST::SendNode]
137
+ def find_insertion_target_node_from(node)
138
+ case node.method_name
139
+ when :add_column
140
+ node
141
+ else
142
+ node.each_ancestor(:block).first
143
+ end
144
+ end
145
+
146
+ # @param node [RuboCop::AST::SendNode]
147
+ # @return [RuboCop::AST::SymNode]
148
+ def find_table_node_from(node)
149
+ case node.method_name
150
+ when :add_column
151
+ node.first_argument
152
+ else
153
+ node.each_ancestor(:block).first.send_node.first_argument
154
+ end
155
+ end
156
+
157
+ # @param node [RuboCop::AST::SendNode]
158
+ # @return [Boolean]
159
+ def in_change_table?(node)
160
+ node.each_ancestor(:block).first&.method?(:change_table)
161
+ end
162
+
163
+ # @param corrector [RuboCop::Cop::Corrector]
164
+ # @param node [RuboCop::AST::Node]
165
+ # @param string [String]
166
+ def insert_after_with_same_indentation(
167
+ corrector,
168
+ node,
169
+ string
170
+ )
171
+ corrector.insert_after(
172
+ node,
173
+ format(
174
+ "\n%<indentation>s%<string>s",
175
+ indentation: ' ' * node.location.column,
176
+ string: string
177
+ )
178
+ )
179
+ end
180
+
181
+ # @param corrector [RuboCop::Cop::Corrector]
182
+ # @param default_option_node [RuboCop::AST::PairNode]
183
+ # @param send_node [RuboCop::AST::SendNode]
184
+ # @return [void]
185
+ def insert_change_column_default(
186
+ corrector,
187
+ default_option_node:,
188
+ send_node:
189
+ )
190
+ insert_after_with_same_indentation(
191
+ corrector,
192
+ find_insertion_target_node_from(send_node),
193
+ format(
194
+ 'change_column_default %<table>s, %<column>s, %<default>s',
195
+ column: find_column_node_from(send_node).source,
196
+ default: default_option_node.value.source,
197
+ table: find_table_node_from(send_node).source
198
+ )
199
+ )
200
+ end
201
+
202
+ # @param corrector [RuboCop::Cop::Corrector]
203
+ # @param node [RuboCop::AST::Node]
204
+ # @return [void]
205
+ def remove_pair(
206
+ corrector,
207
+ node
208
+ )
209
+ corrector.remove(
210
+ range_with_surrounding_comma(
211
+ range_with_surrounding_space(
212
+ node.location.expression,
213
+ side: :left
214
+ ),
215
+ :left
216
+ )
217
+ )
218
+ end
219
+
220
+ # @param node [RuboCop::AST::SendNode]
221
+ # @return [Boolean]
222
+ def target_method?(node)
223
+ add_column?(node) ||
224
+ (column_type_method?(node) && in_change_table?(node))
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Sevencop
6
+ # Activate foreign key validation in a separate migration in PostgreSQL.
7
+ #
8
+ # To avoid blocking writes on both tables.
9
+ #
10
+ # @safety
11
+ # Only meaningful in PostgreSQL.
12
+ #
13
+ # @example
14
+ # # bad
15
+ # class AddForeignKeyFromArticlesToUsers < ActiveRecord::Migration[7.0]
16
+ # def change
17
+ # add_foreign_key :articles, :users
18
+ # end
19
+ # end
20
+ #
21
+ # # good
22
+ # class AddForeignKeyFromArticlesToUsersWithoutValidation < ActiveRecord::Migration[7.0]
23
+ # def change
24
+ # add_foreign_key :articles, :users, validate: false
25
+ # end
26
+ # end
27
+ #
28
+ # class ActivateForeignKeyValidationFromArticlesToUsers < ActiveRecord::Migration[7.0]
29
+ # def change
30
+ # validate_foreign_key :articles, :users
31
+ # end
32
+ # end
33
+ class RailsMigrationAddForeignKey < RuboCop::Cop::Base
34
+ extend AutoCorrector
35
+
36
+ include RangeHelp
37
+
38
+ MSG = 'Activate foreign key validation in a separate migration in PostgreSQL.'
39
+
40
+ RESTRICT_ON_SEND = %i[
41
+ add_foreign_key
42
+ add_reference
43
+ ].freeze
44
+
45
+ # @param node [RuboCop::AST::SendNode]
46
+ # @return [void]
47
+ def on_send(node)
48
+ return unless bad?(node)
49
+
50
+ add_offense(node) do |corrector|
51
+ autocorrect(corrector, node)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ # @!method add_foreign_key_without_validate_option?(node)
58
+ # @param node [RuboCop::AST::SendNode]
59
+ # @return [Boolean]
60
+ def_node_matcher :add_foreign_key_without_validate_option?, <<~PATTERN
61
+ (send
62
+ nil?
63
+ :add_foreign_key
64
+ _
65
+ _
66
+ (hash
67
+ (pair
68
+ !(sym :validate)
69
+ _
70
+ )*
71
+ )?
72
+ )
73
+ PATTERN
74
+
75
+ # @!method option_validate_true_value_node_from_add_foreign_key(node)
76
+ # @param node [RuboCop::AST::SendNode]
77
+ # @return [RuboCop::AST::PairNode]
78
+ def_node_matcher :option_validate_true_value_node_from_add_foreign_key, <<~PATTERN
79
+ (send
80
+ nil?
81
+ :add_foreign_key
82
+ _
83
+ _
84
+ (hash
85
+ <
86
+ (pair
87
+ (sym :validate)
88
+ $true
89
+ )
90
+ ...
91
+ >
92
+ )
93
+ )
94
+ PATTERN
95
+
96
+ # @!method option_foreign_key_true_node_from_add_reference(node)
97
+ # @param node [RuboCop::AST::SendNode]
98
+ # @return [RuboCop::AST::PairNode]
99
+ def_node_matcher :option_foreign_key_true_node_from_add_reference, <<~PATTERN
100
+ (send
101
+ nil?
102
+ :add_reference
103
+ _
104
+ _
105
+ (hash
106
+ <
107
+ $(pair
108
+ (sym :foreign_key)
109
+ true
110
+ )
111
+ ...
112
+ >
113
+ )
114
+ )
115
+ PATTERN
116
+
117
+ # @param node [RuboCop::AST::SendNode]
118
+ # @return [Boolean]
119
+ def add_foreign_key_with_validate_option_true?(node)
120
+ option_validate_true_value_node_from_add_foreign_key(node)
121
+ end
122
+
123
+ # @param node [RuboCop::AST::SendNode]
124
+ # @return [Boolean]
125
+ def add_reference_with_validate_option_true?(node)
126
+ option_foreign_key_true_node_from_add_reference(node)
127
+ end
128
+
129
+ # @param corrector [RuboCop::Cop::Corrector]
130
+ # @param node [RuboCop::AST::SendNode]
131
+ # @return [void]
132
+ def autocorrect(
133
+ corrector,
134
+ node
135
+ )
136
+ if add_foreign_key_without_validate_option?(node)
137
+ corrector.insert_after(node.last_argument, ', validate: false')
138
+ elsif add_foreign_key_with_validate_option_true?(node)
139
+ corrector.replace(
140
+ option_validate_true_value_node_from_add_foreign_key(node),
141
+ 'false'
142
+ )
143
+ elsif add_reference_with_validate_option_true?(node)
144
+ corrector.remove(
145
+ range_with_surrounding_comma(
146
+ range_with_surrounding_space(
147
+ option_foreign_key_true_node_from_add_reference(node).location.expression,
148
+ side: :left
149
+ ),
150
+ :left
151
+ )
152
+ )
153
+ end
154
+ end
155
+
156
+ # @param node [RuboCop::AST::SendNode]
157
+ # @return [Boolean]
158
+ def bad?(node)
159
+ add_foreign_key_without_validate_option?(node) ||
160
+ add_foreign_key_with_validate_option_true?(node) ||
161
+ add_reference_with_validate_option_true?(node)
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Sevencop
6
+ # Use `algorithm: :concurrently` on adding indexes to existing tables in PostgreSQL.
7
+ #
8
+ # To avoid blocking writes.
9
+ #
10
+ # @safety
11
+ # Only meaningful in PostgreSQL.
12
+ #
13
+ # @example
14
+ # # bad
15
+ # class AddIndexToUsersName < ActiveRecord::Migration[7.0]
16
+ # def change
17
+ # add_index :users, :name
18
+ # end
19
+ # end
20
+ #
21
+ # # good
22
+ # class AddIndexToUsersNameConcurrently < ActiveRecord::Migration[7.0]
23
+ # disable_ddl_transaction!
24
+ #
25
+ # def change
26
+ # add_index :users, :name, algorithm: :concurrently
27
+ # end
28
+ # end
29
+ class RailsMigrationAddIndexConcurrently < RuboCop::Cop::Base
30
+ extend AutoCorrector
31
+
32
+ include ::Sevencop::CopConcerns::DisableDdlTransaction
33
+
34
+ MSG = 'Use `algorithm: :concurrently` on adding indexes to existing tables in PostgreSQL.'
35
+
36
+ RESTRICT_ON_SEND = %i[
37
+ add_index
38
+ index
39
+ ].freeze
40
+
41
+ # @param node [RuboCop::AST::SendNode]
42
+ # @return [void]
43
+ def on_send(node)
44
+ return unless bad?(node)
45
+
46
+ add_offense(node) do |corrector|
47
+ autocorrect(corrector, node)
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ # @!method add_index?(node)
54
+ # @param node [RuboCop::AST::SendNode]
55
+ # @return [Boolean]
56
+ def_node_matcher :add_index?, <<~PATTERN
57
+ (send
58
+ nil?
59
+ :add_index
60
+ _
61
+ _
62
+ ...
63
+ )
64
+ PATTERN
65
+
66
+ # @!method add_index_concurrently?(node)
67
+ # @param node [RuboCop::AST::SendNode]
68
+ # @return [Boolean]
69
+ def_node_matcher :add_index_concurrently?, <<~PATTERN
70
+ (send
71
+ nil?
72
+ :add_index
73
+ _
74
+ _
75
+ (hash
76
+ <
77
+ (pair
78
+ (sym :algorithm)
79
+ (sym :concurrently)
80
+ )
81
+ ...
82
+ >
83
+ )
84
+ )
85
+ PATTERN
86
+
87
+ # @!method index?(node)
88
+ # @param node [RuboCop::AST::SendNode]
89
+ # @return [Boolean]
90
+ def_node_matcher :index?, <<~PATTERN
91
+ (send
92
+ lvar
93
+ :index
94
+ _
95
+ ...
96
+ )
97
+ PATTERN
98
+
99
+ # @!method index_concurrently?(node)
100
+ # @param node [RuboCop::AST::SendNode]
101
+ # @return [Boolean]
102
+ def_node_matcher :index_concurrently?, <<~PATTERN
103
+ (send
104
+ lvar
105
+ :index
106
+ _
107
+ (hash
108
+ <
109
+ (pair
110
+ (sym :algorithm)
111
+ (sym :concurrently)
112
+ )
113
+ ...
114
+ >
115
+ )
116
+ )
117
+ PATTERN
118
+
119
+ # @param corrector [RuboCop::Cop::Corrector]
120
+ # @param node [RuboCop::AST::SendNode]
121
+ # @return [void]
122
+ def autocorrect(
123
+ corrector,
124
+ node
125
+ )
126
+ insert_disable_ddl_transaction(corrector, node) unless within_disable_ddl_transaction?(node)
127
+ insert_algorithm_option(corrector, node)
128
+ end
129
+
130
+ # @param node [RuboCop::AST::SendNode]
131
+ # @return [Boolean]
132
+ def bad?(node)
133
+ case node.method_name
134
+ when :add_index
135
+ add_index?(node) && !add_index_concurrently?(node)
136
+ when :index
137
+ index?(node) && in_change_table?(node) && !index_concurrently?(node)
138
+ end
139
+ end
140
+
141
+ # @param node [RuboCop::AST::SendNode]
142
+ # @return [Boolean]
143
+ def in_change_table?(node)
144
+ node.each_ancestor(:block).first&.method?(:change_table)
145
+ end
146
+
147
+ # @param corrector [RuboCop::Cop::Corrector]
148
+ # @param node [RuboCop::AST::SendNode]
149
+ # @return [void]
150
+ def insert_algorithm_option(
151
+ corrector,
152
+ node
153
+ )
154
+ target_node = node.last_argument
155
+ target_node = target_node.pairs.last if target_node.hash_type?
156
+ corrector.insert_after(
157
+ target_node,
158
+ ', algorithm: :concurrently'
159
+ )
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Sevencop
6
+ # Use `in_batches` in batch processing.
7
+ #
8
+ # For more efficient batch processing.
9
+ #
10
+ # @safety
11
+ # There are some cases where we should not do that,
12
+ # or this type of consideration might be already done in a way that we cannot detect.
13
+ #
14
+ # @example
15
+ # # bad
16
+ # class BackfillSomeColumn < ActiveRecord::Migration[7.0]
17
+ # disable_ddl_transaction!
18
+ #
19
+ # def change
20
+ # User.update_all(some_column: 'some value')
21
+ # end
22
+ # end
23
+ #
24
+ # # good
25
+ # class BackfillSomeColumnToUsers < ActiveRecord::Migration[7.0]
26
+ # disable_ddl_transaction!
27
+ #
28
+ # def up
29
+ # User.within_in_batches do |relation|
30
+ # relation.update_all(some_column: 'some value')
31
+ # end
32
+ # end
33
+ # end
34
+ class RailsMigrationBatchInBatches < RuboCop::Cop::Base
35
+ extend AutoCorrector
36
+
37
+ include ::Sevencop::CopConcerns::BatchProcessing
38
+
39
+ MSG = 'Use `in_batches` in batch processing.'
40
+
41
+ RESTRICT_ON_SEND = %i[
42
+ delete_all
43
+ update_all
44
+ ].freeze
45
+
46
+ # @param node [RuboCop::AST::SendNode]
47
+ # @return [void]
48
+ def on_send(node)
49
+ return unless wrong?(node)
50
+
51
+ add_offense(node) do |corrector|
52
+ autocorrect(corrector, node)
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ # @param corrector [RuboCop::Cop::Corrector]
59
+ # @param node [RuboCop::AST::SendNode]
60
+ # @return [void]
61
+ def autocorrect(
62
+ corrector,
63
+ node
64
+ )
65
+ range = node.location.selector.with(
66
+ end_pos: node.location.expression.end_pos
67
+ )
68
+ corrector.replace(
69
+ range,
70
+ <<~TEXT.chomp
71
+ in_batches do |relation|
72
+ relation.#{range.source}
73
+ end
74
+ TEXT
75
+ )
76
+ end
77
+
78
+ # @param node [RuboCop::AST::Node]
79
+ # @return [Boolean]
80
+ def within_in_batches?(node)
81
+ node.each_ancestor(:block).any? do |ancestor|
82
+ ancestor.method?(:in_batches)
83
+ end
84
+ end
85
+
86
+ # @param node [RuboCop::AST::SendNode]
87
+ # @return [Boolean]
88
+ def wrong?(node)
89
+ batch_processing?(node) &&
90
+ !within_in_batches?(node)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end