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,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks for calls to `link_to` that contain a
|
7
|
+
# `target: '_blank'` but no `rel: 'noopener'`. This can be a security
|
8
|
+
# risk as the loaded page will have control over the previous page
|
9
|
+
# and could change its location for phishing purposes.
|
10
|
+
#
|
11
|
+
# The option `rel: 'noreferrer'` also blocks this behavior
|
12
|
+
# and removes the http-referrer header.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# # bad
|
16
|
+
# link_to 'Click here', url, target: '_blank'
|
17
|
+
#
|
18
|
+
# # good
|
19
|
+
# link_to 'Click here', url, target: '_blank', rel: 'noopener'
|
20
|
+
#
|
21
|
+
# # good
|
22
|
+
# link_to 'Click here', url, target: '_blank', rel: 'noreferrer'
|
23
|
+
class LinkToBlank < Cop
|
24
|
+
MSG = 'Specify a `:rel` option containing noopener.'
|
25
|
+
|
26
|
+
def_node_matcher :blank_target?, <<~PATTERN
|
27
|
+
(pair {(sym :target) (str "target")} {(str "_blank") (sym :_blank)})
|
28
|
+
PATTERN
|
29
|
+
|
30
|
+
def_node_matcher :includes_noopener?, <<~PATTERN
|
31
|
+
(pair {(sym :rel) (str "rel")} ({str sym} #contains_noopener?))
|
32
|
+
PATTERN
|
33
|
+
|
34
|
+
def_node_matcher :rel_node?, <<~PATTERN
|
35
|
+
(pair {(sym :rel) (str "rel")} (str _))
|
36
|
+
PATTERN
|
37
|
+
|
38
|
+
def on_send(node)
|
39
|
+
return unless node.method?(:link_to)
|
40
|
+
|
41
|
+
option_nodes = node.each_child_node(:hash)
|
42
|
+
|
43
|
+
option_nodes.map(&:children).each do |options|
|
44
|
+
blank = options.find { |o| blank_target?(o) }
|
45
|
+
if blank && options.none? { |o| includes_noopener?(o) }
|
46
|
+
add_offense(blank)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def autocorrect(node)
|
52
|
+
lambda do |corrector|
|
53
|
+
send_node = node.parent.parent
|
54
|
+
|
55
|
+
option_nodes = send_node.each_child_node(:hash)
|
56
|
+
rel_node = nil
|
57
|
+
option_nodes.map(&:children).each do |options|
|
58
|
+
rel_node ||= options.find { |o| rel_node?(o) }
|
59
|
+
end
|
60
|
+
|
61
|
+
if rel_node
|
62
|
+
append_to_rel(rel_node, corrector)
|
63
|
+
else
|
64
|
+
add_rel(send_node, node, corrector)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def append_to_rel(rel_node, corrector)
|
72
|
+
existing_rel = rel_node.children.last.value
|
73
|
+
str_range = rel_node.children.last.loc.expression.adjust(
|
74
|
+
begin_pos: 1,
|
75
|
+
end_pos: -1
|
76
|
+
)
|
77
|
+
corrector.replace(str_range, "#{existing_rel} noopener")
|
78
|
+
end
|
79
|
+
|
80
|
+
def add_rel(send_node, offence_node, corrector)
|
81
|
+
opening_quote = offence_node.children.last.source[0]
|
82
|
+
closing_quote = opening_quote == ':' ? '' : opening_quote
|
83
|
+
new_rel_exp = ", rel: #{opening_quote}noopener#{closing_quote}"
|
84
|
+
range = send_node.arguments.last.source_range
|
85
|
+
|
86
|
+
corrector.insert_after(range, new_rel_exp)
|
87
|
+
end
|
88
|
+
|
89
|
+
def contains_noopener?(value)
|
90
|
+
return false unless value
|
91
|
+
|
92
|
+
rel_array = value.to_s.split(' ')
|
93
|
+
rel_array.include?('noopener') || rel_array.include?('noreferrer')
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks for add_column call with NOT NULL constraint
|
7
|
+
# in migration file.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# # bad
|
11
|
+
# add_column :users, :name, :string, null: false
|
12
|
+
# add_reference :products, :category, null: false
|
13
|
+
#
|
14
|
+
# # good
|
15
|
+
# add_column :users, :name, :string, null: true
|
16
|
+
# add_column :users, :name, :string, null: false, default: ''
|
17
|
+
# add_reference :products, :category
|
18
|
+
# add_reference :products, :category, null: false, default: 1
|
19
|
+
class NotNullColumn < Cop
|
20
|
+
MSG = 'Do not add a NOT NULL column without a default value.'
|
21
|
+
|
22
|
+
def_node_matcher :add_not_null_column?, <<~PATTERN
|
23
|
+
(send nil? :add_column _ _ _ (hash $...))
|
24
|
+
PATTERN
|
25
|
+
|
26
|
+
def_node_matcher :add_not_null_reference?, <<~PATTERN
|
27
|
+
(send nil? :add_reference _ _ (hash $...))
|
28
|
+
PATTERN
|
29
|
+
|
30
|
+
def_node_matcher :null_false?, <<~PATTERN
|
31
|
+
(pair (sym :null) (false))
|
32
|
+
PATTERN
|
33
|
+
|
34
|
+
def_node_matcher :default_option?, <<~PATTERN
|
35
|
+
(pair (sym :default) !nil)
|
36
|
+
PATTERN
|
37
|
+
|
38
|
+
def on_send(node)
|
39
|
+
check_add_column(node)
|
40
|
+
check_add_reference(node)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def check_add_column(node)
|
46
|
+
pairs = add_not_null_column?(node)
|
47
|
+
check_pairs(pairs)
|
48
|
+
end
|
49
|
+
|
50
|
+
def check_add_reference(node)
|
51
|
+
pairs = add_not_null_reference?(node)
|
52
|
+
check_pairs(pairs)
|
53
|
+
end
|
54
|
+
|
55
|
+
def check_pairs(pairs)
|
56
|
+
return unless pairs
|
57
|
+
return if pairs.any? { |pair| default_option?(pair) }
|
58
|
+
|
59
|
+
null_false = pairs.find { |pair| null_false?(pair) }
|
60
|
+
return unless null_false
|
61
|
+
|
62
|
+
add_offense(null_false)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks for the use of output calls like puts and print
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# # bad
|
10
|
+
# puts 'A debug message'
|
11
|
+
# pp 'A debug message'
|
12
|
+
# print 'A debug message'
|
13
|
+
#
|
14
|
+
# # good
|
15
|
+
# Rails.logger.debug 'A debug message'
|
16
|
+
class Output < Cop
|
17
|
+
MSG = 'Do not write to stdout. ' \
|
18
|
+
"Use Rails's logger if you want to log."
|
19
|
+
|
20
|
+
def_node_matcher :output?, <<~PATTERN
|
21
|
+
(send nil? {:ap :p :pp :pretty_print :print :puts} ...)
|
22
|
+
PATTERN
|
23
|
+
|
24
|
+
def_node_matcher :io_output?, <<~PATTERN
|
25
|
+
(send
|
26
|
+
{
|
27
|
+
(gvar #match_gvar?)
|
28
|
+
{(const nil? :STDOUT) (const nil? :STDERR)}
|
29
|
+
}
|
30
|
+
{:binwrite :syswrite :write :write_nonblock}
|
31
|
+
...)
|
32
|
+
PATTERN
|
33
|
+
|
34
|
+
def on_send(node)
|
35
|
+
return unless (output?(node) || io_output?(node)) &&
|
36
|
+
node.arguments?
|
37
|
+
|
38
|
+
add_offense(node, location: :selector)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def match_gvar?(sym)
|
44
|
+
%i[$stdout $stderr].include?(sym)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks for the use of output safety calls like `html_safe`,
|
7
|
+
# `raw`, and `safe_concat`. These methods do not escape content. They
|
8
|
+
# simply return a SafeBuffer containing the content as is. Instead,
|
9
|
+
# use `safe_join` to join content and escape it and concat to
|
10
|
+
# concatenate content and escape it, ensuring its safety.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# user_content = "<b>hi</b>"
|
14
|
+
#
|
15
|
+
# # bad
|
16
|
+
# "<p>#{user_content}</p>".html_safe
|
17
|
+
# # => ActiveSupport::SafeBuffer "<p><b>hi</b></p>"
|
18
|
+
#
|
19
|
+
# # good
|
20
|
+
# content_tag(:p, user_content)
|
21
|
+
# # => ActiveSupport::SafeBuffer "<p><b>hi</b></p>"
|
22
|
+
#
|
23
|
+
# # bad
|
24
|
+
# out = ""
|
25
|
+
# out << "<li>#{user_content}</li>"
|
26
|
+
# out << "<li>#{user_content}</li>"
|
27
|
+
# out.html_safe
|
28
|
+
# # => ActiveSupport::SafeBuffer "<li><b>hi</b></li><li><b>hi</b></li>"
|
29
|
+
#
|
30
|
+
# # good
|
31
|
+
# out = []
|
32
|
+
# out << content_tag(:li, user_content)
|
33
|
+
# out << content_tag(:li, user_content)
|
34
|
+
# safe_join(out)
|
35
|
+
# # => ActiveSupport::SafeBuffer
|
36
|
+
# # "<li><b>hi</b></li><li><b>hi</b></li>"
|
37
|
+
#
|
38
|
+
# # bad
|
39
|
+
# out = "<h1>trusted content</h1>".html_safe
|
40
|
+
# out.safe_concat(user_content)
|
41
|
+
# # => ActiveSupport::SafeBuffer "<h1>trusted_content</h1><b>hi</b>"
|
42
|
+
#
|
43
|
+
# # good
|
44
|
+
# out = "<h1>trusted content</h1>".html_safe
|
45
|
+
# out.concat(user_content)
|
46
|
+
# # => ActiveSupport::SafeBuffer
|
47
|
+
# # "<h1>trusted_content</h1><b>hi</b>"
|
48
|
+
#
|
49
|
+
# # safe, though maybe not good style
|
50
|
+
# out = "trusted content"
|
51
|
+
# result = out.concat(user_content)
|
52
|
+
# # => String "trusted content<b>hi</b>"
|
53
|
+
# # because when rendered in ERB the String will be escaped:
|
54
|
+
# # <%= result %>
|
55
|
+
# # => trusted content<b>hi</b>
|
56
|
+
#
|
57
|
+
# # bad
|
58
|
+
# (user_content + " " + content_tag(:span, user_content)).html_safe
|
59
|
+
# # => ActiveSupport::SafeBuffer "<b>hi</b> <span><b>hi</b></span>"
|
60
|
+
#
|
61
|
+
# # good
|
62
|
+
# safe_join([user_content, " ", content_tag(:span, user_content)])
|
63
|
+
# # => ActiveSupport::SafeBuffer
|
64
|
+
# # "<b>hi</b> <span><b>hi</b></span>"
|
65
|
+
class OutputSafety < Cop
|
66
|
+
MSG = 'Tagging a string as html safe may be a security risk.'
|
67
|
+
|
68
|
+
def on_send(node)
|
69
|
+
return if non_interpolated_string?(node)
|
70
|
+
|
71
|
+
return unless looks_like_rails_html_safe?(node) ||
|
72
|
+
looks_like_rails_raw?(node) ||
|
73
|
+
looks_like_rails_safe_concat?(node)
|
74
|
+
|
75
|
+
add_offense(node, location: :selector)
|
76
|
+
end
|
77
|
+
alias on_csend on_send
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def non_interpolated_string?(node)
|
82
|
+
node.receiver&.str_type? && !node.receiver.dstr_type?
|
83
|
+
end
|
84
|
+
|
85
|
+
def looks_like_rails_html_safe?(node)
|
86
|
+
node.receiver && node.method?(:html_safe) && !node.arguments?
|
87
|
+
end
|
88
|
+
|
89
|
+
def looks_like_rails_raw?(node)
|
90
|
+
node.command?(:raw) && node.arguments.one?
|
91
|
+
end
|
92
|
+
|
93
|
+
def looks_like_rails_safe_concat?(node)
|
94
|
+
node.method?(:safe_concat) && node.arguments.one?
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks for correct grammar when using ActiveSupport's
|
7
|
+
# core extensions to the numeric classes.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# # bad
|
11
|
+
# 3.day.ago
|
12
|
+
# 1.months.ago
|
13
|
+
#
|
14
|
+
# # good
|
15
|
+
# 3.days.ago
|
16
|
+
# 1.month.ago
|
17
|
+
class PluralizationGrammar < Cop
|
18
|
+
SINGULAR_DURATION_METHODS = { second: :seconds,
|
19
|
+
minute: :minutes,
|
20
|
+
hour: :hours,
|
21
|
+
day: :days,
|
22
|
+
week: :weeks,
|
23
|
+
fortnight: :fortnights,
|
24
|
+
month: :months,
|
25
|
+
year: :years }.freeze
|
26
|
+
|
27
|
+
PLURAL_DURATION_METHODS = SINGULAR_DURATION_METHODS.invert.freeze
|
28
|
+
|
29
|
+
MSG = 'Prefer `%<number>s.%<correct>s`.'
|
30
|
+
|
31
|
+
def on_send(node)
|
32
|
+
return unless duration_method?(node.method_name)
|
33
|
+
return unless literal_number?(node.receiver)
|
34
|
+
|
35
|
+
return unless offense?(node)
|
36
|
+
|
37
|
+
add_offense(node)
|
38
|
+
end
|
39
|
+
|
40
|
+
def autocorrect(node)
|
41
|
+
lambda do |corrector|
|
42
|
+
method_name = node.loc.selector.source
|
43
|
+
|
44
|
+
corrector.replace(node.loc.selector, correct_method(method_name))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def message(node)
|
51
|
+
number, = *node.receiver
|
52
|
+
|
53
|
+
format(MSG, number: number,
|
54
|
+
correct: correct_method(node.method_name.to_s))
|
55
|
+
end
|
56
|
+
|
57
|
+
def correct_method(method_name)
|
58
|
+
if plural_method?(method_name)
|
59
|
+
singularize(method_name)
|
60
|
+
else
|
61
|
+
pluralize(method_name)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def offense?(node)
|
66
|
+
number, = *node.receiver
|
67
|
+
|
68
|
+
singular_receiver?(number) && plural_method?(node.method_name) ||
|
69
|
+
plural_receiver?(number) && singular_method?(node.method_name)
|
70
|
+
end
|
71
|
+
|
72
|
+
def plural_method?(method_name)
|
73
|
+
method_name.to_s.end_with?('s')
|
74
|
+
end
|
75
|
+
|
76
|
+
def singular_method?(method_name)
|
77
|
+
!plural_method?(method_name)
|
78
|
+
end
|
79
|
+
|
80
|
+
def singular_receiver?(number)
|
81
|
+
number.abs == 1
|
82
|
+
end
|
83
|
+
|
84
|
+
def plural_receiver?(number)
|
85
|
+
!singular_receiver?(number)
|
86
|
+
end
|
87
|
+
|
88
|
+
def literal_number?(node)
|
89
|
+
node && (node.int_type? || node.float_type?)
|
90
|
+
end
|
91
|
+
|
92
|
+
def pluralize(method_name)
|
93
|
+
SINGULAR_DURATION_METHODS.fetch(method_name.to_sym).to_s
|
94
|
+
end
|
95
|
+
|
96
|
+
def singularize(method_name)
|
97
|
+
PLURAL_DURATION_METHODS.fetch(method_name.to_sym).to_s
|
98
|
+
end
|
99
|
+
|
100
|
+
def duration_method?(method_name)
|
101
|
+
SINGULAR_DURATION_METHODS.key?(method_name) ||
|
102
|
+
PLURAL_DURATION_METHODS.key?(method_name)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks code that can be written more easily using
|
7
|
+
# `Object#presence` defined by Active Support.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# # bad
|
11
|
+
# a.present? ? a : nil
|
12
|
+
#
|
13
|
+
# # bad
|
14
|
+
# !a.present? ? nil : a
|
15
|
+
#
|
16
|
+
# # bad
|
17
|
+
# a.blank? ? nil : a
|
18
|
+
#
|
19
|
+
# # bad
|
20
|
+
# !a.blank? ? a : nil
|
21
|
+
#
|
22
|
+
# # good
|
23
|
+
# a.presence
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# # bad
|
27
|
+
# a.present? ? a : b
|
28
|
+
#
|
29
|
+
# # bad
|
30
|
+
# !a.present? ? b : a
|
31
|
+
#
|
32
|
+
# # bad
|
33
|
+
# a.blank? ? b : a
|
34
|
+
#
|
35
|
+
# # bad
|
36
|
+
# !a.blank? ? a : b
|
37
|
+
#
|
38
|
+
# # good
|
39
|
+
# a.presence || b
|
40
|
+
class Presence < Cop
|
41
|
+
include RangeHelp
|
42
|
+
|
43
|
+
MSG = 'Use `%<prefer>s` instead of `%<current>s`.'
|
44
|
+
|
45
|
+
def_node_matcher :redundant_receiver_and_other, <<~PATTERN
|
46
|
+
{
|
47
|
+
(if
|
48
|
+
(send $_recv :present?)
|
49
|
+
_recv
|
50
|
+
$!begin
|
51
|
+
)
|
52
|
+
(if
|
53
|
+
(send $_recv :blank?)
|
54
|
+
$!begin
|
55
|
+
_recv
|
56
|
+
)
|
57
|
+
}
|
58
|
+
PATTERN
|
59
|
+
|
60
|
+
def_node_matcher :redundant_negative_receiver_and_other, <<~PATTERN
|
61
|
+
{
|
62
|
+
(if
|
63
|
+
(send (send $_recv :present?) :!)
|
64
|
+
$!begin
|
65
|
+
_recv
|
66
|
+
)
|
67
|
+
(if
|
68
|
+
(send (send $_recv :blank?) :!)
|
69
|
+
_recv
|
70
|
+
$!begin
|
71
|
+
)
|
72
|
+
}
|
73
|
+
PATTERN
|
74
|
+
|
75
|
+
def on_if(node)
|
76
|
+
return if ignore_if_node?(node)
|
77
|
+
|
78
|
+
redundant_receiver_and_other(node) do |receiver, other|
|
79
|
+
unless ignore_other_node?(other) || receiver.nil?
|
80
|
+
add_offense(node, message: message(node, receiver, other))
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
redundant_negative_receiver_and_other(node) do |receiver, other|
|
85
|
+
unless ignore_other_node?(other) || receiver.nil?
|
86
|
+
add_offense(node, message: message(node, receiver, other))
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def autocorrect(node)
|
92
|
+
lambda do |corrector|
|
93
|
+
redundant_receiver_and_other(node) do |receiver, other|
|
94
|
+
corrector.replace(node.source_range, replacement(receiver, other))
|
95
|
+
end
|
96
|
+
|
97
|
+
redundant_negative_receiver_and_other(node) do |receiver, other|
|
98
|
+
corrector.replace(node.source_range, replacement(receiver, other))
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def ignore_if_node?(node)
|
106
|
+
node.elsif?
|
107
|
+
end
|
108
|
+
|
109
|
+
def ignore_other_node?(node)
|
110
|
+
node && (node.if_type? || node.rescue_type? || node.while_type?)
|
111
|
+
end
|
112
|
+
|
113
|
+
def message(node, receiver, other)
|
114
|
+
format(MSG,
|
115
|
+
prefer: replacement(receiver, other),
|
116
|
+
current: node.source)
|
117
|
+
end
|
118
|
+
|
119
|
+
def replacement(receiver, other)
|
120
|
+
or_source = if other&.send_type?
|
121
|
+
build_source_for_or_method(other)
|
122
|
+
elsif other.nil? || other.nil_type?
|
123
|
+
''
|
124
|
+
else
|
125
|
+
" || #{other.source}"
|
126
|
+
end
|
127
|
+
|
128
|
+
"#{receiver.source}.presence" + or_source
|
129
|
+
end
|
130
|
+
|
131
|
+
def build_source_for_or_method(other)
|
132
|
+
if other.parenthesized? || other.method?('[]') || !other.arguments?
|
133
|
+
" || #{other.source}"
|
134
|
+
else
|
135
|
+
method = range_between(
|
136
|
+
other.source_range.begin_pos,
|
137
|
+
other.first_argument.source_range.begin_pos - 1
|
138
|
+
).source
|
139
|
+
|
140
|
+
arguments = other.arguments.map(&:source).join(', ')
|
141
|
+
|
142
|
+
" || #{method}(#{arguments})"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|