rubocop-migration 0.2.0 → 0.4.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 (45) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +58 -0
  3. data/CODE_OF_CONDUCT.md +84 -0
  4. data/Gemfile +11 -1
  5. data/Gemfile.lock +89 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +42 -18
  8. data/Rakefile +9 -3
  9. data/config/default.yml +151 -0
  10. data/data/reserved_words_mysql.txt +750 -0
  11. data/lib/rubocop/cop/migration/add_check_constraint.rb +111 -0
  12. data/lib/rubocop/cop/migration/add_column_with_default_value.rb +229 -0
  13. data/lib/rubocop/cop/migration/add_foreign_key.rb +166 -0
  14. data/lib/rubocop/cop/migration/add_index_columns_count.rb +114 -0
  15. data/lib/rubocop/cop/migration/add_index_concurrently.rb +164 -0
  16. data/lib/rubocop/cop/migration/add_index_duplicate.rb +183 -0
  17. data/lib/rubocop/cop/migration/batch_in_batches.rb +95 -0
  18. data/lib/rubocop/cop/migration/batch_in_transaction.rb +83 -0
  19. data/lib/rubocop/cop/migration/batch_with_throttling.rb +108 -0
  20. data/lib/rubocop/cop/migration/change_column.rb +113 -0
  21. data/lib/rubocop/cop/migration/change_column_null.rb +208 -0
  22. data/lib/rubocop/cop/migration/create_table_force.rb +89 -0
  23. data/lib/rubocop/cop/migration/jsonb.rb +131 -0
  24. data/lib/rubocop/cop/migration/remove_column.rb +246 -0
  25. data/lib/rubocop/cop/migration/rename_column.rb +81 -0
  26. data/lib/rubocop/cop/migration/rename_table.rb +79 -0
  27. data/lib/rubocop/cop/migration/reserved_word_mysql.rb +232 -0
  28. data/lib/rubocop/migration/config_loader.rb +51 -0
  29. data/lib/rubocop/migration/cop_concerns/batch_processing.rb +34 -0
  30. data/lib/rubocop/migration/cop_concerns/column_type_method.rb +28 -0
  31. data/lib/rubocop/migration/cop_concerns/disable_ddl_transaction.rb +51 -0
  32. data/lib/rubocop/migration/cop_concerns.rb +11 -0
  33. data/lib/rubocop/migration/rubocop_extension.rb +15 -0
  34. data/lib/rubocop/migration/version.rb +4 -2
  35. data/lib/rubocop/migration.rb +23 -0
  36. data/rubocop-migration.gemspec +25 -31
  37. metadata +49 -137
  38. data/.gitignore +0 -15
  39. data/bin/console +0 -14
  40. data/bin/setup +0 -8
  41. data/circle.yml +0 -6
  42. data/lib/rubocop/cop/migration/unsafe_migration.rb +0 -69
  43. data/lib/rubocop/migration/strong_migrations_checker.rb +0 -47
  44. data/lib/rubocop-migration.rb +0 -16
  45. data/vendor/.gitkeep +0 -0
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/inflector'
4
+ require 'pathname'
5
+
6
+ module RuboCop
7
+ module Cop
8
+ module Migration
9
+ # Make sure the column is already ignored by the running app before removing it.
10
+ #
11
+ # Active Record caches database columns at runtime, so if you drop a column, it can cause exceptions until your app reboots.
12
+ #
13
+ # @safety
14
+ # The logic to check if it is included in `ignored_columns` may fail.
15
+ #
16
+ # @example
17
+ # # bad
18
+ # class User < ApplicationRecord
19
+ # end
20
+ #
21
+ # class RemoveUsersSomeColumn < ActiveRecord::Migration[7.0]
22
+ # def change
23
+ # remove_column :users, :some_column
24
+ # end
25
+ # end
26
+ #
27
+ # # good
28
+ # class User < ApplicationRecord
29
+ # self.ignored_columns += %w[some_column]
30
+ # end
31
+ #
32
+ # class RemoveUsersSomeColumn < ActiveRecord::Migration[7.0]
33
+ # def change
34
+ # remove_column :users, :some_column
35
+ # end
36
+ # end
37
+ class RemoveColumn < RuboCop::Cop::Base
38
+ MSG = 'Make sure the column is already ignored by the running app before removing it.'
39
+
40
+ RESTRICT_ON_SEND = %i[
41
+ remove_column
42
+ ].freeze
43
+
44
+ # @param node [RuboCop::AST::SendNode]
45
+ # @return [void]
46
+ def on_send(node)
47
+ return unless bad?(node)
48
+
49
+ add_offense(node)
50
+ end
51
+
52
+ private
53
+
54
+ # @!method ignored_columns?(node)
55
+ # @param node [RuboCop::AST::Node]
56
+ # @return [Boolean]
57
+ def_node_matcher :ignored_columns?, <<~PATTERN
58
+ (send
59
+ self
60
+ :ignored_columns
61
+ ...
62
+ )
63
+ PATTERN
64
+
65
+ # @!method remove_column?(node)
66
+ # @param node [RuboCop::AST::SendNode]
67
+ # @return [Boolean]
68
+ def_node_matcher :remove_column?, <<~PATTERN
69
+ (send
70
+ nil?
71
+ :remove_column
72
+ ...
73
+ )
74
+ PATTERN
75
+
76
+ # @!method ignored_column_nodes_from(node)
77
+ # @param node [RuboCop::AST::Node]
78
+ # @return [Array<String, Symbol>, nil]
79
+ def_node_matcher :ignored_column_nodes_from, <<~PATTERN
80
+ `{
81
+ (op_asgn
82
+ (send
83
+ self
84
+ :ignored_columns
85
+ ...
86
+ )
87
+ :+
88
+ (array
89
+ ({str sym} $_)*
90
+ )
91
+ )
92
+
93
+ (send
94
+ self
95
+ :ignored_columns=
96
+ (array
97
+ ({str sym} $_)*
98
+ )
99
+ )
100
+ }
101
+ PATTERN
102
+
103
+ # @param node [RuboCop::AST::SendNode]
104
+ # @return [Boolean]
105
+ def bad?(node)
106
+ remove_column?(node) &&
107
+ !ignored?(node)
108
+ end
109
+
110
+ # @param node [RuboCop::AST::SendNode]
111
+ # @return [String, nil]
112
+ def find_column_name_from(node)
113
+ node.arguments[1].value&.to_s
114
+ end
115
+
116
+ # @param table_name [String]
117
+ # @return [Array<String>]
118
+ def find_ignored_column_names_from(table_name)
119
+ pathname = model_pathname_from(
120
+ singularize(table_name)
121
+ )
122
+ return [] unless pathname.exist?
123
+
124
+ ignored_column_nodes = ignored_column_nodes_from(
125
+ parse(
126
+ content: pathname.read,
127
+ path: pathname.to_s
128
+ )
129
+ )
130
+ return [] unless ignored_column_nodes
131
+
132
+ ignored_column_nodes.map(&:to_s)
133
+ end
134
+
135
+ # @param node [RuboCop::AST::SendNode]
136
+ # @return [String, nil]
137
+ def find_table_name_from(node)
138
+ node.arguments[0].value&.to_s
139
+ end
140
+
141
+ # @param node [RuboCop::AST::SendNode]
142
+ # @return [Boolean]
143
+ def ignored?(node)
144
+ find_ignored_column_names_from(
145
+ find_table_name_from(node)
146
+ ).include?(
147
+ find_column_name_from(node)
148
+ )
149
+ end
150
+
151
+ # @param snake_cased_model_name [String]
152
+ # @return [Pathname]
153
+ def model_pathname_from(snake_cased_model_name)
154
+ ::Pathname.new("app/models/#{snake_cased_model_name}.rb")
155
+ end
156
+
157
+ # @param content [String]
158
+ # @param path [String]
159
+ # @return [RuboCop::AST::Node]
160
+ def parse(
161
+ content:,
162
+ path:
163
+ )
164
+ Parser.call(
165
+ content: content,
166
+ path: path,
167
+ target_ruby_version: target_ruby_version
168
+ )
169
+ end
170
+
171
+ # @param plural [String]
172
+ # @return [String]
173
+ def singularize(plural)
174
+ ::ActiveSupport::Inflector.singularize(plural)
175
+ end
176
+
177
+ class Parser
178
+ class << self
179
+ # @param content [String]
180
+ # @param path [String]
181
+ # @param target_ruby_version [#to_s]
182
+ # @return [RuboCop::AST::Node]
183
+ def call(
184
+ content:,
185
+ path:,
186
+ target_ruby_version:
187
+ )
188
+ new(
189
+ content: content,
190
+ path: path,
191
+ target_ruby_version: target_ruby_version
192
+ ).call
193
+ end
194
+ end
195
+
196
+ # @param content [String]
197
+ # @param path [String]
198
+ # @param target_ruby_version [#to_s]
199
+ def initialize(
200
+ content:,
201
+ path:,
202
+ target_ruby_version:
203
+ )
204
+ @content = content
205
+ @path = path
206
+ @target_ruby_version = target_ruby_version
207
+ end
208
+
209
+ # @return [RuboCop::AST::Node]
210
+ def call
211
+ parser.parse(buffer)
212
+ end
213
+
214
+ private
215
+
216
+ # @return [Parser::Source::Buffer]
217
+ def buffer
218
+ ::Parser::Source::Buffer.new(
219
+ @path,
220
+ source: @content
221
+ )
222
+ end
223
+
224
+ # @return [RuboCop::AST::Builder]
225
+ def builder
226
+ ::RuboCop::AST::Builder.new
227
+ end
228
+
229
+ def parser
230
+ parser_class.new(builder)
231
+ end
232
+
233
+ # @return [Class]
234
+ def parser_class
235
+ ::Parser.const_get(parser_class_name)
236
+ end
237
+
238
+ # @return [String]
239
+ def parser_class_name
240
+ "Ruby#{@target_ruby_version.to_s.delete('.')}"
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Migration
6
+ # Avoid renaming columns that are in use.
7
+ #
8
+ # It will cause errors in your application.
9
+ # A safer approach is to:
10
+ #
11
+ # 1. Create a new column
12
+ # 2. Write to both columns
13
+ # 3. Backfill data from the old column to the new column
14
+ # 4. Move reads from the old column to the new column
15
+ # 5. Stop writing to the old column
16
+ # 6. Drop the old column
17
+ #
18
+ # @safety
19
+ # Only meaningful if the column is in use.
20
+ #
21
+ # @example
22
+ # # bad
23
+ # class RenameUsersSettingsToProperties < ActiveRecord::Migration[7.0]
24
+ # def change
25
+ # rename_column :users, :settings, :properties
26
+ # end
27
+ # end
28
+ #
29
+ # # good
30
+ # class AddUsersProperties < ActiveRecord::Migration[7.0]
31
+ # def change
32
+ # add_column :users, :properties, :jsonb
33
+ # end
34
+ # end
35
+ #
36
+ # class User < ApplicationRecord
37
+ # self.ignored_columns += %w[settings]
38
+ # end
39
+ #
40
+ # class RemoveUsersSettings < ActiveRecord::Migration[7.0]
41
+ # def change
42
+ # remove_column :users, :settings
43
+ # end
44
+ # end
45
+ class RenameColumn < RuboCop::Cop::Base
46
+ MSG = 'Avoid renaming columns that are in use.'
47
+
48
+ RESTRICT_ON_SEND = %i[
49
+ rename_column
50
+ ].freeze
51
+
52
+ # @param node [RuboCop::AST::SendNode]
53
+ # @return [void]
54
+ def on_send(node)
55
+ return unless bad?(node)
56
+
57
+ add_offense(node)
58
+ end
59
+
60
+ private
61
+
62
+ # @!method rename_column?(node)
63
+ # @param node [RuboCop::AST::SendNode]
64
+ # @return [Boolean]
65
+ def_node_matcher :rename_column?, <<~PATTERN
66
+ (send
67
+ nil?
68
+ :rename_column
69
+ ...
70
+ )
71
+ PATTERN
72
+
73
+ # @param node [RuboCop::AST::SendNode]
74
+ # @return [Boolean]
75
+ def bad?(node)
76
+ rename_column?(node)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Migration
6
+ # Avoid renaming tables that are in use.
7
+ #
8
+ # It will cause errors in your application.
9
+ # A safer approach is to:
10
+ #
11
+ # 1. Create a new table
12
+ # 2. Write to both tables
13
+ # 3. Backfill data from the old table to new table
14
+ # 4. Move reads from the old table to the new table
15
+ # 5. Stop writing to the old table
16
+ # 6. Drop the old table
17
+ #
18
+ # @safety
19
+ # Only meaningful if the table is in use.
20
+ #
21
+ # @example
22
+ # # bad
23
+ # class RenameUsersToAccouts < ActiveRecord::Migration[7.0]
24
+ # def change
25
+ # rename_table :users, :accounts
26
+ # end
27
+ # end
28
+ #
29
+ # # good
30
+ # class AddAccounts < ActiveRecord::Migration[7.0]
31
+ # def change
32
+ # create_table :accounts do |t|
33
+ # t.string :name, null: false
34
+ # end
35
+ # end
36
+ # end
37
+ #
38
+ # class RemoveUsers < ActiveRecord::Migration[7.0]
39
+ # def change
40
+ # remove_table :users
41
+ # end
42
+ # end
43
+ class RenameTable < RuboCop::Cop::Base
44
+ MSG = 'Avoid renaming tables that are in use.'
45
+
46
+ RESTRICT_ON_SEND = %i[
47
+ rename_table
48
+ ].freeze
49
+
50
+ # @param node [RuboCop::AST::SendNode]
51
+ # @return [void]
52
+ def on_send(node)
53
+ return unless bad?(node)
54
+
55
+ add_offense(node)
56
+ end
57
+
58
+ private
59
+
60
+ # @!method rename_table?(node)
61
+ # @param node [RuboCop::AST::SendNode]
62
+ # @return [Boolean]
63
+ def_node_matcher :rename_table?, <<~PATTERN
64
+ (send
65
+ nil?
66
+ :rename_table
67
+ ...
68
+ )
69
+ PATTERN
70
+
71
+ # @param node [RuboCop::AST::SendNode]
72
+ # @return [Boolean]
73
+ def bad?(node)
74
+ rename_table?(node)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Migration
8
+ # Avoid using MySQL reserved words as identifiers.
9
+ #
10
+ # @example
11
+ # # bad
12
+ # # NOTE: `role` is a reserved word in MySQL.
13
+ # add_column :users, :role, :string
14
+ #
15
+ # # good
16
+ # add_column :users, :some_other_good_name, :string
17
+ class ReservedWordMysql < RuboCop::Cop::Base
18
+ include ::RuboCop::Migration::CopConcerns::ColumnTypeMethod
19
+
20
+ MSG = 'Avoid using MySQL reserved words as identifiers.'
21
+
22
+ # Obtained from https://dev.mysql.com/doc/refman/8.0/en/keywords.html.
23
+ PATH_TO_RESERVED_WORDS_FILE = File.expand_path(
24
+ '../../../../data/reserved_words_mysql.txt',
25
+ __dir__
26
+ ).freeze
27
+
28
+ RESTRICT_ON_SEND = [
29
+ :add_column,
30
+ :add_index,
31
+ :add_reference,
32
+ :create_join_table,
33
+ :create_table,
34
+ :rename,
35
+ :rename_column,
36
+ :rename_index,
37
+ :rename_table,
38
+ *COLUMN_TYPE_METHOD_NAMES
39
+ ].freeze
40
+
41
+ class << self
42
+ # @return [Array<String>]
43
+ def reserved_words
44
+ @reserved_words ||= ::Set.new(
45
+ ::File.read(PATH_TO_RESERVED_WORDS_FILE).split("\n")
46
+ ).freeze
47
+ end
48
+ end
49
+
50
+ # @param node [RuboCop::AST::DefNode]
51
+ # @return [void]
52
+ def on_send(node)
53
+ offended_identifier_nodes_from(node).each do |identifier_node|
54
+ add_offense(identifier_node)
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ # @!method index_name_option_from_add_index(node)
61
+ # @param node [RuboCop::AST::SendNode]
62
+ # @return [RuboCop::AST::Node, nil]
63
+ def_node_matcher :index_name_option_from_add_index, <<~PATTERN
64
+ (send
65
+ nil?
66
+ :add_index
67
+ _
68
+ _
69
+ (hash
70
+ <
71
+ (pair
72
+ (sym :name)
73
+ $_
74
+ )
75
+ >
76
+ ...
77
+ )
78
+ )
79
+ PATTERN
80
+
81
+ # @!method index_name_option_from_add_reference(node)
82
+ # @param node [RuboCop::AST::SendNode]
83
+ # @return [RuboCop::AST::Node, nil]
84
+ def_node_matcher :index_name_option_from_add_reference, <<~PATTERN
85
+ (send
86
+ nil?
87
+ :add_reference
88
+ _
89
+ _
90
+ (hash
91
+ <
92
+ (pair
93
+ (sym :index)
94
+ (hash
95
+ <
96
+ (pair
97
+ (sym :name)
98
+ $_
99
+ )
100
+ >
101
+ ...
102
+ )
103
+ )
104
+ >
105
+ ...
106
+ )
107
+ )
108
+ PATTERN
109
+
110
+ # @!method index_name_option_from_column_type(node)
111
+ # @param node [RuboCop::AST::SendNode]
112
+ # @return [RuboCop::AST::Node, nil]
113
+ def_node_matcher :index_name_option_from_column_type, <<~PATTERN
114
+ (send
115
+ lvar
116
+ COLUMN_TYPE_METHOD_NAMES
117
+ _
118
+ (hash
119
+ <
120
+ (pair
121
+ (sym :index)
122
+ (hash
123
+ <
124
+ (pair
125
+ (sym :name)
126
+ $_
127
+ )
128
+ ...
129
+ >
130
+ )
131
+ )
132
+ ...
133
+ >
134
+ )
135
+ )
136
+ PATTERN
137
+
138
+ # @!method table_name_option_from(node)
139
+ # @param node [RuboCop::AST::SendNode]
140
+ # @return [RuboCop::AST::Node, nil]
141
+ def_node_matcher :table_name_option_from, <<~PATTERN
142
+ (send
143
+ nil?
144
+ :create_join_table
145
+ _
146
+ _
147
+ (hash
148
+ <
149
+ (pair
150
+ (sym :table_name)
151
+ $_
152
+ )
153
+ ...
154
+ >
155
+ )
156
+ )
157
+ PATTERN
158
+
159
+ # @param node [RuboCop::AST::SendNode]
160
+ # @return [Array<RuboCop::AST::Node>]
161
+ def identifier_column_name_nodes_from(node)
162
+ case node.method_name
163
+ when :add_column, :rename
164
+ [node.arguments[1]]
165
+ when :rename_column
166
+ [node.arguments[2]]
167
+ when *COLUMN_TYPE_METHOD_NAMES
168
+ [node.arguments[0]]
169
+ else
170
+ []
171
+ end
172
+ end
173
+
174
+ # @param node [RuboCop::AST::SendNode]
175
+ # @return [Array<RuboCop::AST::Node>]
176
+ def identifier_index_name_nodes_from(node)
177
+ case node.method_name
178
+ when :add_index
179
+ [index_name_option_from_add_index(node)].compact
180
+ when :add_reference
181
+ [index_name_option_from_add_reference(node)].compact
182
+ when :rename_index
183
+ [node.arguments[2]]
184
+ when *COLUMN_TYPE_METHOD_NAMES
185
+ [index_name_option_from_column_type(node)].compact
186
+ else
187
+ []
188
+ end
189
+ end
190
+
191
+ # @param node [RuboCop::AST::SendNode]
192
+ # @return [Array<RuboCop::AST::Node>]
193
+ def identifier_nodes_from(node)
194
+ identifier_table_name_nodes_from(node) +
195
+ identifier_column_name_nodes_from(node) +
196
+ identifier_index_name_nodes_from(node)
197
+ end
198
+
199
+ # @param node [RuboCop::AST::SendNode]
200
+ # @return [Array<RuboCop::AST::Node>]
201
+ def identifier_table_name_nodes_from(node)
202
+ case node.method_name
203
+ when :create_join_table
204
+ [table_name_option_from(node)].compact
205
+ when :create_table
206
+ [node.arguments[0]]
207
+ when :rename_table
208
+ [node.arguments[1]]
209
+ else
210
+ []
211
+ end
212
+ end
213
+
214
+ # @param node [RuboCop::AST::Node]
215
+ # @return [Array<RuboCop::AST::Node>]
216
+ def offended_identifier_nodes_from(node)
217
+ identifier_nodes_from(node).select do |identifier_node|
218
+ reserved_word_identifier_node?(identifier_node)
219
+ end
220
+ end
221
+
222
+ # @param node [RuboCop::AST::Node]
223
+ # @return [Boolean]
224
+ def reserved_word_identifier_node?(node)
225
+ return false unless node.respond_to?(:value)
226
+
227
+ self.class.reserved_words.include?(node.value.to_s)
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module RuboCop
6
+ module Migration
7
+ # Merge default RuboCop config with plugin config.
8
+ class ConfigLoader
9
+ class << self
10
+ # @param path [String]
11
+ # @return [RuboCop::Config]
12
+ def call(path:)
13
+ new(path: path).call
14
+ end
15
+ end
16
+
17
+ # @param path [String]
18
+ def initialize(path:)
19
+ @path = path
20
+ end
21
+
22
+ # @return [RuboCop::Config]
23
+ def call
24
+ ::RuboCop::ConfigLoader.merge_with_default(
25
+ plugin_config,
26
+ @path
27
+ )
28
+ end
29
+
30
+ private
31
+
32
+ # @return [RuboCop::Config]
33
+ def plugin_config
34
+ config = ::RuboCop::Config.new(
35
+ plugin_config_hash,
36
+ @path
37
+ )
38
+ config.make_excludes_absolute
39
+ config
40
+ end
41
+
42
+ # @return [Hash]
43
+ def plugin_config_hash
44
+ ::RuboCop::ConfigLoader.send(
45
+ :load_yaml_configuration,
46
+ @path
47
+ )
48
+ end
49
+ end
50
+ end
51
+ end