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,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/inflector'
4
+ require 'pathname'
5
+
6
+ module RuboCop
7
+ module Cop
8
+ module Sevencop
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 RailsMigrationRemoveColumn < 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
+ # @return [#parse]
172
+ def parser
173
+ parser_class.new(::RuboCop::AST::Builder.new)
174
+ end
175
+
176
+ # @return [Class]
177
+ def parser_class
178
+ ::Parser.const_get(
179
+ "Ruby#{target_ruby_version.to_s.delete('.')}"
180
+ )
181
+ end
182
+
183
+ # @param plural [String]
184
+ # @return [String]
185
+ def singularize(plural)
186
+ ::ActiveSupport::Inflector.singularize(plural)
187
+ end
188
+
189
+ class Parser
190
+ class << self
191
+ # @param content [String]
192
+ # @param path [String]
193
+ # @param target_ruby_version [#to_s]
194
+ # @return [RuboCop::AST::Node]
195
+ def call(
196
+ content:,
197
+ path:,
198
+ target_ruby_version:
199
+ )
200
+ new(
201
+ content: content,
202
+ path: path,
203
+ target_ruby_version: target_ruby_version
204
+ ).call
205
+ end
206
+ end
207
+
208
+ # @param content [String]
209
+ # @param path [String]
210
+ # @param target_ruby_version [#to_s]
211
+ def initialize(
212
+ content:,
213
+ path:,
214
+ target_ruby_version:
215
+ )
216
+ @content = content
217
+ @path = path
218
+ @target_ruby_version = target_ruby_version
219
+ end
220
+
221
+ # @return [RuboCop::AST::Node]
222
+ def call
223
+ parser.parse(buffer)
224
+ end
225
+
226
+ private
227
+
228
+ # @return [Parser::Source::Buffer]
229
+ def buffer
230
+ ::Parser::Source::Buffer.new(
231
+ @path,
232
+ source: @content
233
+ )
234
+ end
235
+
236
+ # @return [RuboCop::AST::Builder]
237
+ def builder
238
+ ::RuboCop::AST::Builder.new
239
+ end
240
+
241
+ def parser
242
+ parser_class.new(builder)
243
+ end
244
+
245
+ # @return [Class]
246
+ def parser_class
247
+ ::Parser.const_get(parser_class_name)
248
+ end
249
+
250
+ # @return [String]
251
+ def parser_class_name
252
+ "Ruby#{@target_ruby_version.to_s.delete('.')}"
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Sevencop
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 RailsMigrationRenameColumn < 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 Sevencop
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 RailsMigrationRenameTable < 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
@@ -15,24 +15,7 @@ module RuboCop
15
15
  # # good
16
16
  # add_column :users, :some_other_good_name, :string
17
17
  class RailsMigrationReservedWordMysql < RuboCop::Cop::Base
18
- COLUMN_TYPE_METHOD_NAMES = ::Set.new(
19
- %i[
20
- bigint
21
- binary
22
- blob
23
- boolean
24
- date
25
- datetime
26
- decimal
27
- float
28
- integer
29
- numeric
30
- primary_key
31
- string
32
- text
33
- time
34
- ]
35
- ).freeze
18
+ include ::Sevencop::CopConcerns::ColumnTypeMethod
36
19
 
37
20
  MSG = 'Avoid using MySQL reserved words as identifiers.'
38
21
 
@@ -129,7 +112,7 @@ module RuboCop
129
112
  # @return [RuboCop::AST::Node, nil]
130
113
  def_node_matcher :index_name_option_from_column_type, <<~PATTERN
131
114
  (send
132
- (lvar _)
115
+ lvar
133
116
  COLUMN_TYPE_METHOD_NAMES
134
117
  _
135
118
  (hash
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Sevencop
6
+ # Keep unique index columns count less than a specified number.
7
+ #
8
+ # Adding a non-unique index with more than three columns rarely improves performance.
9
+ # Instead, start an index with columns that narrow down the results the most.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # add_index :users, %i[a b c d]
14
+ #
15
+ # # good (`MaxColumnsCount: 3` by default)
16
+ # add_index :users, %i[a b c]
17
+ class RailsMigrationUniqueIndexColumnsCount < RuboCop::Cop::Base
18
+ RESTRICT_ON_SEND = %i[
19
+ add_index
20
+ index
21
+ ].freeze
22
+
23
+ # @param node [RuboCop::AST::SendNode]
24
+ # @return [void]
25
+ def on_send(node)
26
+ column_names_node = column_names_node_from(node)
27
+ return unless column_names_node
28
+
29
+ column_names_count = columns_count_from(column_names_node)
30
+ return if column_names_count <= max_columns_count
31
+
32
+ add_offense(
33
+ column_names_node,
34
+ message: "Keep unique index columns count less than #{max_columns_count}."
35
+ )
36
+ end
37
+
38
+ private
39
+
40
+ # @!method column_names_node_from_add_index(node)
41
+ # @param node [RuboCop::AST::SendNode]
42
+ # @return [RuboCop::AST::Node, nil]
43
+ def_node_matcher :column_names_node_from_add_index, <<~PATTERN
44
+ (send
45
+ nil?
46
+ _
47
+ _
48
+ $({array | sym} ...)
49
+ )
50
+ PATTERN
51
+
52
+ # @!method column_names_node_from_index(node)
53
+ # @param node [RuboCop::AST::SendNode]
54
+ # @return [RuboCop::AST::Node, nil]
55
+ def_node_matcher :column_names_node_from_index, <<~PATTERN
56
+ (send
57
+ (lvar ...)
58
+ _
59
+ $({array | sym} ...)
60
+ )
61
+ PATTERN
62
+
63
+ # @param node [RuboCop::AST::SendNode]
64
+ # @return [RuboCop::AST::Node, nil]
65
+ def column_names_node_from(node)
66
+ case node.method_name
67
+ when :add_index
68
+ column_names_node_from_add_index(node)
69
+ when :index
70
+ column_names_node_from_index(node)
71
+ end
72
+ end
73
+
74
+ # @param node [RuboCop::AST::Node]
75
+ # @return [Integer, nil]
76
+ def columns_count_from(node)
77
+ case node.type
78
+ when :array
79
+ node.values.count
80
+ when :sym
81
+ 1
82
+ end
83
+ end
84
+
85
+ # @return [Integer]
86
+ def max_columns_count
87
+ cop_config['MaxColumnsCount']
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -5,23 +5,24 @@ require 'rubocop'
5
5
  module Sevencop
6
6
  # Merge default RuboCop config with plugin config.
7
7
  class ConfigLoader
8
- PLUGIN_CONFIG_PATH = ::File.expand_path(
9
- '../../config/default.yml',
10
- __dir__
11
- )
12
-
13
8
  class << self
9
+ # @param path [String]
14
10
  # @return [RuboCop::Config]
15
- def call
16
- new.call
11
+ def call(path:)
12
+ new(path: path).call
17
13
  end
18
14
  end
19
15
 
16
+ # @param path [String]
17
+ def initialize(path:)
18
+ @path = path
19
+ end
20
+
20
21
  # @return [RuboCop::Config]
21
22
  def call
22
23
  ::RuboCop::ConfigLoader.merge_with_default(
23
24
  plugin_config,
24
- PLUGIN_CONFIG_PATH
25
+ @path
25
26
  )
26
27
  end
27
28
 
@@ -31,7 +32,7 @@ module Sevencop
31
32
  def plugin_config
32
33
  config = ::RuboCop::Config.new(
33
34
  plugin_config_hash,
34
- PLUGIN_CONFIG_PATH
35
+ @path
35
36
  )
36
37
  config.make_excludes_absolute
37
38
  config
@@ -41,7 +42,7 @@ module Sevencop
41
42
  def plugin_config_hash
42
43
  ::RuboCop::ConfigLoader.send(
43
44
  :load_yaml_configuration,
44
- PLUGIN_CONFIG_PATH
45
+ @path
45
46
  )
46
47
  end
47
48
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevencop
4
+ module CopConcerns
5
+ module BatchProcessing
6
+ BATCH_PROCESSING_METHOD_NAMES = ::Set.new(
7
+ %i[
8
+ delete_all
9
+ update_all
10
+ ]
11
+ ).freeze
12
+
13
+ class << self
14
+ def included(klass)
15
+ super
16
+ klass.class_eval do
17
+ # @!method batch_processing?(node)
18
+ # @param node [RuboCop::AST::SendNode]
19
+ # @return [Boolean]
20
+ def_node_matcher :batch_processing?, <<~PATTERN
21
+ (send
22
+ !nil?
23
+ BATCH_PROCESSING_METHOD_NAMES
24
+ ...
25
+ )
26
+ PATTERN
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevencop
4
+ module CopConcerns
5
+ module ColumnTypeMethod
6
+ COLUMN_TYPE_METHOD_NAMES = ::Set.new(
7
+ %i[
8
+ bigint
9
+ binary
10
+ blob
11
+ boolean
12
+ date
13
+ datetime
14
+ decimal
15
+ float
16
+ integer
17
+ numeric
18
+ primary_key
19
+ string
20
+ text
21
+ time
22
+ ]
23
+ ).freeze
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevencop
4
+ module CopConcerns
5
+ module DisableDdlTransaction
6
+ class << self
7
+ def included(klass)
8
+ super
9
+ klass.class_eval do
10
+ # @!method disable_ddl_transaction?(node)
11
+ # @param node [RuboCop::AST::SendNode]
12
+ # @return [Boolean]
13
+ def_node_matcher :disable_ddl_transaction?, <<~PATTERN
14
+ (send
15
+ nil?
16
+ :disable_ddl_transaction!
17
+ ...
18
+ )
19
+ PATTERN
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ # @param corrector [RuboCop::Cop::Corrector]
27
+ # @param node [RuboCop::AST::SendNode]
28
+ # @return [void]
29
+ def insert_disable_ddl_transaction(
30
+ corrector,
31
+ node
32
+ )
33
+ corrector.insert_before(
34
+ node.each_ancestor(:def).first,
35
+ "disable_ddl_transaction!\n\n "
36
+ )
37
+ end
38
+
39
+ # @param node [RuboCop::AST::SendNode]
40
+ # @return [Boolean]
41
+ def within_disable_ddl_transaction?(node)
42
+ node.each_ancestor(:def).first&.left_siblings&.any? do |sibling|
43
+ sibling.is_a?(::RuboCop::AST::SendNode) &&
44
+ disable_ddl_transaction?(sibling)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end