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