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,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Sevencop
6
+ # Disable transaction in batch processing.
7
+ #
8
+ # To avoid locking the table.
9
+ #
10
+ # @safety
11
+ # There are some cases where transaction is really needed.
12
+ #
13
+ # @example
14
+ # # bad
15
+ # class AddSomeColumnToUsersThenBackfillSomeColumn < ActiveRecord::Migration[7.0]
16
+ # def change
17
+ # add_column :users, :some_column, :text
18
+ # User.update_all(some_column: 'some value')
19
+ # end
20
+ # end
21
+ #
22
+ # # good
23
+ # class AddSomeColumnToUsers < ActiveRecord::Migration[7.0]
24
+ # def change
25
+ # add_column :users, :some_column, :text
26
+ # end
27
+ # end
28
+ #
29
+ # class BackfillSomeColumnToUsers < ActiveRecord::Migration[7.0]
30
+ # disable_ddl_transaction!
31
+ #
32
+ # def up
33
+ # User.unscoped.in_batches do |relation|
34
+ # relation.update_all(some_column: 'some value')
35
+ # sleep(0.01)
36
+ # end
37
+ # end
38
+ # end
39
+ class RailsMigrationBatchInTransaction < RuboCop::Cop::Base
40
+ extend AutoCorrector
41
+
42
+ include ::Sevencop::CopConcerns::BatchProcessing
43
+ include ::Sevencop::CopConcerns::DisableDdlTransaction
44
+
45
+ MSG = 'Disable transaction in batch processing.'
46
+
47
+ RESTRICT_ON_SEND = %i[
48
+ delete_all
49
+ update_all
50
+ ].freeze
51
+
52
+ # @param node [RuboCop::AST::SendNode]
53
+ # @return [void]
54
+ def on_send(node)
55
+ return unless wrong?(node)
56
+
57
+ add_offense(node) do |corrector|
58
+ autocorrect(corrector, node)
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ # @param corrector [RuboCop::Cop::Corrector]
65
+ # @param node [RuboCop::AST::SendNode]
66
+ # @return [void]
67
+ def autocorrect(
68
+ corrector,
69
+ node
70
+ )
71
+ insert_disable_ddl_transaction(corrector, node)
72
+ end
73
+
74
+ # @param node [RuboCop::AST::SendNode]
75
+ # @return [Boolean]
76
+ def wrong?(node)
77
+ batch_processing?(node) &&
78
+ !within_disable_ddl_transaction?(node)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Sevencop
6
+ # Use throttling in batch processing.
7
+ #
8
+ # @safety
9
+ # There are some cases where we should not do throttling,
10
+ # or the throttling might be already done in a way that we cannot detect.
11
+ #
12
+ # @example
13
+ # # bad
14
+ # class BackfillSomeColumn < ActiveRecord::Migration[7.0]
15
+ # disable_ddl_transaction!
16
+ #
17
+ # def change
18
+ # User.in_batches do |relation|
19
+ # relation.update_all(some_column: 'some value')
20
+ # end
21
+ # end
22
+ # end
23
+ #
24
+ # # good
25
+ # class BackfillSomeColumnToUsers < ActiveRecord::Migration[7.0]
26
+ # disable_ddl_transaction!
27
+ #
28
+ # def up
29
+ # User.in_batches do |relation|
30
+ # relation.update_all(some_column: 'some value')
31
+ # sleep(0.01)
32
+ # end
33
+ # end
34
+ # end
35
+ class RailsMigrationBatchWithThrottling < RuboCop::Cop::Base
36
+ extend AutoCorrector
37
+
38
+ include ::Sevencop::CopConcerns::BatchProcessing
39
+
40
+ MSG = 'Use throttling in batch processing.'
41
+
42
+ RESTRICT_ON_SEND = %i[
43
+ delete_all
44
+ update_all
45
+ ].freeze
46
+
47
+ # @param node [RuboCop::AST::SendNode]
48
+ # @return [void]
49
+ def on_send(node)
50
+ return unless wrong?(node)
51
+
52
+ add_offense(node) do |corrector|
53
+ autocorrect(corrector, node)
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ # @!method sleep?(node)
60
+ # @param node [RuboCop::AST::Node]
61
+ # @return [Boolean]
62
+ def_node_matcher :sleep?, <<~PATTERN
63
+ (send
64
+ nil?
65
+ :sleep
66
+ ...
67
+ )
68
+ PATTERN
69
+
70
+ # @param corrector [RuboCop::Cop::Corrector]
71
+ # @param node [RuboCop::AST::SendNode]
72
+ # @return [void]
73
+ def autocorrect(
74
+ corrector,
75
+ node
76
+ )
77
+ corrector.insert_after(
78
+ node,
79
+ "\n#{' ' * node.location.column}sleep(0.01)"
80
+ )
81
+ end
82
+
83
+ # @param node [RuboCop::AST::Node]
84
+ # @return [Boolean]
85
+ def in_block?(node)
86
+ node.parent&.block_type? ||
87
+ (node.parent&.begin_type? && node.parent.parent&.block_type?)
88
+ end
89
+
90
+ # @param node [RuboCop::AST::SendNode]
91
+ # @return [Boolean]
92
+ def with_throttling?(node)
93
+ (node.left_siblings + node.right_siblings).any? do |sibling|
94
+ sleep?(sibling)
95
+ end
96
+ end
97
+
98
+ # @param node [RuboCop::AST::SendNode]
99
+ # @return [Boolean]
100
+ def wrong?(node)
101
+ batch_processing?(node) &&
102
+ in_block?(node) &&
103
+ !with_throttling?(node)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Sevencop
6
+ # Avoid changing column type that is in use.
7
+ #
8
+ # Changing the type of a column causes the entire table to be rewritten.
9
+ # During this time, reads and writes are blocked in Postgres, and writes are blocked in MySQL and MariaDB.
10
+ #
11
+ # Some changes don’t require a table rewrite and are safe in PostgreSQL:
12
+ #
13
+ # Type | Safe Changes
14
+ # --- | ---
15
+ # `cidr` | Changing to `inet`
16
+ # `citext` | Changing to `text` if not indexed, changing to `string` with no `:limit` if not indexed
17
+ # `datetime` | Increasing or removing `:precision`, changing to `timestamptz` when session time zone is UTC in Postgres 12+
18
+ # `decimal` | Increasing `:precision` at same `:scale`, removing `:precision` and `:scale`
19
+ # `interval` | Increasing or removing `:precision`
20
+ # `numeric` | Increasing `:precision` at same `:scale`, removing `:precision` and `:scale`
21
+ # `string` | Increasing or removing `:limit`, changing to `text`, changing `citext` if not indexed
22
+ # `text` | Changing to `string` with no `:limit`, changing to `citext` if not indexed
23
+ # `time` | Increasing or removing `:precision`
24
+ # `timestamptz` | Increasing or removing `:limit`, changing to `datetime` when session time zone is UTC in Postgres 12+
25
+ #
26
+ # And some in MySQL and MariaDB:
27
+ #
28
+ # Type | Safe Changes
29
+ # --- | ---
30
+ # `string` | Increasing `:limit` from under 255 up to 255, increasing `:limit` from over 255 to the max
31
+ #
32
+ # A safer approach is to:
33
+ #
34
+ # 1. Create a new column
35
+ # 2. Write to both columns
36
+ # 3. Backfill data from the old column to the new column
37
+ # 4. Move reads from the old column to the new column
38
+ # 5. Stop writing to the old column
39
+ # 6. Drop the old column
40
+ #
41
+ # @safety
42
+ # Only meaningful if the table is in use and the type change is really unsafe as described above.
43
+ #
44
+ # @example
45
+ # # bad
46
+ # class ChangeUsersSomeColumnType < ActiveRecord::Migration[7.0]
47
+ # def change
48
+ # change_column :users, :some_column, :new_type
49
+ # end
50
+ # end
51
+ #
52
+ # # good
53
+ # class AddUsersAnotherColumn < ActiveRecord::Migration[7.0]
54
+ # def change
55
+ # add_column :users, :another_column, :new_type
56
+ # end
57
+ # end
58
+ #
59
+ # class RemoveUsersSomeColumn < ActiveRecord::Migration[7.0]
60
+ # def change
61
+ # remove_column :users, :some_column
62
+ # end
63
+ # end
64
+ class RailsMigrationChangeColumn < RuboCop::Cop::Base
65
+ MSG = 'Avoid changing column type that is in use.'
66
+
67
+ RESTRICT_ON_SEND = %i[
68
+ change
69
+ change_column
70
+ ].freeze
71
+
72
+ # @param node [RuboCop::AST::SendNode]
73
+ # @return [void]
74
+ def on_send(node)
75
+ return unless bad?(node)
76
+
77
+ add_offense(node)
78
+ end
79
+
80
+ private
81
+
82
+ # @!method change?(node)
83
+ # @param node [RuboCop::AST::SendNode]
84
+ # @return [Boolean]
85
+ def_node_matcher :change?, <<~PATTERN
86
+ (send
87
+ lvar
88
+ :change
89
+ ...
90
+ )
91
+ PATTERN
92
+
93
+ # @!method change_column?(node)
94
+ # @param node [RuboCop::AST::SendNode]
95
+ # @return [Boolean]
96
+ def_node_matcher :change_column?, <<~PATTERN
97
+ (send
98
+ nil?
99
+ :change_column
100
+ ...
101
+ )
102
+ PATTERN
103
+
104
+ # @param node [RuboCop::AST::SendNode]
105
+ # @return [Boolean]
106
+ def bad?(node)
107
+ change?(node) ||
108
+ change_column?(node)
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Sevencop
6
+ # Avoid simply setting `NOT NULL` constraint on an existing column in PostgreSQL.
7
+ #
8
+ # It blocks reads and writes while every row is checked.
9
+ # In PostgreSQL 12+, you can safely set `NOT NULL` constraint if corresponding check constraint exists.
10
+ #
11
+ # @safety
12
+ # Only meaningful in PostgreSQL 12+.
13
+ #
14
+ # @example
15
+ # # bad
16
+ # class SetNotNullColumnConstraintToUsersName < ActiveRecord::Migration[7.0]
17
+ # def change
18
+ # change_column_null :users, :name, false
19
+ # end
20
+ # end
21
+ #
22
+ # # good
23
+ # class SetNotNullCheckConstraintToUsersName < ActiveRecord::Migration[7.0]
24
+ # def change
25
+ # add_check_constraint :users, 'name IS NOT NULL', name: 'users_name_is_not_null', validate: false
26
+ # end
27
+ # end
28
+ #
29
+ # class ReplaceNotNullConstraintOnUsersName < ActiveRecord::Migration[7.0]
30
+ # def change
31
+ # validate_constraint :users, name: 'users_name_is_not_null'
32
+ # change_column_null :users, :name, false
33
+ # remove_check_constraint :users, name: 'users_name_is_not_null'
34
+ # end
35
+ # end
36
+ class RailsMigrationChangeColumnNull < RuboCop::Cop::Base
37
+ extend AutoCorrector
38
+
39
+ MSG = 'Avoid simply setting `NOT NULL` constraint on an existing column in PostgreSQL.'
40
+
41
+ RESTRICT_ON_SEND = %i[
42
+ change_column_null
43
+ ].freeze
44
+
45
+ # @param node [RuboCop::AST::SendNode]
46
+ # @return [void]
47
+ def on_send(node)
48
+ return if in_second_migration?(node)
49
+
50
+ add_offense(node) do |corrector|
51
+ autocorrect(corrector, node)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ # @!method parse_table_name_and_column_name(node)
58
+ # @param node [RuboCop::AST::SendNode]
59
+ # @return [Array<Symbol>, nil]
60
+ def_node_matcher :parse_table_name_and_column_name, <<~PATTERN
61
+ (send
62
+ nil?
63
+ _
64
+ ({str sym} $_)
65
+ ({str sym} $_)
66
+ ...
67
+ )
68
+ PATTERN
69
+
70
+ # @!method remove_check_constraint?(node)
71
+ # @param node [RuboCop::AST::SendNode]
72
+ # @return [Boolean]
73
+ def_node_matcher :remove_check_constraint?, <<~PATTERN
74
+ (send nil? :remove_check_constraint ...)
75
+ PATTERN
76
+
77
+ # @!method validate_constraint?(node)
78
+ # @param node [RuboCop::AST::SendNode]
79
+ # @return [Boolean]
80
+ def_node_matcher :validate_constraint?, <<~PATTERN
81
+ (send nil? :validate_constraint ...)
82
+ PATTERN
83
+
84
+ # @param corrector [RuboCop::Cop::Corrector]
85
+ # @param node [RuboCop::AST::SendNode]
86
+ # @return [void]
87
+ def autocorrect(
88
+ corrector,
89
+ node
90
+ )
91
+ table_name, column_name = parse_table_name_and_column_name(node)
92
+ corrector.replace(
93
+ node,
94
+ format(
95
+ "add_check_constraint :%<table>s, '%<column>s IS NOT NULL', name: '%<constraint>s', validate: false",
96
+ column: column_name,
97
+ constraint: "#{table_name}_#{column_name}_is_not_null",
98
+ table: table_name
99
+ )
100
+ )
101
+ end
102
+
103
+ # @param node [RuboCop::AST::SendNode]
104
+ # @return [Boolean]
105
+ def called_after_validate_constraint?(node)
106
+ node.left_siblings.any? do |sibling|
107
+ validate_constraint?(sibling)
108
+ end
109
+ end
110
+
111
+ # @param node [RuboCop::AST::SendNode]
112
+ # @return [Boolean]
113
+ def called_before_remove_check_constraint?(node)
114
+ node.right_siblings.any? do |sibling|
115
+ remove_check_constraint?(sibling)
116
+ end
117
+ end
118
+
119
+ # @param node [RuboCop::AST::SendNode]
120
+ # @return [Boolean]
121
+ def in_second_migration?(node)
122
+ called_after_validate_constraint?(node) ||
123
+ called_before_remove_check_constraint?(node)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Sevencop
6
+ # Create tables without `force: true` option.
7
+ #
8
+ # The `force: true` option can drop an existing table.
9
+ # If you indend to drop an existing table, explicitly call `drop_table` first.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # class CreateUsers < ActiveRecord::Migration[7.0]
14
+ # def change
15
+ # create_table :users, force: true
16
+ # end
17
+ # end
18
+ #
19
+ # # good
20
+ # class CreateUsers < ActiveRecord::Migration[7.0]
21
+ # def change
22
+ # create_table :users
23
+ # end
24
+ # end
25
+ class RailsMigrationCreateTableForce < RuboCop::Cop::Base
26
+ extend AutoCorrector
27
+
28
+ include RangeHelp
29
+
30
+ MSG = 'Create tables without `force: true` option.'
31
+
32
+ RESTRICT_ON_SEND = %i[
33
+ create_table
34
+ ].freeze
35
+
36
+ # @param node [RuboCop::AST::SendNode]
37
+ # @return [void]
38
+ def on_send(node)
39
+ option_node = option_force_true_from_create_table(node)
40
+ return unless option_node
41
+
42
+ add_offense(option_node) do |corrector|
43
+ autocorrect(corrector, option_node)
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ # @!method option_force_true_from_create_table(node)
50
+ # @param node [RuboCop::AST::SendNode]
51
+ # @return [RuboCop::AST::PairNode, nil]
52
+ def_node_matcher :option_force_true_from_create_table, <<~PATTERN
53
+ (send
54
+ nil?
55
+ :create_table
56
+ _
57
+ (hash
58
+ <
59
+ $(pair
60
+ (sym :force)
61
+ true
62
+ )
63
+ ...
64
+ >
65
+ )
66
+ )
67
+ PATTERN
68
+
69
+ # @param corrector [RuboCop::Cop::Corrector]
70
+ # @param node [RuboCop::AST::PairNode]
71
+ # @return [void]
72
+ def autocorrect(
73
+ corrector,
74
+ node
75
+ )
76
+ corrector.remove(
77
+ range_with_surrounding_comma(
78
+ range_with_surrounding_space(
79
+ node.location.expression,
80
+ side: :left
81
+ ),
82
+ :left
83
+ )
84
+ )
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Sevencop
6
+ # Prefer `jsonb` to `json`.
7
+ #
8
+ # In PostgreSQL, there is no equality operator for the json column type,
9
+ # which can cause errors for existing `SELECT DISTINCT` queries in your application.
10
+ #
11
+ # @safety
12
+ # Only meaningful in PostgreSQL.
13
+ #
14
+ # @example
15
+ # # bad
16
+ # add_column :users, :properties, :json
17
+ #
18
+ # # good
19
+ # add_column :users, :properties, :jsonb
20
+ class RailsMigrationJsonb < RuboCop::Cop::Base
21
+ extend AutoCorrector
22
+
23
+ MSG = 'Prefer `jsonb` to `json`.'
24
+
25
+ RESTRICT_ON_SEND = %i[
26
+ add_column
27
+ change
28
+ change_column
29
+ json
30
+ ].freeze
31
+
32
+ # @param node [RuboCop::AST::SendNode]
33
+ # @return [void]
34
+ def on_send(node)
35
+ json_range = json_range_from_target_send_node(node)
36
+ return unless json_range
37
+
38
+ add_offense(json_range) do |corrector|
39
+ corrector.replace(json_range, 'jsonb')
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ # @!method json_type_node_from_add_column(node)
46
+ # @param node [RuboCop::AST::SendNode]
47
+ # @return [RuboCop::AST::SymNode, nil]
48
+ def_node_matcher :json_type_node_from_add_column, <<~PATTERN
49
+ (send
50
+ nil?
51
+ _
52
+ _
53
+ _
54
+ $(sym :json)
55
+ )
56
+ PATTERN
57
+ alias json_type_node_from_change_column json_type_node_from_add_column
58
+
59
+ # @!method json_type_node_from_change(node)
60
+ # @param node [RuboCop::AST::SendNode]
61
+ # @return [RuboCop::AST::SymNode, nil]
62
+ def_node_matcher :json_type_node_from_change, <<~PATTERN
63
+ (send
64
+ lvar
65
+ _
66
+ _
67
+ $(sym :json)
68
+ )
69
+ PATTERN
70
+
71
+ # @!method json_type_node_from_json(node)
72
+ # @param node [RuboCop::AST::SendNode]
73
+ # @return [RuboCop::AST::SendNode, nil]
74
+ def_node_matcher :json_type_node_from_json, <<~PATTERN
75
+ $(send
76
+ lvar
77
+ _
78
+ ...
79
+ )
80
+ PATTERN
81
+
82
+ # @param corrector [RuboCop::Cop::Corrector]
83
+ # @param node [RuboCop::AST::SendNode, RuboCop::AST::SymNode]
84
+ # @return [void]
85
+ def autocorrect(
86
+ corrector,
87
+ node
88
+ )
89
+ corrector.replace(node, 'jsonb')
90
+ end
91
+
92
+ # @param node [RuboCop::AST::SendNode]
93
+ # @return [RuboCop::AST::SymNode, nil]
94
+ def json_node_from_target_send_node(node)
95
+ case node.method_name
96
+ when :add_column
97
+ json_type_node_from_add_column(node)
98
+ when :change
99
+ json_type_node_from_change(node)
100
+ when :change_column
101
+ json_type_node_from_change_column(node)
102
+ when :json
103
+ json_type_node_from_json(node)
104
+ end
105
+ end
106
+
107
+ # @param node [RuboCop::AST::SendNode, RuboCop::AST::SymNode]
108
+ # @return [Parser::Source::Range]
109
+ def json_range_from_json_node(node)
110
+ case node.type
111
+ when :send
112
+ node.location.selector
113
+ when :sym
114
+ node.location.expression.with(
115
+ begin_pos: node.location.expression.begin_pos + 1
116
+ )
117
+ end
118
+ end
119
+
120
+ # @param node [RuboCop::AST::SendNode]
121
+ # @return [Parser::Source::Range]
122
+ def json_range_from_target_send_node(node)
123
+ json_node = json_node_from_target_send_node(node)
124
+ return unless json_node
125
+
126
+ json_range_from_json_node(json_node)
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end