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