rubocop-rails 2.0.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.
- checksums.yaml +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +73 -0
- data/bin/setup +7 -0
- data/config/default.yml +466 -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 +117 -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_job.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 +289 -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_uniqueness.rb +45 -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 +124 -0
- data/lib/rubocop/cop/rails/present.rb +153 -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 +93 -0
- data/lib/rubocop/cop/rails/request_referer.rb +56 -0
- data/lib/rubocop/cop/rails/reversible_migration.rb +286 -0
- data/lib/rubocop/cop/rails/safe_navigation.rb +87 -0
- data/lib/rubocop/cop/rails/save_bang.rb +316 -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 +238 -0
- data/lib/rubocop/cop/rails/uniq_before_pluck.rb +105 -0
- data/lib/rubocop/cop/rails/unknown_env.rb +63 -0
- data/lib/rubocop/cop/rails/validation.rb +109 -0
- data/lib/rubocop/cop/rails_cops.rb +64 -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 +143 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks for use of the helper methods which reference
|
7
|
+
# instance variables.
|
8
|
+
#
|
9
|
+
# Relying on instance variables makes it difficult to re-use helper
|
10
|
+
# methods.
|
11
|
+
#
|
12
|
+
# If it seems awkward to explicitly pass in each dependent
|
13
|
+
# variable, consider moving the behaviour elsewhere, for
|
14
|
+
# example to a model, decorator or presenter.
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# # bad
|
18
|
+
# def welcome_message
|
19
|
+
# "Hello #{@user.name}"
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# # good
|
23
|
+
# def welcome_message(user)
|
24
|
+
# "Hello #{user.name}"
|
25
|
+
# end
|
26
|
+
class HelperInstanceVariable < Cop
|
27
|
+
MSG = 'Do not use instance variables in helpers.'
|
28
|
+
|
29
|
+
def on_ivar(node)
|
30
|
+
add_offense(node)
|
31
|
+
end
|
32
|
+
|
33
|
+
def on_ivasgn(node)
|
34
|
+
add_offense(node, location: :name)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop is used to identify usages of http methods like `get`, `post`,
|
7
|
+
# `put`, `patch` without the usage of keyword arguments in your tests and
|
8
|
+
# change them to use keyword args. This cop only applies to Rails >= 5.
|
9
|
+
# If you are running Rails < 5 you should disable the
|
10
|
+
# Rails/HttpPositionalArguments cop or set your TargetRailsVersion in your
|
11
|
+
# .rubocop.yml file to 4.0, etc.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# # bad
|
15
|
+
# get :new, { user_id: 1}
|
16
|
+
#
|
17
|
+
# # good
|
18
|
+
# get :new, params: { user_id: 1 }
|
19
|
+
class HttpPositionalArguments < Cop
|
20
|
+
extend TargetRailsVersion
|
21
|
+
|
22
|
+
MSG = 'Use keyword arguments instead of ' \
|
23
|
+
'positional arguments for http call: `%<verb>s`.'
|
24
|
+
KEYWORD_ARGS = %i[
|
25
|
+
method params session body flash xhr as headers env
|
26
|
+
].freeze
|
27
|
+
HTTP_METHODS = %i[get post put patch delete head].freeze
|
28
|
+
|
29
|
+
minimum_target_rails_version 5.0
|
30
|
+
|
31
|
+
def_node_matcher :http_request?, <<-PATTERN
|
32
|
+
(send nil? {#{HTTP_METHODS.map(&:inspect).join(' ')}} !nil? $_ ...)
|
33
|
+
PATTERN
|
34
|
+
|
35
|
+
def on_send(node)
|
36
|
+
http_request?(node) do |data|
|
37
|
+
return unless needs_conversion?(data)
|
38
|
+
|
39
|
+
add_offense(node, location: :selector,
|
40
|
+
message: format(MSG, verb: node.method_name))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# given a pre Rails 5 method: get :new, {user_id: @user.id}, {}
|
45
|
+
#
|
46
|
+
# @return lambda of auto correct procedure
|
47
|
+
# the result should look like:
|
48
|
+
# get :new, params: { user_id: @user.id }, session: {}
|
49
|
+
# the http_method is the method used to call the controller
|
50
|
+
# the controller node can be a symbol, method, object or string
|
51
|
+
# that represents the path/action on the Rails controller
|
52
|
+
# the data is the http parameters and environment sent in
|
53
|
+
# the Rails 5 http call
|
54
|
+
def autocorrect(node)
|
55
|
+
lambda do |corrector|
|
56
|
+
corrector.replace(node.loc.expression, correction(node))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def needs_conversion?(data)
|
63
|
+
return true unless data.hash_type?
|
64
|
+
|
65
|
+
data.each_pair.none? do |pair|
|
66
|
+
special_keyword_arg?(pair.key) ||
|
67
|
+
format_arg?(pair.key) && data.pairs.one?
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def special_keyword_arg?(node)
|
72
|
+
node.sym_type? && KEYWORD_ARGS.include?(node.value)
|
73
|
+
end
|
74
|
+
|
75
|
+
def format_arg?(node)
|
76
|
+
node.sym_type? && node.value == :format
|
77
|
+
end
|
78
|
+
|
79
|
+
def convert_hash_data(data, type)
|
80
|
+
return '' if data.hash_type? && data.empty?
|
81
|
+
|
82
|
+
hash_data = if data.hash_type?
|
83
|
+
format('{ %<data>s }',
|
84
|
+
data: data.pairs.map(&:source).join(', '))
|
85
|
+
else
|
86
|
+
# user supplies an object,
|
87
|
+
# no need to surround with braces
|
88
|
+
data.source
|
89
|
+
end
|
90
|
+
|
91
|
+
format(', %<type>s: %<hash_data>s', type: type, hash_data: hash_data)
|
92
|
+
end
|
93
|
+
|
94
|
+
def correction(node)
|
95
|
+
http_path, *data = *node.arguments
|
96
|
+
|
97
|
+
controller_action = http_path.source
|
98
|
+
params = convert_hash_data(data.first, 'params')
|
99
|
+
session = convert_hash_data(data.last, 'session') if data.size > 1
|
100
|
+
|
101
|
+
format(correction_template(node), name: node.method_name,
|
102
|
+
action: controller_action,
|
103
|
+
params: params,
|
104
|
+
session: session)
|
105
|
+
end
|
106
|
+
|
107
|
+
def correction_template(node)
|
108
|
+
if parentheses?(node)
|
109
|
+
'%<name>s(%<action>s%<params>s%<session>s)'
|
110
|
+
else
|
111
|
+
'%<name>s %<action>s%<params>s%<session>s'
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# Enforces use of symbolic or numeric value to define HTTP status.
|
7
|
+
#
|
8
|
+
# @example EnforcedStyle: symbolic (default)
|
9
|
+
# # bad
|
10
|
+
# render :foo, status: 200
|
11
|
+
# render json: { foo: 'bar' }, status: 200
|
12
|
+
# render plain: 'foo/bar', status: 304
|
13
|
+
# redirect_to root_url, status: 301
|
14
|
+
#
|
15
|
+
# # good
|
16
|
+
# render :foo, status: :ok
|
17
|
+
# render json: { foo: 'bar' }, status: :ok
|
18
|
+
# render plain: 'foo/bar', status: :not_modified
|
19
|
+
# redirect_to root_url, status: :moved_permanently
|
20
|
+
#
|
21
|
+
# @example EnforcedStyle: numeric
|
22
|
+
# # bad
|
23
|
+
# render :foo, status: :ok
|
24
|
+
# render json: { foo: 'bar' }, status: :not_found
|
25
|
+
# render plain: 'foo/bar', status: :not_modified
|
26
|
+
# redirect_to root_url, status: :moved_permanently
|
27
|
+
#
|
28
|
+
# # good
|
29
|
+
# render :foo, status: 200
|
30
|
+
# render json: { foo: 'bar' }, status: 404
|
31
|
+
# render plain: 'foo/bar', status: 304
|
32
|
+
# redirect_to root_url, status: 301
|
33
|
+
#
|
34
|
+
class HttpStatus < Cop
|
35
|
+
include ConfigurableEnforcedStyle
|
36
|
+
|
37
|
+
def_node_matcher :http_status, <<-PATTERN
|
38
|
+
{
|
39
|
+
(send nil? {:render :redirect_to} _ $hash)
|
40
|
+
(send nil? {:render :redirect_to} $hash)
|
41
|
+
}
|
42
|
+
PATTERN
|
43
|
+
|
44
|
+
def_node_matcher :status_code, <<-PATTERN
|
45
|
+
(hash <(pair (sym :status) ${int sym}) ...>)
|
46
|
+
PATTERN
|
47
|
+
|
48
|
+
def on_send(node)
|
49
|
+
http_status(node) do |hash_node|
|
50
|
+
status = status_code(hash_node)
|
51
|
+
return unless status
|
52
|
+
|
53
|
+
checker = checker_class.new(status)
|
54
|
+
return unless checker.offensive?
|
55
|
+
|
56
|
+
add_offense(checker.node, message: checker.message)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def autocorrect(node)
|
61
|
+
lambda do |corrector|
|
62
|
+
checker = checker_class.new(node)
|
63
|
+
corrector.replace(node.loc.expression, checker.preferred_style)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def checker_class
|
70
|
+
case style
|
71
|
+
when :symbolic
|
72
|
+
SymbolicStyleChecker
|
73
|
+
when :numeric
|
74
|
+
NumericStyleChecker
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# :nodoc:
|
79
|
+
class SymbolicStyleChecker
|
80
|
+
MSG = 'Prefer `%<prefer>s` over `%<current>s` ' \
|
81
|
+
'to define HTTP status code.'
|
82
|
+
DEFAULT_MSG = 'Prefer `symbolic` over `numeric` ' \
|
83
|
+
'to define HTTP status code.'
|
84
|
+
|
85
|
+
attr_reader :node
|
86
|
+
def initialize(node)
|
87
|
+
@node = node
|
88
|
+
end
|
89
|
+
|
90
|
+
def offensive?
|
91
|
+
!node.sym_type? && !custom_http_status_code?
|
92
|
+
end
|
93
|
+
|
94
|
+
def message
|
95
|
+
format(MSG, prefer: preferred_style, current: number.to_s)
|
96
|
+
end
|
97
|
+
|
98
|
+
def preferred_style
|
99
|
+
symbol.inspect
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def symbol
|
105
|
+
::Rack::Utils::SYMBOL_TO_STATUS_CODE.key(number)
|
106
|
+
end
|
107
|
+
|
108
|
+
def number
|
109
|
+
node.children.first
|
110
|
+
end
|
111
|
+
|
112
|
+
def custom_http_status_code?
|
113
|
+
node.int_type? &&
|
114
|
+
!::Rack::Utils::SYMBOL_TO_STATUS_CODE.value?(number)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# :nodoc:
|
119
|
+
class NumericStyleChecker
|
120
|
+
MSG = 'Prefer `%<prefer>s` over `%<current>s` ' \
|
121
|
+
'to define HTTP status code.'
|
122
|
+
DEFAULT_MSG = 'Prefer `numeric` over `symbolic` ' \
|
123
|
+
'to define HTTP status code.'
|
124
|
+
PERMITTED_STATUS = %i[error success missing redirect].freeze
|
125
|
+
|
126
|
+
attr_reader :node
|
127
|
+
def initialize(node)
|
128
|
+
@node = node
|
129
|
+
end
|
130
|
+
|
131
|
+
def offensive?
|
132
|
+
!node.int_type? && !permitted_symbol?
|
133
|
+
end
|
134
|
+
|
135
|
+
def message
|
136
|
+
format(MSG, prefer: preferred_style, current: symbol.inspect)
|
137
|
+
end
|
138
|
+
|
139
|
+
def preferred_style
|
140
|
+
number.to_s
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def number
|
146
|
+
::Rack::Utils::SYMBOL_TO_STATUS_CODE[symbol]
|
147
|
+
end
|
148
|
+
|
149
|
+
def symbol
|
150
|
+
node.value
|
151
|
+
end
|
152
|
+
|
153
|
+
def permitted_symbol?
|
154
|
+
node.sym_type? && PERMITTED_STATUS.include?(node.value)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks that `if` and `only` (or `except`) are not used together
|
7
|
+
# as options of `skip_*` action filter.
|
8
|
+
#
|
9
|
+
# The `if` option will be ignored when `if` and `only` are used together.
|
10
|
+
# Similarly, the `except` option will be ignored when `if` and `except`
|
11
|
+
# are used together.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# # bad
|
15
|
+
# class MyPageController < ApplicationController
|
16
|
+
# skip_before_action :login_required,
|
17
|
+
# only: :show, if: :trusted_origin?
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# # good
|
21
|
+
# class MyPageController < ApplicationController
|
22
|
+
# skip_before_action :login_required,
|
23
|
+
# if: -> { trusted_origin? && action_name == "show" }
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# @example
|
27
|
+
# # bad
|
28
|
+
# class MyPageController < ApplicationController
|
29
|
+
# skip_before_action :login_required,
|
30
|
+
# except: :admin, if: :trusted_origin?
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# # good
|
34
|
+
# class MyPageController < ApplicationController
|
35
|
+
# skip_before_action :login_required,
|
36
|
+
# if: -> { trusted_origin? && action_name != "admin" }
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# @see https://api.rubyonrails.org/classes/AbstractController/Callbacks/ClassMethods.html#method-i-_normalize_callback_options
|
40
|
+
class IgnoredSkipActionFilterOption < Cop
|
41
|
+
MSG = <<~MSG.chomp.freeze
|
42
|
+
`%<ignore>s` option will be ignored when `%<prefer>s` and `%<ignore>s` are used together.
|
43
|
+
MSG
|
44
|
+
|
45
|
+
FILTERS = %w[
|
46
|
+
:skip_after_action
|
47
|
+
:skip_around_action
|
48
|
+
:skip_before_action
|
49
|
+
:skip_action_callback
|
50
|
+
].freeze
|
51
|
+
|
52
|
+
def_node_matcher :filter_options, <<-PATTERN
|
53
|
+
(send
|
54
|
+
nil?
|
55
|
+
{#{FILTERS.join(' ')}}
|
56
|
+
_
|
57
|
+
$_)
|
58
|
+
PATTERN
|
59
|
+
|
60
|
+
def on_send(node)
|
61
|
+
options = filter_options(node)
|
62
|
+
return unless options
|
63
|
+
return unless options.hash_type?
|
64
|
+
|
65
|
+
options = options_hash(options)
|
66
|
+
|
67
|
+
if if_and_only?(options)
|
68
|
+
add_offense(options[:if],
|
69
|
+
message: format(MSG, prefer: :only, ignore: :if))
|
70
|
+
elsif if_and_except?(options)
|
71
|
+
add_offense(options[:except],
|
72
|
+
message: format(MSG, prefer: :if, ignore: :except))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def options_hash(options)
|
79
|
+
options.pairs
|
80
|
+
.select { |pair| pair.key.sym_type? }
|
81
|
+
.map { |pair| [pair.key.value, pair] }.to_h
|
82
|
+
end
|
83
|
+
|
84
|
+
def if_and_only?(options)
|
85
|
+
options.key?(:if) && options.key?(:only)
|
86
|
+
end
|
87
|
+
|
88
|
+
def if_and_except?(options)
|
89
|
+
options.key?(:if) && options.key?(:except)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,246 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop looks for has_(one|many) and belongs_to associations where
|
7
|
+
# Active Record can't automatically determine the inverse association
|
8
|
+
# because of a scope or the options used. Using the blog with order scope
|
9
|
+
# example below, traversing the a Blog's association in both directions
|
10
|
+
# with `blog.posts.first.blog` would cause the `blog` to be loaded from
|
11
|
+
# the database twice.
|
12
|
+
#
|
13
|
+
# `:inverse_of` must be manually specified for Active Record to use the
|
14
|
+
# associated object in memory, or set to `false` to opt-out. Note that
|
15
|
+
# setting `nil` does not stop Active Record from trying to determine the
|
16
|
+
# inverse automatically, and is not considered a valid value for this.
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
# # good
|
20
|
+
# class Blog < ApplicationRecord
|
21
|
+
# has_many :posts
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# class Post < ApplicationRecord
|
25
|
+
# belongs_to :blog
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# @example
|
29
|
+
# # bad
|
30
|
+
# class Blog < ApplicationRecord
|
31
|
+
# has_many :posts, -> { order(published_at: :desc) }
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# class Post < ApplicationRecord
|
35
|
+
# belongs_to :blog
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# # good
|
39
|
+
# class Blog < ApplicationRecord
|
40
|
+
# has_many(:posts,
|
41
|
+
# -> { order(published_at: :desc) },
|
42
|
+
# inverse_of: :blog)
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# class Post < ApplicationRecord
|
46
|
+
# belongs_to :blog
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# # good
|
50
|
+
# class Blog < ApplicationRecord
|
51
|
+
# with_options inverse_of: :blog do
|
52
|
+
# has_many :posts, -> { order(published_at: :desc) }
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# class Post < ApplicationRecord
|
57
|
+
# belongs_to :blog
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# # good
|
61
|
+
# # When you don't want to use the inverse association.
|
62
|
+
# class Blog < ApplicationRecord
|
63
|
+
# has_many(:posts,
|
64
|
+
# -> { order(published_at: :desc) },
|
65
|
+
# inverse_of: false)
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# @example
|
69
|
+
# # bad
|
70
|
+
# class Picture < ApplicationRecord
|
71
|
+
# belongs_to :imageable, polymorphic: true
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
# class Employee < ApplicationRecord
|
75
|
+
# has_many :pictures, as: :imageable
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
# class Product < ApplicationRecord
|
79
|
+
# has_many :pictures, as: :imageable
|
80
|
+
# end
|
81
|
+
#
|
82
|
+
# # good
|
83
|
+
# class Picture < ApplicationRecord
|
84
|
+
# belongs_to :imageable, polymorphic: true
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
# class Employee < ApplicationRecord
|
88
|
+
# has_many :pictures, as: :imageable, inverse_of: :imageable
|
89
|
+
# end
|
90
|
+
#
|
91
|
+
# class Product < ApplicationRecord
|
92
|
+
# has_many :pictures, as: :imageable, inverse_of: :imageable
|
93
|
+
# end
|
94
|
+
#
|
95
|
+
# @example
|
96
|
+
# # bad
|
97
|
+
# # However, RuboCop can not detect this pattern...
|
98
|
+
# class Physician < ApplicationRecord
|
99
|
+
# has_many :appointments
|
100
|
+
# has_many :patients, through: :appointments
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# class Appointment < ApplicationRecord
|
104
|
+
# belongs_to :physician
|
105
|
+
# belongs_to :patient
|
106
|
+
# end
|
107
|
+
#
|
108
|
+
# class Patient < ApplicationRecord
|
109
|
+
# has_many :appointments
|
110
|
+
# has_many :physicians, through: :appointments
|
111
|
+
# end
|
112
|
+
#
|
113
|
+
# # good
|
114
|
+
# class Physician < ApplicationRecord
|
115
|
+
# has_many :appointments
|
116
|
+
# has_many :patients, through: :appointments
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# class Appointment < ApplicationRecord
|
120
|
+
# belongs_to :physician, inverse_of: :appointments
|
121
|
+
# belongs_to :patient, inverse_of: :appointments
|
122
|
+
# end
|
123
|
+
#
|
124
|
+
# class Patient < ApplicationRecord
|
125
|
+
# has_many :appointments
|
126
|
+
# has_many :physicians, through: :appointments
|
127
|
+
# end
|
128
|
+
#
|
129
|
+
# @see https://guides.rubyonrails.org/association_basics.html#bi-directional-associations
|
130
|
+
# @see https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#module-ActiveRecord::Associations::ClassMethods-label-Setting+Inverses
|
131
|
+
class InverseOf < Cop
|
132
|
+
extend TargetRailsVersion
|
133
|
+
|
134
|
+
minimum_target_rails_version 4.1
|
135
|
+
|
136
|
+
SPECIFY_MSG = 'Specify an `:inverse_of` option.'
|
137
|
+
NIL_MSG = 'You specified `inverse_of: nil`, you probably meant to ' \
|
138
|
+
'use `inverse_of: false`.'
|
139
|
+
|
140
|
+
def_node_matcher :association_recv_arguments, <<-PATTERN
|
141
|
+
(send $_ {:has_many :has_one :belongs_to} _ $...)
|
142
|
+
PATTERN
|
143
|
+
|
144
|
+
def_node_matcher :options_from_argument, <<-PATTERN
|
145
|
+
(hash $...)
|
146
|
+
PATTERN
|
147
|
+
|
148
|
+
def_node_matcher :conditions_option?, <<-PATTERN
|
149
|
+
(pair (sym :conditions) !nil)
|
150
|
+
PATTERN
|
151
|
+
|
152
|
+
def_node_matcher :through_option?, <<-PATTERN
|
153
|
+
(pair (sym :through) !nil)
|
154
|
+
PATTERN
|
155
|
+
|
156
|
+
def_node_matcher :polymorphic_option?, <<-PATTERN
|
157
|
+
(pair (sym :polymorphic) !nil)
|
158
|
+
PATTERN
|
159
|
+
|
160
|
+
def_node_matcher :as_option?, <<-PATTERN
|
161
|
+
(pair (sym :as) !nil)
|
162
|
+
PATTERN
|
163
|
+
|
164
|
+
def_node_matcher :foreign_key_option?, <<-PATTERN
|
165
|
+
(pair (sym :foreign_key) !nil)
|
166
|
+
PATTERN
|
167
|
+
|
168
|
+
def_node_matcher :inverse_of_option?, <<-PATTERN
|
169
|
+
(pair (sym :inverse_of) !nil)
|
170
|
+
PATTERN
|
171
|
+
|
172
|
+
def_node_matcher :inverse_of_nil_option?, <<-PATTERN
|
173
|
+
(pair (sym :inverse_of) nil)
|
174
|
+
PATTERN
|
175
|
+
|
176
|
+
def on_send(node)
|
177
|
+
recv, arguments = association_recv_arguments(node)
|
178
|
+
return unless arguments
|
179
|
+
|
180
|
+
with_options = with_options_arguments(recv, node)
|
181
|
+
|
182
|
+
options = arguments.concat(with_options).flat_map do |arg|
|
183
|
+
options_from_argument(arg)
|
184
|
+
end
|
185
|
+
return if options_ignoring_inverse_of?(options)
|
186
|
+
|
187
|
+
return unless scope?(arguments) ||
|
188
|
+
options_requiring_inverse_of?(options)
|
189
|
+
|
190
|
+
return if options_contain_inverse_of?(options)
|
191
|
+
|
192
|
+
add_offense(node, message: message(options), location: :selector)
|
193
|
+
end
|
194
|
+
|
195
|
+
def scope?(arguments)
|
196
|
+
arguments.any?(&:block_type?)
|
197
|
+
end
|
198
|
+
|
199
|
+
def options_requiring_inverse_of?(options)
|
200
|
+
required = options.any? do |opt|
|
201
|
+
conditions_option?(opt) ||
|
202
|
+
foreign_key_option?(opt)
|
203
|
+
end
|
204
|
+
|
205
|
+
return required if target_rails_version >= 5.2
|
206
|
+
|
207
|
+
required || options.any? { |opt| as_option?(opt) }
|
208
|
+
end
|
209
|
+
|
210
|
+
def options_ignoring_inverse_of?(options)
|
211
|
+
options.any? do |opt|
|
212
|
+
through_option?(opt) || polymorphic_option?(opt)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def options_contain_inverse_of?(options)
|
217
|
+
options.any? { |opt| inverse_of_option?(opt) }
|
218
|
+
end
|
219
|
+
|
220
|
+
def with_options_arguments(recv, node)
|
221
|
+
blocks = node.each_ancestor(:block).select do |block|
|
222
|
+
block.send_node.command?(:with_options) &&
|
223
|
+
same_context_in_with_options?(block.arguments.first, recv)
|
224
|
+
end
|
225
|
+
blocks.flat_map { |n| n.send_node.arguments }
|
226
|
+
end
|
227
|
+
|
228
|
+
def same_context_in_with_options?(arg, recv)
|
229
|
+
return true if arg.nil? && recv.nil?
|
230
|
+
|
231
|
+
arg && recv && arg.children[0] == recv.children[0]
|
232
|
+
end
|
233
|
+
|
234
|
+
private
|
235
|
+
|
236
|
+
def message(options)
|
237
|
+
if options.any? { |opt| inverse_of_nil_option?(opt) }
|
238
|
+
NIL_MSG
|
239
|
+
else
|
240
|
+
SPECIFY_MSG
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|