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