rubocop-rails 2.4.1
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.
- checksums.yaml +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +92 -0
- data/bin/setup +7 -0
- data/config/default.yml +510 -0
- data/lib/rubocop-rails.rb +12 -0
- data/lib/rubocop/cop/mixin/target_rails_version.rb +16 -0
- data/lib/rubocop/cop/rails/action_filter.rb +111 -0
- data/lib/rubocop/cop/rails/active_record_aliases.rb +48 -0
- data/lib/rubocop/cop/rails/active_record_override.rb +82 -0
- data/lib/rubocop/cop/rails/active_support_aliases.rb +69 -0
- data/lib/rubocop/cop/rails/application_controller.rb +36 -0
- data/lib/rubocop/cop/rails/application_job.rb +40 -0
- data/lib/rubocop/cop/rails/application_mailer.rb +40 -0
- data/lib/rubocop/cop/rails/application_record.rb +40 -0
- data/lib/rubocop/cop/rails/assert_not.rb +44 -0
- data/lib/rubocop/cop/rails/belongs_to.rb +102 -0
- data/lib/rubocop/cop/rails/blank.rb +164 -0
- data/lib/rubocop/cop/rails/bulk_change_table.rb +293 -0
- data/lib/rubocop/cop/rails/create_table_with_timestamps.rb +91 -0
- data/lib/rubocop/cop/rails/date.rb +161 -0
- data/lib/rubocop/cop/rails/delegate.rb +132 -0
- data/lib/rubocop/cop/rails/delegate_allow_blank.rb +37 -0
- data/lib/rubocop/cop/rails/dynamic_find_by.rb +91 -0
- data/lib/rubocop/cop/rails/enum_hash.rb +75 -0
- data/lib/rubocop/cop/rails/enum_uniqueness.rb +65 -0
- data/lib/rubocop/cop/rails/environment_comparison.rb +68 -0
- data/lib/rubocop/cop/rails/exit.rb +67 -0
- data/lib/rubocop/cop/rails/file_path.rb +108 -0
- data/lib/rubocop/cop/rails/find_by.rb +55 -0
- data/lib/rubocop/cop/rails/find_each.rb +51 -0
- data/lib/rubocop/cop/rails/has_and_belongs_to_many.rb +25 -0
- data/lib/rubocop/cop/rails/has_many_or_has_one_dependent.rb +106 -0
- data/lib/rubocop/cop/rails/helper_instance_variable.rb +39 -0
- data/lib/rubocop/cop/rails/http_positional_arguments.rb +117 -0
- data/lib/rubocop/cop/rails/http_status.rb +160 -0
- data/lib/rubocop/cop/rails/ignored_skip_action_filter_option.rb +94 -0
- data/lib/rubocop/cop/rails/inverse_of.rb +246 -0
- data/lib/rubocop/cop/rails/lexically_scoped_action_filter.rb +175 -0
- data/lib/rubocop/cop/rails/link_to_blank.rb +98 -0
- data/lib/rubocop/cop/rails/not_null_column.rb +67 -0
- data/lib/rubocop/cop/rails/output.rb +49 -0
- data/lib/rubocop/cop/rails/output_safety.rb +99 -0
- data/lib/rubocop/cop/rails/pluralization_grammar.rb +107 -0
- data/lib/rubocop/cop/rails/presence.rb +148 -0
- data/lib/rubocop/cop/rails/present.rb +153 -0
- data/lib/rubocop/cop/rails/rake_environment.rb +91 -0
- data/lib/rubocop/cop/rails/read_write_attribute.rb +74 -0
- data/lib/rubocop/cop/rails/redundant_allow_nil.rb +111 -0
- data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +136 -0
- data/lib/rubocop/cop/rails/reflection_class_name.rb +37 -0
- data/lib/rubocop/cop/rails/refute_methods.rb +76 -0
- data/lib/rubocop/cop/rails/relative_date_constant.rb +102 -0
- data/lib/rubocop/cop/rails/request_referer.rb +56 -0
- data/lib/rubocop/cop/rails/reversible_migration.rb +284 -0
- data/lib/rubocop/cop/rails/safe_navigation.rb +85 -0
- data/lib/rubocop/cop/rails/safe_navigation_with_blank.rb +48 -0
- data/lib/rubocop/cop/rails/save_bang.rb +331 -0
- data/lib/rubocop/cop/rails/scope_args.rb +29 -0
- data/lib/rubocop/cop/rails/skips_model_validations.rb +87 -0
- data/lib/rubocop/cop/rails/time_zone.rb +249 -0
- data/lib/rubocop/cop/rails/uniq_before_pluck.rb +105 -0
- data/lib/rubocop/cop/rails/unknown_env.rb +84 -0
- data/lib/rubocop/cop/rails/validation.rb +147 -0
- data/lib/rubocop/cop/rails_cops.rb +61 -0
- data/lib/rubocop/rails.rb +12 -0
- data/lib/rubocop/rails/inject.rb +18 -0
- data/lib/rubocop/rails/version.rb +10 -0
- metadata +148 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks that models subclass ApplicationRecord with Rails 5.0.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
#
|
10
|
+
# # good
|
11
|
+
# class Rails5Model < ApplicationRecord
|
12
|
+
# # ...
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# # bad
|
16
|
+
# class Rails4Model < ActiveRecord::Base
|
17
|
+
# # ...
|
18
|
+
# end
|
19
|
+
class ApplicationRecord < Cop
|
20
|
+
extend TargetRailsVersion
|
21
|
+
|
22
|
+
minimum_target_rails_version 5.0
|
23
|
+
|
24
|
+
MSG = 'Models should subclass `ApplicationRecord`.'
|
25
|
+
SUPERCLASS = 'ApplicationRecord'
|
26
|
+
BASE_PATTERN = '(const (const nil? :ActiveRecord) :Base)'
|
27
|
+
|
28
|
+
# rubocop:disable Layout/ClassStructure
|
29
|
+
include RuboCop::Cop::EnforceSuperclass
|
30
|
+
# rubocop:enable Layout/ClassStructure
|
31
|
+
|
32
|
+
def autocorrect(node)
|
33
|
+
lambda do |corrector|
|
34
|
+
corrector.replace(node.source_range, self.class::SUPERCLASS)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
#
|
7
|
+
# Use `assert_not` instead of `assert !`.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# # bad
|
11
|
+
# assert !x
|
12
|
+
#
|
13
|
+
# # good
|
14
|
+
# assert_not x
|
15
|
+
#
|
16
|
+
class AssertNot < RuboCop::Cop::Cop
|
17
|
+
MSG = 'Prefer `assert_not` over `assert !`.'
|
18
|
+
|
19
|
+
def_node_matcher :offensive?, '(send nil? :assert (send ... :!) ...)'
|
20
|
+
|
21
|
+
def on_send(node)
|
22
|
+
add_offense(node) if offensive?(node)
|
23
|
+
end
|
24
|
+
|
25
|
+
def autocorrect(node)
|
26
|
+
expression = node.loc.expression
|
27
|
+
|
28
|
+
lambda do |corrector|
|
29
|
+
corrector.replace(
|
30
|
+
expression,
|
31
|
+
corrected_source(expression.source)
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def corrected_source(source)
|
39
|
+
source.gsub(/^assert(\(| ) *! */, 'assert_not\\1')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop looks for belongs_to associations where we control whether the
|
7
|
+
# association is required via the deprecated `required` option instead.
|
8
|
+
#
|
9
|
+
# Since Rails 5, belongs_to associations are required by default and this
|
10
|
+
# can be controlled through the use of `optional: true`.
|
11
|
+
#
|
12
|
+
# From the release notes:
|
13
|
+
#
|
14
|
+
# belongs_to will now trigger a validation error by default if the
|
15
|
+
# association is not present. You can turn this off on a
|
16
|
+
# per-association basis with optional: true. Also deprecate required
|
17
|
+
# option in favor of optional for belongs_to. (Pull Request)
|
18
|
+
#
|
19
|
+
# In the case that the developer is doing `required: false`, we
|
20
|
+
# definitely want to autocorrect to `optional: true`.
|
21
|
+
#
|
22
|
+
# However, without knowing whether they've set overridden the default
|
23
|
+
# value of `config.active_record.belongs_to_required_by_default`, we
|
24
|
+
# can't say whether it's safe to remove `required: true` or whether we
|
25
|
+
# should replace it with `optional: false` (or, similarly, remove a
|
26
|
+
# superfluous `optional: false`). Therefore, in the cases we're using
|
27
|
+
# `required: true`, we'll simply invert it to `optional: false` and the
|
28
|
+
# user can remove depending on their defaults.
|
29
|
+
#
|
30
|
+
# @example
|
31
|
+
# # bad
|
32
|
+
# class Post < ApplicationRecord
|
33
|
+
# belongs_to :blog, required: false
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# # good
|
37
|
+
# class Post < ApplicationRecord
|
38
|
+
# belongs_to :blog, optional: true
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# # bad
|
42
|
+
# class Post < ApplicationRecord
|
43
|
+
# belongs_to :blog, required: true
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# # good
|
47
|
+
# class Post < ApplicationRecord
|
48
|
+
# belongs_to :blog, optional: false
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# @see https://guides.rubyonrails.org/5_0_release_notes.html
|
52
|
+
# @see https://github.com/rails/rails/pull/18937
|
53
|
+
class BelongsTo < Cop
|
54
|
+
extend TargetRailsVersion
|
55
|
+
|
56
|
+
minimum_target_rails_version 5.0
|
57
|
+
|
58
|
+
SUPERFLOUS_REQUIRE_FALSE_MSG =
|
59
|
+
'You specified `required: false`, in Rails > 5.0 the required ' \
|
60
|
+
'option is deprecated and you want to use `optional: true`.'
|
61
|
+
|
62
|
+
SUPERFLOUS_REQUIRE_TRUE_MSG =
|
63
|
+
'You specified `required: true`, in Rails > 5.0 the required ' \
|
64
|
+
'option is deprecated and you want to use `optional: false`. ' \
|
65
|
+
'In most configurations, this is the default and you can omit ' \
|
66
|
+
'this option altogether'
|
67
|
+
|
68
|
+
def_node_matcher :match_belongs_to_with_options, <<~PATTERN
|
69
|
+
(send _ :belongs_to _
|
70
|
+
(hash <$(pair (sym :required) ${true false}) ...>)
|
71
|
+
)
|
72
|
+
PATTERN
|
73
|
+
|
74
|
+
def on_send(node)
|
75
|
+
match_belongs_to_with_options(node) do |_option_node, option_value|
|
76
|
+
message =
|
77
|
+
if option_value.true_type?
|
78
|
+
SUPERFLOUS_REQUIRE_TRUE_MSG
|
79
|
+
elsif option_value.false_type?
|
80
|
+
SUPERFLOUS_REQUIRE_FALSE_MSG
|
81
|
+
end
|
82
|
+
|
83
|
+
add_offense(node, message: message, location: :selector)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def autocorrect(node)
|
88
|
+
option_node, option_value = match_belongs_to_with_options(node)
|
89
|
+
return unless option_node
|
90
|
+
|
91
|
+
lambda do |corrector|
|
92
|
+
if option_value.true_type?
|
93
|
+
corrector.replace(option_node.loc.expression, 'optional: false')
|
94
|
+
elsif option_value.false_type?
|
95
|
+
corrector.replace(option_node.loc.expression, 'optional: true')
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks for code that can be written with simpler conditionals
|
7
|
+
# using `Object#blank?` defined by Active Support.
|
8
|
+
#
|
9
|
+
# Interaction with `Style/UnlessElse`:
|
10
|
+
# The configuration of `NotPresent` will not produce an offense in the
|
11
|
+
# context of `unless else` if `Style/UnlessElse` is inabled. This is
|
12
|
+
# to prevent interference between the auto-correction of the two cops.
|
13
|
+
#
|
14
|
+
# @example NilOrEmpty: true (default)
|
15
|
+
# # Converts usages of `nil? || empty?` to `blank?`
|
16
|
+
#
|
17
|
+
# # bad
|
18
|
+
# foo.nil? || foo.empty?
|
19
|
+
# foo == nil || foo.empty?
|
20
|
+
#
|
21
|
+
# # good
|
22
|
+
# foo.blank?
|
23
|
+
#
|
24
|
+
# @example NotPresent: true (default)
|
25
|
+
# # Converts usages of `!present?` to `blank?`
|
26
|
+
#
|
27
|
+
# # bad
|
28
|
+
# !foo.present?
|
29
|
+
#
|
30
|
+
# # good
|
31
|
+
# foo.blank?
|
32
|
+
#
|
33
|
+
# @example UnlessPresent: true (default)
|
34
|
+
# # Converts usages of `unless present?` to `if blank?`
|
35
|
+
#
|
36
|
+
# # bad
|
37
|
+
# something unless foo.present?
|
38
|
+
#
|
39
|
+
# # good
|
40
|
+
# something if foo.blank?
|
41
|
+
#
|
42
|
+
# # bad
|
43
|
+
# unless foo.present?
|
44
|
+
# something
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# # good
|
48
|
+
# if foo.blank?
|
49
|
+
# something
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# # good
|
53
|
+
# def blank?
|
54
|
+
# !present?
|
55
|
+
# end
|
56
|
+
class Blank < Cop
|
57
|
+
MSG_NIL_OR_EMPTY = 'Use `%<prefer>s` instead of `%<current>s`.'
|
58
|
+
MSG_NOT_PRESENT = 'Use `%<prefer>s` instead of `%<current>s`.'
|
59
|
+
MSG_UNLESS_PRESENT = 'Use `if %<prefer>s` instead of ' \
|
60
|
+
'`%<current>s`.'
|
61
|
+
|
62
|
+
# `(send nil $_)` is not actually a valid match for an offense. Nodes
|
63
|
+
# that have a single method call on the left hand side
|
64
|
+
# (`bar || foo.empty?`) will blow up when checking
|
65
|
+
# `(send (:nil) :== $_)`.
|
66
|
+
def_node_matcher :nil_or_empty?, <<~PATTERN
|
67
|
+
(or
|
68
|
+
{
|
69
|
+
(send $_ :!)
|
70
|
+
(send $_ :nil?)
|
71
|
+
(send $_ :== nil)
|
72
|
+
(send nil :== $_)
|
73
|
+
}
|
74
|
+
{
|
75
|
+
(send $_ :empty?)
|
76
|
+
(send (send (send $_ :empty?) :!) :!)
|
77
|
+
}
|
78
|
+
)
|
79
|
+
PATTERN
|
80
|
+
|
81
|
+
def_node_matcher :not_present?, '(send (send $_ :present?) :!)'
|
82
|
+
|
83
|
+
def_node_matcher :defining_blank?, '(def :blank? (args) ...)'
|
84
|
+
|
85
|
+
def_node_matcher :unless_present?, <<~PATTERN
|
86
|
+
(:if $(send $_ :present?) {nil? (...)} ...)
|
87
|
+
PATTERN
|
88
|
+
|
89
|
+
def on_send(node)
|
90
|
+
return unless cop_config['NotPresent']
|
91
|
+
|
92
|
+
not_present?(node) do |receiver|
|
93
|
+
# accepts !present? if its in the body of a `blank?` method
|
94
|
+
next if defining_blank?(node.parent)
|
95
|
+
|
96
|
+
add_offense(node,
|
97
|
+
message: format(MSG_NOT_PRESENT,
|
98
|
+
prefer: replacement(receiver),
|
99
|
+
current: node.source))
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def on_or(node)
|
104
|
+
return unless cop_config['NilOrEmpty']
|
105
|
+
|
106
|
+
nil_or_empty?(node) do |var1, var2|
|
107
|
+
return unless var1 == var2
|
108
|
+
|
109
|
+
add_offense(node,
|
110
|
+
message: format(MSG_NIL_OR_EMPTY,
|
111
|
+
prefer: replacement(var1),
|
112
|
+
current: node.source))
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def on_if(node)
|
117
|
+
return unless cop_config['UnlessPresent']
|
118
|
+
return unless node.unless?
|
119
|
+
return if node.else? && config.for_cop('Style/UnlessElse')['Enabled']
|
120
|
+
|
121
|
+
unless_present?(node) do |method_call, receiver|
|
122
|
+
range = unless_condition(node, method_call)
|
123
|
+
|
124
|
+
add_offense(node,
|
125
|
+
location: range,
|
126
|
+
message: format(MSG_UNLESS_PRESENT,
|
127
|
+
prefer: replacement(receiver),
|
128
|
+
current: range.source))
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def autocorrect(node)
|
133
|
+
lambda do |corrector|
|
134
|
+
method_call, variable1 = unless_present?(node)
|
135
|
+
|
136
|
+
if method_call
|
137
|
+
corrector.replace(node.loc.keyword, 'if')
|
138
|
+
range = method_call.loc.expression
|
139
|
+
else
|
140
|
+
variable1, _variable2 = nil_or_empty?(node) || not_present?(node)
|
141
|
+
range = node.loc.expression
|
142
|
+
end
|
143
|
+
|
144
|
+
corrector.replace(range, replacement(variable1))
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
def unless_condition(node, method_call)
|
151
|
+
if node.modifier_form?
|
152
|
+
node.loc.keyword.join(node.loc.expression.end)
|
153
|
+
else
|
154
|
+
node.loc.expression.begin.join(method_call.loc.expression)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def replacement(node)
|
159
|
+
node.respond_to?(:source) ? "#{node.source}.blank?" : 'blank?'
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,293 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This Cop checks whether alter queries are combinable.
|
7
|
+
# If combinable queries are detected, it suggests to you
|
8
|
+
# to use `change_table` with `bulk: true` instead.
|
9
|
+
# This option causes the migration to generate a single
|
10
|
+
# ALTER TABLE statement combining multiple column alterations.
|
11
|
+
#
|
12
|
+
# The `bulk` option is only supported on the MySQL and
|
13
|
+
# the PostgreSQL (5.2 later) adapter; thus it will
|
14
|
+
# automatically detect an adapter from `development` environment
|
15
|
+
# in `config/database.yml` when the `Database` option is not set.
|
16
|
+
# If the adapter is not `mysql2` or `postgresql`,
|
17
|
+
# this Cop ignores offenses.
|
18
|
+
#
|
19
|
+
# @example
|
20
|
+
# # bad
|
21
|
+
# def change
|
22
|
+
# add_column :users, :name, :string, null: false
|
23
|
+
# add_column :users, :nickname, :string
|
24
|
+
#
|
25
|
+
# # ALTER TABLE `users` ADD `name` varchar(255) NOT NULL
|
26
|
+
# # ALTER TABLE `users` ADD `nickname` varchar(255)
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# # good
|
30
|
+
# def change
|
31
|
+
# change_table :users, bulk: true do |t|
|
32
|
+
# t.string :name, null: false
|
33
|
+
# t.string :nickname
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# # ALTER TABLE `users` ADD `name` varchar(255) NOT NULL,
|
37
|
+
# # ADD `nickname` varchar(255)
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# @example
|
41
|
+
# # bad
|
42
|
+
# def change
|
43
|
+
# change_table :users do |t|
|
44
|
+
# t.string :name, null: false
|
45
|
+
# t.string :nickname
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# # good
|
50
|
+
# def change
|
51
|
+
# change_table :users, bulk: true do |t|
|
52
|
+
# t.string :name, null: false
|
53
|
+
# t.string :nickname
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# # good
|
58
|
+
# # When you don't want to combine alter queries.
|
59
|
+
# def change
|
60
|
+
# change_table :users, bulk: false do |t|
|
61
|
+
# t.string :name, null: false
|
62
|
+
# t.string :nickname
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# @see https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-change_table
|
67
|
+
# @see https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Table.html
|
68
|
+
class BulkChangeTable < Cop
|
69
|
+
MSG_FOR_CHANGE_TABLE = <<~MSG.chomp
|
70
|
+
You can combine alter queries using `bulk: true` options.
|
71
|
+
MSG
|
72
|
+
MSG_FOR_ALTER_METHODS = <<~MSG.chomp
|
73
|
+
You can use `change_table :%<table>s, bulk: true` to combine alter queries.
|
74
|
+
MSG
|
75
|
+
|
76
|
+
MYSQL = 'mysql'
|
77
|
+
POSTGRESQL = 'postgresql'
|
78
|
+
|
79
|
+
MIGRATION_METHODS = %i[change up down].freeze
|
80
|
+
|
81
|
+
COMBINABLE_TRANSFORMATIONS = %i[
|
82
|
+
primary_key
|
83
|
+
column
|
84
|
+
string
|
85
|
+
text
|
86
|
+
integer
|
87
|
+
bigint
|
88
|
+
float
|
89
|
+
decimal
|
90
|
+
numeric
|
91
|
+
datetime
|
92
|
+
timestamp
|
93
|
+
time
|
94
|
+
date
|
95
|
+
binary
|
96
|
+
boolean
|
97
|
+
json
|
98
|
+
virtual
|
99
|
+
remove
|
100
|
+
change
|
101
|
+
timestamps
|
102
|
+
remove_timestamps
|
103
|
+
].freeze
|
104
|
+
|
105
|
+
COMBINABLE_ALTER_METHODS = %i[
|
106
|
+
add_column
|
107
|
+
remove_column
|
108
|
+
remove_columns
|
109
|
+
change_column
|
110
|
+
add_timestamps
|
111
|
+
remove_timestamps
|
112
|
+
].freeze
|
113
|
+
|
114
|
+
MYSQL_COMBINABLE_TRANSFORMATIONS = %i[
|
115
|
+
rename
|
116
|
+
index
|
117
|
+
remove_index
|
118
|
+
].freeze
|
119
|
+
|
120
|
+
MYSQL_COMBINABLE_ALTER_METHODS = %i[
|
121
|
+
rename_column
|
122
|
+
add_index
|
123
|
+
remove_index
|
124
|
+
].freeze
|
125
|
+
|
126
|
+
POSTGRESQL_COMBINABLE_TRANSFORMATIONS = %i[
|
127
|
+
change_default
|
128
|
+
].freeze
|
129
|
+
|
130
|
+
POSTGRESQL_COMBINABLE_ALTER_METHODS = %i[
|
131
|
+
change_column_default
|
132
|
+
].freeze
|
133
|
+
|
134
|
+
def on_def(node)
|
135
|
+
return unless support_bulk_alter?
|
136
|
+
return unless MIGRATION_METHODS.include?(node.method_name)
|
137
|
+
return unless node.body
|
138
|
+
|
139
|
+
recorder = AlterMethodsRecorder.new
|
140
|
+
|
141
|
+
node.body.child_nodes.each do |child_node|
|
142
|
+
if call_to_combinable_alter_method? child_node
|
143
|
+
recorder.process(child_node)
|
144
|
+
else
|
145
|
+
recorder.flush
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
recorder.offensive_nodes.each { |n| add_offense_for_alter_methods(n) }
|
150
|
+
end
|
151
|
+
|
152
|
+
def on_send(node)
|
153
|
+
return unless support_bulk_alter?
|
154
|
+
return unless node.command?(:change_table)
|
155
|
+
return if include_bulk_options?(node)
|
156
|
+
return unless node.block_node
|
157
|
+
|
158
|
+
send_nodes = node.block_node.body.each_child_node(:send).to_a
|
159
|
+
|
160
|
+
transformations = send_nodes.select do |send_node|
|
161
|
+
combinable_transformations.include?(send_node.method_name)
|
162
|
+
end
|
163
|
+
|
164
|
+
add_offense_for_change_table(node) if transformations.size > 1
|
165
|
+
end
|
166
|
+
|
167
|
+
private
|
168
|
+
|
169
|
+
# @param node [RuboCop::AST::SendNode] (send nil? :change_table ...)
|
170
|
+
def include_bulk_options?(node)
|
171
|
+
# arguments: [{(sym :table)(str "table")} (hash (pair (sym :bulk) _))]
|
172
|
+
options = node.arguments[1]
|
173
|
+
return false unless options
|
174
|
+
|
175
|
+
options.hash_type? &&
|
176
|
+
options.keys.any? { |key| key.sym_type? && key.value == :bulk }
|
177
|
+
end
|
178
|
+
|
179
|
+
def database
|
180
|
+
cop_config['Database'] || database_from_yaml
|
181
|
+
end
|
182
|
+
|
183
|
+
def database_from_yaml
|
184
|
+
return nil unless database_yaml
|
185
|
+
|
186
|
+
case database_yaml['adapter']
|
187
|
+
when 'mysql2'
|
188
|
+
MYSQL
|
189
|
+
when 'postgresql'
|
190
|
+
POSTGRESQL
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def database_yaml
|
195
|
+
return nil unless File.exist?('config/database.yml')
|
196
|
+
|
197
|
+
yaml = YAML.load_file('config/database.yml')
|
198
|
+
return nil unless yaml.is_a? Hash
|
199
|
+
|
200
|
+
config = yaml['development']
|
201
|
+
return nil unless config.is_a?(Hash)
|
202
|
+
|
203
|
+
config
|
204
|
+
rescue Psych::SyntaxError
|
205
|
+
nil
|
206
|
+
end
|
207
|
+
|
208
|
+
def support_bulk_alter?
|
209
|
+
case database
|
210
|
+
when MYSQL
|
211
|
+
true
|
212
|
+
when POSTGRESQL
|
213
|
+
# Add bulk alter support for PostgreSQL in 5.2.0
|
214
|
+
# @see https://github.com/rails/rails/pull/31331
|
215
|
+
target_rails_version >= 5.2
|
216
|
+
else
|
217
|
+
false
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def call_to_combinable_alter_method?(child_node)
|
222
|
+
child_node.send_type? &&
|
223
|
+
combinable_alter_methods.include?(child_node.method_name)
|
224
|
+
end
|
225
|
+
|
226
|
+
def combinable_alter_methods
|
227
|
+
case database
|
228
|
+
when MYSQL
|
229
|
+
COMBINABLE_ALTER_METHODS + MYSQL_COMBINABLE_ALTER_METHODS
|
230
|
+
when POSTGRESQL
|
231
|
+
COMBINABLE_ALTER_METHODS + POSTGRESQL_COMBINABLE_ALTER_METHODS
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def combinable_transformations
|
236
|
+
case database
|
237
|
+
when MYSQL
|
238
|
+
COMBINABLE_TRANSFORMATIONS + MYSQL_COMBINABLE_TRANSFORMATIONS
|
239
|
+
when POSTGRESQL
|
240
|
+
COMBINABLE_TRANSFORMATIONS + POSTGRESQL_COMBINABLE_TRANSFORMATIONS
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# @param node [RuboCop::AST::SendNode]
|
245
|
+
def add_offense_for_alter_methods(node)
|
246
|
+
# arguments: [{(sym :table)(str "table")} ...]
|
247
|
+
table_node = node.arguments[0]
|
248
|
+
return unless table_node.is_a? RuboCop::AST::BasicLiteralNode
|
249
|
+
|
250
|
+
message = format(MSG_FOR_ALTER_METHODS, table: table_node.value)
|
251
|
+
add_offense(node, message: message)
|
252
|
+
end
|
253
|
+
|
254
|
+
# @param node [RuboCop::AST::SendNode]
|
255
|
+
def add_offense_for_change_table(node)
|
256
|
+
add_offense(node, message: MSG_FOR_CHANGE_TABLE)
|
257
|
+
end
|
258
|
+
|
259
|
+
# Record combinable alter methods and register offensive nodes.
|
260
|
+
class AlterMethodsRecorder
|
261
|
+
def initialize
|
262
|
+
@nodes = []
|
263
|
+
@offensive_nodes = []
|
264
|
+
end
|
265
|
+
|
266
|
+
# @param new_node [RuboCop::AST::SendNode]
|
267
|
+
def process(new_node)
|
268
|
+
# arguments: [{(sym :table)(str "table")} ...]
|
269
|
+
table_node = new_node.arguments[0]
|
270
|
+
if table_node.is_a? RuboCop::AST::BasicLiteralNode
|
271
|
+
flush unless @nodes.all? do |node|
|
272
|
+
node.arguments[0].value.to_s == table_node.value.to_s
|
273
|
+
end
|
274
|
+
@nodes << new_node
|
275
|
+
else
|
276
|
+
flush
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def flush
|
281
|
+
@offensive_nodes << @nodes.first if @nodes.size > 1
|
282
|
+
@nodes = []
|
283
|
+
end
|
284
|
+
|
285
|
+
def offensive_nodes
|
286
|
+
flush
|
287
|
+
@offensive_nodes
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|