rubocop-rails 2.6.0 → 2.9.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 +4 -4
- data/README.md +16 -0
- data/config/default.yml +189 -6
- data/lib/rubocop/cop/mixin/active_record_helper.rb +12 -3
- data/lib/rubocop/cop/mixin/enforce_superclass.rb +40 -0
- data/lib/rubocop/cop/mixin/index_method.rb +25 -11
- data/lib/rubocop/cop/rails/action_filter.rb +10 -14
- data/lib/rubocop/cop/rails/active_record_aliases.rb +13 -17
- data/lib/rubocop/cop/rails/active_record_callbacks_order.rb +148 -0
- data/lib/rubocop/cop/rails/active_record_override.rb +1 -1
- data/lib/rubocop/cop/rails/active_support_aliases.rb +12 -21
- data/lib/rubocop/cop/rails/after_commit_override.rb +91 -0
- data/lib/rubocop/cop/rails/application_controller.rb +3 -7
- data/lib/rubocop/cop/rails/application_job.rb +2 -1
- data/lib/rubocop/cop/rails/application_mailer.rb +2 -7
- data/lib/rubocop/cop/rails/application_record.rb +2 -7
- data/lib/rubocop/cop/rails/arel_star.rb +41 -0
- data/lib/rubocop/cop/rails/assert_not.rb +8 -10
- data/lib/rubocop/cop/rails/attribute_default_block_value.rb +90 -0
- data/lib/rubocop/cop/rails/belongs_to.rb +9 -18
- data/lib/rubocop/cop/rails/blank.rb +27 -27
- data/lib/rubocop/cop/rails/bulk_change_table.rb +1 -1
- data/lib/rubocop/cop/rails/content_tag.rb +20 -33
- data/lib/rubocop/cop/rails/create_table_with_timestamps.rb +2 -1
- data/lib/rubocop/cop/rails/date.rb +10 -11
- data/lib/rubocop/cop/rails/default_scope.rb +61 -0
- data/lib/rubocop/cop/rails/delegate.rb +10 -10
- data/lib/rubocop/cop/rails/delegate_allow_blank.rb +7 -8
- data/lib/rubocop/cop/rails/dynamic_find_by.rb +13 -11
- data/lib/rubocop/cop/rails/enum_hash.rb +11 -10
- data/lib/rubocop/cop/rails/enum_uniqueness.rb +2 -1
- data/lib/rubocop/cop/rails/environment_comparison.rb +18 -14
- data/lib/rubocop/cop/rails/exit.rb +4 -10
- data/lib/rubocop/cop/rails/file_path.rb +5 -4
- data/lib/rubocop/cop/rails/find_by.rb +13 -13
- data/lib/rubocop/cop/rails/find_by_id.rb +94 -0
- data/lib/rubocop/cop/rails/find_each.rb +16 -14
- data/lib/rubocop/cop/rails/has_and_belongs_to_many.rb +3 -2
- data/lib/rubocop/cop/rails/has_many_or_has_one_dependent.rb +4 -7
- data/lib/rubocop/cop/rails/helper_instance_variable.rb +4 -2
- data/lib/rubocop/cop/rails/http_positional_arguments.rb +25 -21
- data/lib/rubocop/cop/rails/http_status.rb +7 -9
- data/lib/rubocop/cop/rails/ignored_skip_action_filter_option.rb +8 -6
- data/lib/rubocop/cop/rails/index_by.rb +11 -2
- data/lib/rubocop/cop/rails/index_with.rb +11 -2
- data/lib/rubocop/cop/rails/inquiry.rb +39 -0
- data/lib/rubocop/cop/rails/inverse_of.rb +3 -2
- data/lib/rubocop/cop/rails/lexically_scoped_action_filter.rb +17 -15
- data/lib/rubocop/cop/rails/link_to_blank.rb +20 -20
- data/lib/rubocop/cop/rails/mailer_name.rb +86 -0
- data/lib/rubocop/cop/rails/match_route.rb +120 -0
- data/lib/rubocop/cop/rails/negate_include.rb +41 -0
- data/lib/rubocop/cop/rails/not_null_column.rb +2 -1
- data/lib/rubocop/cop/rails/order_by_id.rb +52 -0
- data/lib/rubocop/cop/rails/output.rb +5 -2
- data/lib/rubocop/cop/rails/output_safety.rb +3 -2
- data/lib/rubocop/cop/rails/pick.rb +21 -15
- data/lib/rubocop/cop/rails/pluck.rb +56 -0
- data/lib/rubocop/cop/rails/pluck_id.rb +56 -0
- data/lib/rubocop/cop/rails/pluck_in_where.rb +70 -0
- data/lib/rubocop/cop/rails/pluralization_grammar.rb +10 -14
- data/lib/rubocop/cop/rails/presence.rb +12 -13
- data/lib/rubocop/cop/rails/present.rb +30 -24
- data/lib/rubocop/cop/rails/rake_environment.rb +9 -11
- data/lib/rubocop/cop/rails/read_write_attribute.rb +12 -11
- data/lib/rubocop/cop/rails/redundant_allow_nil.rb +29 -31
- data/lib/rubocop/cop/rails/redundant_foreign_key.rb +9 -12
- data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +11 -10
- data/lib/rubocop/cop/rails/reflection_class_name.rb +4 -3
- data/lib/rubocop/cop/rails/refute_methods.rb +9 -10
- data/lib/rubocop/cop/rails/relative_date_constant.rb +20 -9
- data/lib/rubocop/cop/rails/render_inline.rb +41 -0
- data/lib/rubocop/cop/rails/render_plain_text.rb +71 -0
- data/lib/rubocop/cop/rails/request_referer.rb +7 -7
- data/lib/rubocop/cop/rails/reversible_migration.rb +82 -7
- data/lib/rubocop/cop/rails/safe_navigation.rb +12 -11
- data/lib/rubocop/cop/rails/safe_navigation_with_blank.rb +5 -10
- data/lib/rubocop/cop/rails/save_bang.rb +19 -22
- data/lib/rubocop/cop/rails/scope_args.rb +2 -1
- data/lib/rubocop/cop/rails/short_i18n.rb +74 -0
- data/lib/rubocop/cop/rails/skips_model_validations.rb +46 -11
- data/lib/rubocop/cop/rails/squished_sql_heredocs.rb +82 -0
- data/lib/rubocop/cop/rails/time_zone.rb +22 -20
- data/lib/rubocop/cop/rails/uniq_before_pluck.rb +10 -10
- data/lib/rubocop/cop/rails/unique_validation_without_index.rb +18 -8
- data/lib/rubocop/cop/rails/unknown_env.rb +15 -4
- data/lib/rubocop/cop/rails/validation.rb +15 -14
- data/lib/rubocop/cop/rails/where_equals.rb +94 -0
- data/lib/rubocop/cop/rails/where_exists.rb +126 -0
- data/lib/rubocop/cop/rails/where_not.rb +97 -0
- data/lib/rubocop/cop/rails_cops.rb +22 -0
- data/lib/rubocop/rails/schema_loader.rb +4 -4
- data/lib/rubocop/rails/schema_loader/schema.rb +5 -5
- data/lib/rubocop/rails/version.rb +5 -1
- metadata +37 -9
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop enforces that mailer names end with `Mailer` suffix.
|
7
|
+
#
|
8
|
+
# Without the `Mailer` suffix it isn't immediately apparent what's a mailer
|
9
|
+
# and which views are related to the mailer.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# # bad
|
13
|
+
# class User < ActionMailer::Base
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# class User < ApplicationMailer
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# # good
|
20
|
+
# class UserMailer < ActionMailer::Base
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# class UserMailer < ApplicationMailer
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
class MailerName < Base
|
27
|
+
extend AutoCorrector
|
28
|
+
|
29
|
+
MSG = 'Mailer should end with `Mailer` suffix.'
|
30
|
+
|
31
|
+
def_node_matcher :mailer_base_class?, <<~PATTERN
|
32
|
+
{
|
33
|
+
(const (const nil? :ActionMailer) :Base)
|
34
|
+
(const nil? :ApplicationMailer)
|
35
|
+
}
|
36
|
+
PATTERN
|
37
|
+
|
38
|
+
def_node_matcher :class_definition?, <<~PATTERN
|
39
|
+
(class $(const _ !#mailer_suffix?) #mailer_base_class? ...)
|
40
|
+
PATTERN
|
41
|
+
|
42
|
+
def_node_matcher :class_new_definition?, <<~PATTERN
|
43
|
+
(send (const nil? :Class) :new #mailer_base_class?)
|
44
|
+
PATTERN
|
45
|
+
|
46
|
+
def on_class(node)
|
47
|
+
class_definition?(node) do |name_node|
|
48
|
+
add_offense(name_node) do |corrector|
|
49
|
+
autocorrect(corrector, name_node)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def on_send(node)
|
55
|
+
return unless class_new_definition?(node)
|
56
|
+
|
57
|
+
casgn_parent = node.each_ancestor(:casgn).first
|
58
|
+
return unless casgn_parent
|
59
|
+
|
60
|
+
name = casgn_parent.children[1]
|
61
|
+
return if mailer_suffix?(name)
|
62
|
+
|
63
|
+
add_offense(casgn_parent.loc.name) do |corrector|
|
64
|
+
autocorrect(corrector, casgn_parent)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def autocorrect(corrector, node)
|
71
|
+
if node.casgn_type?
|
72
|
+
name = node.children[1]
|
73
|
+
corrector.replace(node.loc.name, "#{name}Mailer")
|
74
|
+
else
|
75
|
+
name = node.children.last
|
76
|
+
corrector.replace(node.source_range, "#{name}Mailer")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def mailer_suffix?(mailer_name)
|
81
|
+
mailer_name.to_s.end_with?('Mailer')
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop identifies places where defining routes with `match`
|
7
|
+
# can be replaced with a specific HTTP method.
|
8
|
+
#
|
9
|
+
# Don't use `match` to define any routes unless there is a need to map multiple request types
|
10
|
+
# among [:get, :post, :patch, :put, :delete] to a single action using the `:via` option.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# # bad
|
14
|
+
# match ':controller/:action/:id'
|
15
|
+
# match 'photos/:id', to: 'photos#show', via: :get
|
16
|
+
#
|
17
|
+
# # good
|
18
|
+
# get ':controller/:action/:id'
|
19
|
+
# get 'photos/:id', to: 'photos#show'
|
20
|
+
# match 'photos/:id', to: 'photos#show', via: [:get, :post]
|
21
|
+
# match 'photos/:id', to: 'photos#show', via: :all
|
22
|
+
#
|
23
|
+
class MatchRoute < Base
|
24
|
+
extend AutoCorrector
|
25
|
+
|
26
|
+
MSG = 'Use `%<http_method>s` instead of `match` to define a route.'
|
27
|
+
RESTRICT_ON_SEND = %i[match].freeze
|
28
|
+
HTTP_METHODS = %i[get post put patch delete].freeze
|
29
|
+
|
30
|
+
def_node_matcher :match_method_call?, <<~PATTERN
|
31
|
+
(send nil? :match $_ $(hash ...) ?)
|
32
|
+
PATTERN
|
33
|
+
|
34
|
+
def on_send(node)
|
35
|
+
match_method_call?(node) do |path_node, options_node|
|
36
|
+
return unless within_routes?(node)
|
37
|
+
|
38
|
+
options_node = path_node.hash_type? ? path_node : options_node.first
|
39
|
+
|
40
|
+
if options_node.nil?
|
41
|
+
register_offense(node, 'get')
|
42
|
+
else
|
43
|
+
via = extract_via(options_node)
|
44
|
+
return unless via.size == 1 && http_method?(via.first)
|
45
|
+
|
46
|
+
register_offense(node, via.first)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def register_offense(node, http_method)
|
54
|
+
add_offense(node, message: format(MSG, http_method: http_method)) do |corrector|
|
55
|
+
match_method_call?(node) do |path_node, options_node|
|
56
|
+
options_node = options_node.first
|
57
|
+
|
58
|
+
corrector.replace(node, replacement(path_node, options_node))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def_node_matcher :routes_draw?, <<~PATTERN
|
64
|
+
(send (send _ :routes) :draw)
|
65
|
+
PATTERN
|
66
|
+
|
67
|
+
def within_routes?(node)
|
68
|
+
node.each_ancestor(:block).any? { |a| routes_draw?(a.send_node) }
|
69
|
+
end
|
70
|
+
|
71
|
+
def extract_via(node)
|
72
|
+
via_pair = via_pair(node)
|
73
|
+
return %i[get] unless via_pair
|
74
|
+
|
75
|
+
_, via = *via_pair
|
76
|
+
|
77
|
+
if via.basic_literal?
|
78
|
+
[via.value]
|
79
|
+
elsif via.array_type?
|
80
|
+
via.values.map(&:value)
|
81
|
+
else
|
82
|
+
[]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def via_pair(node)
|
87
|
+
node.pairs.find { |p| p.key.value == :via }
|
88
|
+
end
|
89
|
+
|
90
|
+
def http_method?(method)
|
91
|
+
HTTP_METHODS.include?(method.to_sym)
|
92
|
+
end
|
93
|
+
|
94
|
+
def replacement(path_node, options_node)
|
95
|
+
if path_node.hash_type?
|
96
|
+
http_method, options = *http_method_and_options(path_node)
|
97
|
+
"#{http_method} #{options.map(&:source).join(', ')}"
|
98
|
+
elsif options_node.nil?
|
99
|
+
"get #{path_node.source}"
|
100
|
+
else
|
101
|
+
http_method, options = *http_method_and_options(options_node)
|
102
|
+
|
103
|
+
if options.any?
|
104
|
+
"#{http_method} #{path_node.source}, #{options.map(&:source).join(', ')}"
|
105
|
+
else
|
106
|
+
"#{http_method} #{path_node.source}"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def http_method_and_options(node)
|
112
|
+
via_pair = via_pair(node)
|
113
|
+
http_method = extract_via(node).first
|
114
|
+
rest_pairs = node.pairs - [via_pair]
|
115
|
+
[http_method, rest_pairs]
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop enforces the use of `collection.exclude?(obj)`
|
7
|
+
# over `!collection.include?(obj)`.
|
8
|
+
#
|
9
|
+
# It is marked as unsafe by default because false positive will occur for
|
10
|
+
# a receiver object that do not have `exclude?` method. (e.g. `IPAddr`)
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# # bad
|
14
|
+
# !array.include?(2)
|
15
|
+
# !hash.include?(:key)
|
16
|
+
#
|
17
|
+
# # good
|
18
|
+
# array.exclude?(2)
|
19
|
+
# hash.exclude?(:key)
|
20
|
+
#
|
21
|
+
class NegateInclude < Base
|
22
|
+
extend AutoCorrector
|
23
|
+
|
24
|
+
MSG = 'Use `.exclude?` and remove the negation part.'
|
25
|
+
RESTRICT_ON_SEND = %i[!].freeze
|
26
|
+
|
27
|
+
def_node_matcher :negate_include_call?, <<~PATTERN
|
28
|
+
(send (send $_ :include? $_) :!)
|
29
|
+
PATTERN
|
30
|
+
|
31
|
+
def on_send(node)
|
32
|
+
return unless (receiver, obj = negate_include_call?(node))
|
33
|
+
|
34
|
+
add_offense(node) do |corrector|
|
35
|
+
corrector.replace(node, "#{receiver.source}.exclude?(#{obj.source})")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -16,8 +16,9 @@ module RuboCop
|
|
16
16
|
# add_column :users, :name, :string, null: false, default: ''
|
17
17
|
# add_reference :products, :category
|
18
18
|
# add_reference :products, :category, null: false, default: 1
|
19
|
-
class NotNullColumn <
|
19
|
+
class NotNullColumn < Base
|
20
20
|
MSG = 'Do not add a NOT NULL column without a default value.'
|
21
|
+
RESTRICT_ON_SEND = %i[add_column add_reference].freeze
|
21
22
|
|
22
23
|
def_node_matcher :add_not_null_column?, <<~PATTERN
|
23
24
|
(send nil? :add_column _ _ _ (hash $...))
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks for places where ordering by `id` column is used.
|
7
|
+
#
|
8
|
+
# Don't use the `id` column for ordering. The sequence of ids is not guaranteed
|
9
|
+
# to be in any particular order, despite often (incidentally) being chronological.
|
10
|
+
# Use a timestamp column to order chronologically. As a bonus the intent is clearer.
|
11
|
+
#
|
12
|
+
# NOTE: Make sure the changed order column does not introduce performance
|
13
|
+
# bottlenecks and appropriate database indexes are added.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# # bad
|
17
|
+
# scope :chronological, -> { order(id: :asc) }
|
18
|
+
# scope :chronological, -> { order(primary_key => :asc) }
|
19
|
+
#
|
20
|
+
# # good
|
21
|
+
# scope :chronological, -> { order(created_at: :asc) }
|
22
|
+
#
|
23
|
+
class OrderById < Base
|
24
|
+
include RangeHelp
|
25
|
+
|
26
|
+
MSG = 'Do not use the `id` column for ordering. '\
|
27
|
+
'Use a timestamp column to order chronologically.'
|
28
|
+
RESTRICT_ON_SEND = %i[order].freeze
|
29
|
+
|
30
|
+
def_node_matcher :order_by_id?, <<~PATTERN
|
31
|
+
(send _ :order
|
32
|
+
{
|
33
|
+
(sym :id)
|
34
|
+
(hash (pair (sym :id) _))
|
35
|
+
(send _ :primary_key)
|
36
|
+
(hash (pair (send _ :primary_key) _))
|
37
|
+
})
|
38
|
+
PATTERN
|
39
|
+
|
40
|
+
def on_send(node)
|
41
|
+
add_offense(offense_range(node)) if order_by_id?(node)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def offense_range(node)
|
47
|
+
range_between(node.loc.selector.begin_pos, node.source_range.end_pos)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -13,9 +13,12 @@ module RuboCop
|
|
13
13
|
#
|
14
14
|
# # good
|
15
15
|
# Rails.logger.debug 'A debug message'
|
16
|
-
class Output <
|
16
|
+
class Output < Base
|
17
17
|
MSG = 'Do not write to stdout. ' \
|
18
18
|
"Use Rails's logger if you want to log."
|
19
|
+
RESTRICT_ON_SEND = %i[
|
20
|
+
ap p pp pretty_print print puts binwrite syswrite write write_nonblock
|
21
|
+
].freeze
|
19
22
|
|
20
23
|
def_node_matcher :output?, <<~PATTERN
|
21
24
|
(send nil? {:ap :p :pp :pretty_print :print :puts} ...)
|
@@ -35,7 +38,7 @@ module RuboCop
|
|
35
38
|
return unless (output?(node) || io_output?(node)) &&
|
36
39
|
node.arguments?
|
37
40
|
|
38
|
-
add_offense(node
|
41
|
+
add_offense(node.loc.selector)
|
39
42
|
end
|
40
43
|
|
41
44
|
private
|
@@ -62,8 +62,9 @@ module RuboCop
|
|
62
62
|
# safe_join([user_content, " ", content_tag(:span, user_content)])
|
63
63
|
# # => ActiveSupport::SafeBuffer
|
64
64
|
# # "<b>hi</b> <span><b>hi</b></span>"
|
65
|
-
class OutputSafety <
|
65
|
+
class OutputSafety < Base
|
66
66
|
MSG = 'Tagging a string as html safe may be a security risk.'
|
67
|
+
RESTRICT_ON_SEND = %i[html_safe raw safe_concat].freeze
|
67
68
|
|
68
69
|
def on_send(node)
|
69
70
|
return if non_interpolated_string?(node)
|
@@ -72,7 +73,7 @@ module RuboCop
|
|
72
73
|
looks_like_rails_raw?(node) ||
|
73
74
|
looks_like_rails_safe_concat?(node)
|
74
75
|
|
75
|
-
add_offense(node
|
76
|
+
add_offense(node.loc.selector)
|
76
77
|
end
|
77
78
|
alias on_csend on_send
|
78
79
|
|
@@ -3,20 +3,26 @@
|
|
3
3
|
module RuboCop
|
4
4
|
module Cop
|
5
5
|
module Rails
|
6
|
-
# This cop enforces the use `pick` over `pluck(...).first`.
|
6
|
+
# This cop enforces the use of `pick` over `pluck(...).first`.
|
7
|
+
#
|
8
|
+
# Using `pluck` followed by `first` creates an intermediate array, which
|
9
|
+
# `pick` avoids. When called on an Active Record relation, `pick` adds a
|
10
|
+
# limit to the query so that only one value is fetched from the database.
|
7
11
|
#
|
8
12
|
# @example
|
9
13
|
# # bad
|
10
14
|
# Model.pluck(:a).first
|
11
|
-
#
|
15
|
+
# [{ a: :b, c: :d }].pluck(:a, :b).first
|
12
16
|
#
|
13
17
|
# # good
|
14
18
|
# Model.pick(:a)
|
15
|
-
#
|
16
|
-
class Pick <
|
19
|
+
# [{ a: :b, c: :d }].pick(:a, :b)
|
20
|
+
class Pick < Base
|
21
|
+
extend AutoCorrector
|
17
22
|
extend TargetRailsVersion
|
18
23
|
|
19
24
|
MSG = 'Prefer `pick(%<args>s)` over `pluck(%<args>s).first`.'
|
25
|
+
RESTRICT_ON_SEND = %i[first].freeze
|
20
26
|
|
21
27
|
minimum_target_rails_version 6.0
|
22
28
|
|
@@ -26,24 +32,24 @@ module RuboCop
|
|
26
32
|
|
27
33
|
def on_send(node)
|
28
34
|
pick_candidate?(node) do
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
35
|
+
receiver = node.receiver
|
36
|
+
receiver_selector = receiver.loc.selector
|
37
|
+
node_selector = node.loc.selector
|
38
|
+
range = receiver_selector.join(node_selector)
|
33
39
|
|
34
|
-
|
35
|
-
|
40
|
+
add_offense(range, message: message(receiver)) do |corrector|
|
41
|
+
first_range = receiver.source_range.end.join(node_selector)
|
36
42
|
|
37
|
-
|
38
|
-
|
39
|
-
|
43
|
+
corrector.remove(first_range)
|
44
|
+
corrector.replace(receiver_selector, 'pick')
|
45
|
+
end
|
40
46
|
end
|
41
47
|
end
|
42
48
|
|
43
49
|
private
|
44
50
|
|
45
|
-
def message(
|
46
|
-
format(MSG, args:
|
51
|
+
def message(receiver)
|
52
|
+
format(MSG, args: receiver.arguments.map(&:source).join(', '))
|
47
53
|
end
|
48
54
|
end
|
49
55
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop enforces the use of `pluck` over `map`.
|
7
|
+
#
|
8
|
+
# `pluck` can be used instead of `map` to extract a single key from each
|
9
|
+
# element in an enumerable. When called on an Active Record relation, it
|
10
|
+
# results in a more efficient query that only selects the necessary key.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# # bad
|
14
|
+
# Post.published.map { |post| post[:title] }
|
15
|
+
# [{ a: :b, c: :d }].collect { |el| el[:a] }
|
16
|
+
#
|
17
|
+
# # good
|
18
|
+
# Post.published.pluck(:title)
|
19
|
+
# [{ a: :b, c: :d }].pluck(:a)
|
20
|
+
class Pluck < Base
|
21
|
+
extend AutoCorrector
|
22
|
+
extend TargetRailsVersion
|
23
|
+
|
24
|
+
MSG = 'Prefer `pluck(:%<value>s)` over `%<method>s { |%<argument>s| %<element>s[:%<value>s] }`.'
|
25
|
+
|
26
|
+
minimum_target_rails_version 5.0
|
27
|
+
|
28
|
+
def_node_matcher :pluck_candidate?, <<~PATTERN
|
29
|
+
(block (send _ ${:map :collect}) (args (arg $_argument)) (send (lvar $_element) :[] (sym $_value)))
|
30
|
+
PATTERN
|
31
|
+
|
32
|
+
def on_block(node)
|
33
|
+
pluck_candidate?(node) do |method, argument, element, value|
|
34
|
+
next unless argument == element
|
35
|
+
|
36
|
+
message = message(method, argument, element, value)
|
37
|
+
|
38
|
+
add_offense(offense_range(node), message: message) do |corrector|
|
39
|
+
corrector.replace(offense_range(node), "pluck(:#{value})")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def offense_range(node)
|
47
|
+
node.send_node.loc.selector.join(node.loc.end)
|
48
|
+
end
|
49
|
+
|
50
|
+
def message(method, argument, element, value)
|
51
|
+
format(MSG, method: method, argument: argument, element: element, value: value)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|