rubocop-rails 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +20 -0
  3. data/README.md +73 -0
  4. data/bin/setup +7 -0
  5. data/config/default.yml +466 -0
  6. data/lib/rubocop-rails.rb +12 -0
  7. data/lib/rubocop/cop/mixin/target_rails_version.rb +16 -0
  8. data/lib/rubocop/cop/rails/action_filter.rb +117 -0
  9. data/lib/rubocop/cop/rails/active_record_aliases.rb +48 -0
  10. data/lib/rubocop/cop/rails/active_record_override.rb +82 -0
  11. data/lib/rubocop/cop/rails/active_support_aliases.rb +69 -0
  12. data/lib/rubocop/cop/rails/application_job.rb +40 -0
  13. data/lib/rubocop/cop/rails/application_record.rb +40 -0
  14. data/lib/rubocop/cop/rails/assert_not.rb +44 -0
  15. data/lib/rubocop/cop/rails/belongs_to.rb +102 -0
  16. data/lib/rubocop/cop/rails/blank.rb +164 -0
  17. data/lib/rubocop/cop/rails/bulk_change_table.rb +289 -0
  18. data/lib/rubocop/cop/rails/create_table_with_timestamps.rb +91 -0
  19. data/lib/rubocop/cop/rails/date.rb +161 -0
  20. data/lib/rubocop/cop/rails/delegate.rb +132 -0
  21. data/lib/rubocop/cop/rails/delegate_allow_blank.rb +37 -0
  22. data/lib/rubocop/cop/rails/dynamic_find_by.rb +91 -0
  23. data/lib/rubocop/cop/rails/enum_uniqueness.rb +45 -0
  24. data/lib/rubocop/cop/rails/environment_comparison.rb +68 -0
  25. data/lib/rubocop/cop/rails/exit.rb +67 -0
  26. data/lib/rubocop/cop/rails/file_path.rb +108 -0
  27. data/lib/rubocop/cop/rails/find_by.rb +55 -0
  28. data/lib/rubocop/cop/rails/find_each.rb +51 -0
  29. data/lib/rubocop/cop/rails/has_and_belongs_to_many.rb +25 -0
  30. data/lib/rubocop/cop/rails/has_many_or_has_one_dependent.rb +106 -0
  31. data/lib/rubocop/cop/rails/helper_instance_variable.rb +39 -0
  32. data/lib/rubocop/cop/rails/http_positional_arguments.rb +117 -0
  33. data/lib/rubocop/cop/rails/http_status.rb +160 -0
  34. data/lib/rubocop/cop/rails/ignored_skip_action_filter_option.rb +94 -0
  35. data/lib/rubocop/cop/rails/inverse_of.rb +246 -0
  36. data/lib/rubocop/cop/rails/lexically_scoped_action_filter.rb +175 -0
  37. data/lib/rubocop/cop/rails/link_to_blank.rb +98 -0
  38. data/lib/rubocop/cop/rails/not_null_column.rb +67 -0
  39. data/lib/rubocop/cop/rails/output.rb +49 -0
  40. data/lib/rubocop/cop/rails/output_safety.rb +99 -0
  41. data/lib/rubocop/cop/rails/pluralization_grammar.rb +107 -0
  42. data/lib/rubocop/cop/rails/presence.rb +124 -0
  43. data/lib/rubocop/cop/rails/present.rb +153 -0
  44. data/lib/rubocop/cop/rails/read_write_attribute.rb +74 -0
  45. data/lib/rubocop/cop/rails/redundant_allow_nil.rb +111 -0
  46. data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +136 -0
  47. data/lib/rubocop/cop/rails/reflection_class_name.rb +37 -0
  48. data/lib/rubocop/cop/rails/refute_methods.rb +76 -0
  49. data/lib/rubocop/cop/rails/relative_date_constant.rb +93 -0
  50. data/lib/rubocop/cop/rails/request_referer.rb +56 -0
  51. data/lib/rubocop/cop/rails/reversible_migration.rb +286 -0
  52. data/lib/rubocop/cop/rails/safe_navigation.rb +87 -0
  53. data/lib/rubocop/cop/rails/save_bang.rb +316 -0
  54. data/lib/rubocop/cop/rails/scope_args.rb +29 -0
  55. data/lib/rubocop/cop/rails/skips_model_validations.rb +87 -0
  56. data/lib/rubocop/cop/rails/time_zone.rb +238 -0
  57. data/lib/rubocop/cop/rails/uniq_before_pluck.rb +105 -0
  58. data/lib/rubocop/cop/rails/unknown_env.rb +63 -0
  59. data/lib/rubocop/cop/rails/validation.rb +109 -0
  60. data/lib/rubocop/cop/rails_cops.rb +64 -0
  61. data/lib/rubocop/rails.rb +12 -0
  62. data/lib/rubocop/rails/inject.rb +18 -0
  63. data/lib/rubocop/rails/version.rb +10 -0
  64. 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