rubocop-rails 2.9.0 → 2.11.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 +4 -4
- data/LICENSE.txt +1 -1
- data/README.md +4 -4
- data/config/default.yml +111 -5
- data/config/obsoletion.yml +7 -0
- data/lib/rubocop/cop/mixin/active_record_helper.rb +11 -0
- data/lib/rubocop/cop/rails/add_column_index.rb +64 -0
- data/lib/rubocop/cop/rails/attribute_default_block_value.rb +1 -1
- data/lib/rubocop/cop/rails/belongs_to.rb +1 -1
- data/lib/rubocop/cop/rails/blank.rb +4 -0
- data/lib/rubocop/cop/rails/content_tag.rb +18 -3
- data/lib/rubocop/cop/rails/date.rb +17 -6
- data/lib/rubocop/cop/rails/dynamic_find_by.rb +4 -2
- data/lib/rubocop/cop/rails/eager_evaluation_log_message.rb +78 -0
- data/lib/rubocop/cop/rails/environment_comparison.rb +1 -2
- data/lib/rubocop/cop/rails/environment_variable_access.rb +67 -0
- data/lib/rubocop/cop/rails/expanded_date_range.rb +86 -0
- data/lib/rubocop/cop/rails/file_path.rb +2 -4
- data/lib/rubocop/cop/rails/find_by.rb +26 -11
- data/lib/rubocop/cop/rails/find_each.rb +6 -7
- data/lib/rubocop/cop/rails/has_many_or_has_one_dependent.rb +34 -4
- data/lib/rubocop/cop/rails/helper_instance_variable.rb +27 -1
- data/lib/rubocop/cop/rails/http_positional_arguments.rb +8 -1
- data/lib/rubocop/cop/rails/http_status.rb +12 -3
- data/lib/rubocop/cop/rails/i18n_locale_assignment.rb +37 -0
- data/lib/rubocop/cop/rails/inverse_of.rb +1 -2
- data/lib/rubocop/cop/rails/link_to_blank.rb +5 -1
- data/lib/rubocop/cop/rails/reflection_class_name.rb +14 -1
- data/lib/rubocop/cop/rails/relative_date_constant.rb +18 -19
- data/lib/rubocop/cop/rails/require_dependency.rb +38 -0
- data/lib/rubocop/cop/rails/reversible_migration.rb +1 -1
- data/lib/rubocop/cop/rails/reversible_migration_method_definition.rb +75 -0
- data/lib/rubocop/cop/rails/safe_navigation.rb +20 -2
- data/lib/rubocop/cop/rails/time_zone.rb +22 -22
- data/lib/rubocop/cop/rails/time_zone_assignment.rb +37 -0
- data/lib/rubocop/cop/rails/unknown_env.rb +1 -1
- data/lib/rubocop/cop/rails/unused_ignored_columns.rb +69 -0
- data/lib/rubocop/cop/rails/where_equals.rb +6 -2
- data/lib/rubocop/cop/rails/where_exists.rb +11 -0
- data/lib/rubocop/cop/rails/where_not.rb +5 -1
- data/lib/rubocop/cop/rails_cops.rb +9 -0
- data/lib/rubocop/rails.rb +2 -0
- data/lib/rubocop/rails/schema_loader/schema.rb +2 -4
- data/lib/rubocop/rails/version.rb +1 -1
- metadata +21 -11
@@ -5,7 +5,9 @@ module RuboCop
|
|
5
5
|
module Rails
|
6
6
|
# This cop looks for `has_many` or `has_one` associations that don't
|
7
7
|
# specify a `:dependent` option.
|
8
|
-
#
|
8
|
+
#
|
9
|
+
# It doesn't register an offense if `:through` or `dependent: nil`
|
10
|
+
# is specified, or if the model is read-only.
|
9
11
|
#
|
10
12
|
# @example
|
11
13
|
# # bad
|
@@ -18,8 +20,18 @@ module RuboCop
|
|
18
20
|
# class User < ActiveRecord::Base
|
19
21
|
# has_many :comments, dependent: :restrict_with_exception
|
20
22
|
# has_one :avatar, dependent: :destroy
|
23
|
+
# has_many :articles, dependent: nil
|
21
24
|
# has_many :patients, through: :appointments
|
22
25
|
# end
|
26
|
+
#
|
27
|
+
# class User < ActiveRecord::Base
|
28
|
+
# has_many :comments
|
29
|
+
# has_one :avatar
|
30
|
+
#
|
31
|
+
# def readonly?
|
32
|
+
# true
|
33
|
+
# end
|
34
|
+
# end
|
23
35
|
class HasManyOrHasOneDependent < Base
|
24
36
|
MSG = 'Specify a `:dependent` option.'
|
25
37
|
RESTRICT_ON_SEND = %i[has_many has_one].freeze
|
@@ -37,7 +49,7 @@ module RuboCop
|
|
37
49
|
PATTERN
|
38
50
|
|
39
51
|
def_node_matcher :dependent_option?, <<~PATTERN
|
40
|
-
(pair (sym :dependent) !nil)
|
52
|
+
(pair (sym :dependent) {!nil (nil)})
|
41
53
|
PATTERN
|
42
54
|
|
43
55
|
def_node_matcher :present_option?, <<~PATTERN
|
@@ -51,8 +63,20 @@ module RuboCop
|
|
51
63
|
(args) ...)
|
52
64
|
PATTERN
|
53
65
|
|
66
|
+
def_node_matcher :association_extension_block?, <<~PATTERN
|
67
|
+
(block
|
68
|
+
(send nil? :has_many _)
|
69
|
+
(args) ...)
|
70
|
+
PATTERN
|
71
|
+
|
72
|
+
def_node_matcher :readonly?, <<~PATTERN
|
73
|
+
(def :readonly?
|
74
|
+
(args)
|
75
|
+
(true))
|
76
|
+
PATTERN
|
77
|
+
|
54
78
|
def on_send(node)
|
55
|
-
return if active_resource?(node.parent)
|
79
|
+
return if active_resource?(node.parent) || readonly_model?(node)
|
56
80
|
return if !association_without_options?(node) && valid_options?(association_with_options?(node))
|
57
81
|
return if valid_options_in_with_options_block?(node)
|
58
82
|
|
@@ -61,10 +85,16 @@ module RuboCop
|
|
61
85
|
|
62
86
|
private
|
63
87
|
|
88
|
+
def readonly_model?(node)
|
89
|
+
return false unless (parent = node.parent)
|
90
|
+
|
91
|
+
parent.each_descendant(:def).any? { |def_node| readonly?(def_node) }
|
92
|
+
end
|
93
|
+
|
64
94
|
def valid_options_in_with_options_block?(node)
|
65
95
|
return true unless node.parent
|
66
96
|
|
67
|
-
n = node.parent.begin_type? ? node.parent.parent : node.parent
|
97
|
+
n = node.parent.begin_type? || association_extension_block?(node.parent) ? node.parent.parent : node.parent
|
68
98
|
|
69
99
|
contain_valid_options_in_with_options_block?(n)
|
70
100
|
end
|
@@ -13,6 +13,9 @@ module RuboCop
|
|
13
13
|
# variable, consider moving the behaviour elsewhere, for
|
14
14
|
# example to a model, decorator or presenter.
|
15
15
|
#
|
16
|
+
# Provided that a class inherits `ActionView::Helpers::FormBuilder`,
|
17
|
+
# an offense will not be registered.
|
18
|
+
#
|
16
19
|
# @example
|
17
20
|
# # bad
|
18
21
|
# def welcome_message
|
@@ -23,18 +26,41 @@ module RuboCop
|
|
23
26
|
# def welcome_message(user)
|
24
27
|
# "Hello #{user.name}"
|
25
28
|
# end
|
29
|
+
#
|
30
|
+
# # good
|
31
|
+
# class MyFormBuilder < ActionView::Helpers::FormBuilder
|
32
|
+
# @template.do_something
|
33
|
+
# end
|
26
34
|
class HelperInstanceVariable < Base
|
27
35
|
MSG = 'Do not use instance variables in helpers.'
|
28
36
|
|
37
|
+
def_node_matcher :form_builder_class?, <<~PATTERN
|
38
|
+
(const
|
39
|
+
(const
|
40
|
+
(const nil? :ActionView) :Helpers) :FormBuilder)
|
41
|
+
PATTERN
|
42
|
+
|
29
43
|
def on_ivar(node)
|
44
|
+
return if inherit_form_builder?(node)
|
45
|
+
|
30
46
|
add_offense(node)
|
31
47
|
end
|
32
48
|
|
33
49
|
def on_ivasgn(node)
|
34
|
-
return if node.parent.or_asgn_type?
|
50
|
+
return if node.parent.or_asgn_type? || inherit_form_builder?(node)
|
35
51
|
|
36
52
|
add_offense(node.loc.name)
|
37
53
|
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def inherit_form_builder?(node)
|
58
|
+
node.each_ancestor(:class) do |class_node|
|
59
|
+
return true if form_builder_class?(class_node.parent_class)
|
60
|
+
end
|
61
|
+
|
62
|
+
false
|
63
|
+
end
|
38
64
|
end
|
39
65
|
end
|
40
66
|
end
|
@@ -18,6 +18,7 @@ module RuboCop
|
|
18
18
|
# get :new, params: { user_id: 1 }
|
19
19
|
# get :new, **options
|
20
20
|
class HttpPositionalArguments < Base
|
21
|
+
include RangeHelp
|
21
22
|
extend AutoCorrector
|
22
23
|
extend TargetRailsVersion
|
23
24
|
|
@@ -44,7 +45,7 @@ module RuboCop
|
|
44
45
|
|
45
46
|
message = format(MSG, verb: node.method_name)
|
46
47
|
|
47
|
-
add_offense(node
|
48
|
+
add_offense(highlight_range(node), message: message) do |corrector|
|
48
49
|
# given a pre Rails 5 method: get :new, {user_id: @user.id}, {}
|
49
50
|
#
|
50
51
|
# @return lambda of auto correct procedure
|
@@ -80,6 +81,12 @@ module RuboCop
|
|
80
81
|
node.sym_type? && node.value == :format
|
81
82
|
end
|
82
83
|
|
84
|
+
def highlight_range(node)
|
85
|
+
_http_path, *data = *node.arguments
|
86
|
+
|
87
|
+
range_between(data.first.source_range.begin_pos, data.last.source_range.end_pos)
|
88
|
+
end
|
89
|
+
|
83
90
|
def convert_hash_data(data, type)
|
84
91
|
return '' if data.hash_type? && data.empty?
|
85
92
|
|
@@ -11,12 +11,14 @@ module RuboCop
|
|
11
11
|
# render json: { foo: 'bar' }, status: 200
|
12
12
|
# render plain: 'foo/bar', status: 304
|
13
13
|
# redirect_to root_url, status: 301
|
14
|
+
# head 200
|
14
15
|
#
|
15
16
|
# # good
|
16
17
|
# render :foo, status: :ok
|
17
18
|
# render json: { foo: 'bar' }, status: :ok
|
18
19
|
# render plain: 'foo/bar', status: :not_modified
|
19
20
|
# redirect_to root_url, status: :moved_permanently
|
21
|
+
# head :ok
|
20
22
|
#
|
21
23
|
# @example EnforcedStyle: numeric
|
22
24
|
# # bad
|
@@ -24,23 +26,26 @@ module RuboCop
|
|
24
26
|
# render json: { foo: 'bar' }, status: :not_found
|
25
27
|
# render plain: 'foo/bar', status: :not_modified
|
26
28
|
# redirect_to root_url, status: :moved_permanently
|
29
|
+
# head :ok
|
27
30
|
#
|
28
31
|
# # good
|
29
32
|
# render :foo, status: 200
|
30
33
|
# render json: { foo: 'bar' }, status: 404
|
31
34
|
# render plain: 'foo/bar', status: 304
|
32
35
|
# redirect_to root_url, status: 301
|
36
|
+
# head 200
|
33
37
|
#
|
34
38
|
class HttpStatus < Base
|
35
39
|
include ConfigurableEnforcedStyle
|
36
40
|
extend AutoCorrector
|
37
41
|
|
38
|
-
RESTRICT_ON_SEND = %i[render redirect_to].freeze
|
42
|
+
RESTRICT_ON_SEND = %i[render redirect_to head].freeze
|
39
43
|
|
40
44
|
def_node_matcher :http_status, <<~PATTERN
|
41
45
|
{
|
42
46
|
(send nil? {:render :redirect_to} _ $hash)
|
43
47
|
(send nil? {:render :redirect_to} $hash)
|
48
|
+
(send nil? :head ${int sym} ...)
|
44
49
|
}
|
45
50
|
PATTERN
|
46
51
|
|
@@ -49,8 +54,12 @@ module RuboCop
|
|
49
54
|
PATTERN
|
50
55
|
|
51
56
|
def on_send(node)
|
52
|
-
http_status(node) do |
|
53
|
-
status =
|
57
|
+
http_status(node) do |hash_node_or_status_code|
|
58
|
+
status = if hash_node_or_status_code.hash_type?
|
59
|
+
status_code(hash_node_or_status_code)
|
60
|
+
else
|
61
|
+
hash_node_or_status_code
|
62
|
+
end
|
54
63
|
return unless status
|
55
64
|
|
56
65
|
checker = checker_class.new(status)
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks for the use of `I18n.locale=` method.
|
7
|
+
#
|
8
|
+
# The `locale` attribute persists for the rest of the Ruby runtime, potentially causing
|
9
|
+
# unexpected behavior at a later time.
|
10
|
+
# Using `I18n.with_locale` ensures the code passed in the block is the only place `I18n.locale` is affected.
|
11
|
+
# It eliminates the possibility of a `locale` sticking around longer than intended.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# # bad
|
15
|
+
# I18n.locale = :fr
|
16
|
+
#
|
17
|
+
# # good
|
18
|
+
# I18n.with_locale(:fr) do
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
class I18nLocaleAssignment < Base
|
22
|
+
MSG = 'Use `I18n.with_locale` with block instead of `I18n.locale=`.'
|
23
|
+
RESTRICT_ON_SEND = %i[locale=].freeze
|
24
|
+
|
25
|
+
def_node_matcher :i18n_locale_assignment?, <<~PATTERN
|
26
|
+
(send (const {nil? cbase} :I18n) :locale= ...)
|
27
|
+
PATTERN
|
28
|
+
|
29
|
+
def on_send(node)
|
30
|
+
return unless i18n_locale_assignment?(node)
|
31
|
+
|
32
|
+
add_offense(node)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -130,8 +130,7 @@ module RuboCop
|
|
130
130
|
# @see https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#module-ActiveRecord::Associations::ClassMethods-label-Setting+Inverses
|
131
131
|
class InverseOf < Base
|
132
132
|
SPECIFY_MSG = 'Specify an `:inverse_of` option.'
|
133
|
-
NIL_MSG = 'You specified `inverse_of: nil`, you probably meant to '
|
134
|
-
'use `inverse_of: false`.'
|
133
|
+
NIL_MSG = 'You specified `inverse_of: nil`, you probably meant to use `inverse_of: false`.'
|
135
134
|
RESTRICT_ON_SEND = %i[has_many has_one belongs_to].freeze
|
136
135
|
|
137
136
|
def_node_matcher :association_recv_arguments, <<~PATTERN
|
@@ -79,7 +79,11 @@ module RuboCop
|
|
79
79
|
opening_quote = offence_node.children.last.source[0]
|
80
80
|
closing_quote = opening_quote == ':' ? '' : opening_quote
|
81
81
|
new_rel_exp = ", rel: #{opening_quote}noopener#{closing_quote}"
|
82
|
-
range = send_node.
|
82
|
+
range = if (last_argument = send_node.last_argument).hash_type?
|
83
|
+
last_argument.pairs.last.source_range
|
84
|
+
else
|
85
|
+
last_argument.source_range
|
86
|
+
end
|
83
87
|
|
84
88
|
corrector.insert_after(range, new_rel_exp)
|
85
89
|
end
|
@@ -5,6 +5,8 @@ module RuboCop
|
|
5
5
|
module Rails
|
6
6
|
# This cop checks if the value of the option `class_name`, in
|
7
7
|
# the definition of a reflection is a string.
|
8
|
+
# It is marked as unsafe because it cannot be determined whether
|
9
|
+
# constant or method return value specified to `class_name` is a string.
|
8
10
|
#
|
9
11
|
# @example
|
10
12
|
# # bad
|
@@ -16,6 +18,7 @@ module RuboCop
|
|
16
18
|
class ReflectionClassName < Base
|
17
19
|
MSG = 'Use a string value for `class_name`.'
|
18
20
|
RESTRICT_ON_SEND = %i[has_many has_one belongs_to].freeze
|
21
|
+
ALLOWED_REFLECTION_CLASS_TYPES = %i[dstr str sym].freeze
|
19
22
|
|
20
23
|
def_node_matcher :association_with_reflection, <<~PATTERN
|
21
24
|
(send nil? {:has_many :has_one :belongs_to} _ _ ?
|
@@ -24,7 +27,7 @@ module RuboCop
|
|
24
27
|
PATTERN
|
25
28
|
|
26
29
|
def_node_matcher :reflection_class_name, <<~PATTERN
|
27
|
-
(pair (sym :class_name)
|
30
|
+
(pair (sym :class_name) #reflection_class_value?)
|
28
31
|
PATTERN
|
29
32
|
|
30
33
|
def on_send(node)
|
@@ -32,6 +35,16 @@ module RuboCop
|
|
32
35
|
add_offense(reflection_class_name.loc.expression)
|
33
36
|
end
|
34
37
|
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def reflection_class_value?(class_value)
|
42
|
+
if class_value.send_type?
|
43
|
+
!class_value.method?(:to_s) || class_value.receiver&.const_type?
|
44
|
+
else
|
45
|
+
!ALLOWED_REFLECTION_CLASS_TYPES.include?(class_value.type)
|
46
|
+
end
|
47
|
+
end
|
35
48
|
end
|
36
49
|
end
|
37
50
|
end
|
@@ -31,11 +31,12 @@ module RuboCop
|
|
31
31
|
include RangeHelp
|
32
32
|
extend AutoCorrector
|
33
33
|
|
34
|
-
MSG = 'Do not assign
|
34
|
+
MSG = 'Do not assign `%<method_name>s` to constants as it ' \
|
35
35
|
'will be evaluated only once.'
|
36
|
+
RELATIVE_DATE_METHODS = %i[since from_now after ago until before yesterday tomorrow].to_set.freeze
|
36
37
|
|
37
38
|
def on_casgn(node)
|
38
|
-
|
39
|
+
nested_relative_date(node) do |method_name|
|
39
40
|
add_offense(node, message: message(method_name)) do |corrector|
|
40
41
|
autocorrect(corrector, node)
|
41
42
|
end
|
@@ -50,7 +51,7 @@ module RuboCop
|
|
50
51
|
lhs.children.zip(rhs.children).each do |(name, value)|
|
51
52
|
next unless name.casgn_type?
|
52
53
|
|
53
|
-
|
54
|
+
nested_relative_date(value) do |method_name|
|
54
55
|
add_offense(offense_range(name, value), message: message(method_name)) do |corrector|
|
55
56
|
autocorrect(corrector, node)
|
56
57
|
end
|
@@ -59,7 +60,7 @@ module RuboCop
|
|
59
60
|
end
|
60
61
|
|
61
62
|
def on_or_asgn(node)
|
62
|
-
relative_date_or_assignment
|
63
|
+
relative_date_or_assignment(node) do |method_name|
|
63
64
|
add_offense(node, message: format(MSG, method_name: method_name))
|
64
65
|
end
|
65
66
|
end
|
@@ -88,24 +89,22 @@ module RuboCop
|
|
88
89
|
range_between(name.loc.expression.begin_pos, value.loc.expression.end_pos)
|
89
90
|
end
|
90
91
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
(
|
96
|
-
|
97
|
-
|
92
|
+
def nested_relative_date(node, &callback)
|
93
|
+
return if node.block_type?
|
94
|
+
|
95
|
+
node.each_child_node do |child|
|
96
|
+
nested_relative_date(child, &callback)
|
97
|
+
end
|
98
|
+
|
99
|
+
relative_date(node, &callback)
|
100
|
+
end
|
98
101
|
|
99
|
-
def_node_matcher :relative_date_or_assignment
|
100
|
-
(:or_asgn (casgn _ _) (send _ $
|
102
|
+
def_node_matcher :relative_date_or_assignment, <<~PATTERN
|
103
|
+
(:or_asgn (casgn _ _) (send _ $RELATIVE_DATE_METHODS))
|
101
104
|
PATTERN
|
102
105
|
|
103
|
-
def_node_matcher :relative_date
|
104
|
-
|
105
|
-
({erange irange} _ (send _ ${:since :from_now :after :ago :until :before}))
|
106
|
-
({erange irange} (send _ ${:since :from_now :after :ago :until :before}) _)
|
107
|
-
(send _ ${:since :from_now :after :ago :until :before})
|
108
|
-
}
|
106
|
+
def_node_matcher :relative_date, <<~PATTERN
|
107
|
+
(send _ $RELATIVE_DATE_METHODS)
|
109
108
|
PATTERN
|
110
109
|
end
|
111
110
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks for the usage of `require_dependency`.
|
7
|
+
#
|
8
|
+
# `require_dependency` is an obsolete method for Rails applications running in Zeitwerk mode.
|
9
|
+
# In Zeitwerk mode, the semantics should match Ruby's and no need to be defensive with load order,
|
10
|
+
# just refer to classes and modules normally.
|
11
|
+
# If the constant name is dynamic, camelize if needed, and constantize.
|
12
|
+
#
|
13
|
+
# Applications running in Zeitwerk mode should not use `require_dependency`.
|
14
|
+
#
|
15
|
+
# NOTE: This cop is disabled by default. Please enable it if you are using Zeitwerk mode.
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# # bad
|
19
|
+
# require_dependency 'some_lib'
|
20
|
+
class RequireDependency < Base
|
21
|
+
extend TargetRailsVersion
|
22
|
+
|
23
|
+
minimum_target_rails_version 6.0
|
24
|
+
|
25
|
+
MSG = 'Do not use `require_dependency` with Zeitwerk mode.'
|
26
|
+
RESTRICT_ON_SEND = %i[require_dependency].freeze
|
27
|
+
|
28
|
+
def_node_matcher :require_dependency_call?, <<~PATTERN
|
29
|
+
(send {nil? (const _ :Kernel)} :require_dependency _)
|
30
|
+
PATTERN
|
31
|
+
|
32
|
+
def on_send(node)
|
33
|
+
require_dependency_call?(node) { add_offense(node) }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -237,7 +237,7 @@ module RuboCop
|
|
237
237
|
|
238
238
|
def check_drop_table_node(node)
|
239
239
|
drop_table_call(node) do
|
240
|
-
unless node.parent.block_type?
|
240
|
+
unless node.parent.block_type? || node.last_argument.block_pass_type?
|
241
241
|
add_offense(
|
242
242
|
node,
|
243
243
|
message: format(MSG, action: 'drop_table(without block)')
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks whether the migration implements
|
7
|
+
# either a `change` method or both an `up` and a `down`
|
8
|
+
# method.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# # bad
|
12
|
+
# class SomeMigration < ActiveRecord::Migration[6.0]
|
13
|
+
# def up
|
14
|
+
# # up migration
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# # <----- missing down method
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# class SomeMigration < ActiveRecord::Migration[6.0]
|
21
|
+
# # <----- missing up method
|
22
|
+
#
|
23
|
+
# def down
|
24
|
+
# # down migration
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# # good
|
29
|
+
# class SomeMigration < ActiveRecord::Migration[6.0]
|
30
|
+
# def change
|
31
|
+
# # reversible migration
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# # good
|
36
|
+
# class SomeMigration < ActiveRecord::Migration[6.0]
|
37
|
+
# def up
|
38
|
+
# # up migration
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# def down
|
42
|
+
# # down migration
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
class ReversibleMigrationMethodDefinition < Base
|
46
|
+
MSG = 'Migrations must contain either a `change` method, or ' \
|
47
|
+
'both an `up` and a `down` method.'
|
48
|
+
|
49
|
+
def_node_matcher :migration_class?, <<~PATTERN
|
50
|
+
(class
|
51
|
+
(const nil? _)
|
52
|
+
(send
|
53
|
+
(const (const {nil? cbase} :ActiveRecord) :Migration)
|
54
|
+
:[]
|
55
|
+
(float _))
|
56
|
+
_)
|
57
|
+
PATTERN
|
58
|
+
|
59
|
+
def_node_matcher :change_method?, <<~PATTERN
|
60
|
+
[ #migration_class? `(def :change (args) _) ]
|
61
|
+
PATTERN
|
62
|
+
|
63
|
+
def_node_matcher :up_and_down_methods?, <<~PATTERN
|
64
|
+
[ #migration_class? `(def :up (args) _) `(def :down (args) _) ]
|
65
|
+
PATTERN
|
66
|
+
|
67
|
+
def on_class(node)
|
68
|
+
return if change_method?(node) || up_and_down_methods?(node)
|
69
|
+
|
70
|
+
add_offense(node)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|