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.
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