rubocop-rails 2.4.2 → 2.7.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +5 -1
  4. data/config/default.yml +179 -9
  5. data/lib/rubocop-rails.rb +3 -0
  6. data/lib/rubocop/cop/mixin/active_record_helper.rb +84 -0
  7. data/lib/rubocop/cop/mixin/index_method.rb +161 -0
  8. data/lib/rubocop/cop/rails/active_record_callbacks_order.rb +145 -0
  9. data/lib/rubocop/cop/rails/content_tag.rb +69 -0
  10. data/lib/rubocop/cop/rails/create_table_with_timestamps.rb +1 -3
  11. data/lib/rubocop/cop/rails/default_scope.rb +54 -0
  12. data/lib/rubocop/cop/rails/delegate.rb +2 -4
  13. data/lib/rubocop/cop/rails/dynamic_find_by.rb +40 -15
  14. data/lib/rubocop/cop/rails/environment_comparison.rb +60 -14
  15. data/lib/rubocop/cop/rails/exit.rb +2 -2
  16. data/lib/rubocop/cop/rails/file_path.rb +2 -1
  17. data/lib/rubocop/cop/rails/find_by_id.rb +103 -0
  18. data/lib/rubocop/cop/rails/http_positional_arguments.rb +2 -2
  19. data/lib/rubocop/cop/rails/http_status.rb +2 -0
  20. data/lib/rubocop/cop/rails/index_by.rb +56 -0
  21. data/lib/rubocop/cop/rails/index_with.rb +59 -0
  22. data/lib/rubocop/cop/rails/inquiry.rb +34 -0
  23. data/lib/rubocop/cop/rails/inverse_of.rb +0 -4
  24. data/lib/rubocop/cop/rails/link_to_blank.rb +3 -3
  25. data/lib/rubocop/cop/rails/mailer_name.rb +80 -0
  26. data/lib/rubocop/cop/rails/match_route.rb +117 -0
  27. data/lib/rubocop/cop/rails/negate_include.rb +39 -0
  28. data/lib/rubocop/cop/rails/pick.rb +55 -0
  29. data/lib/rubocop/cop/rails/pluck.rb +59 -0
  30. data/lib/rubocop/cop/rails/pluck_id.rb +58 -0
  31. data/lib/rubocop/cop/rails/pluck_in_where.rb +36 -0
  32. data/lib/rubocop/cop/rails/presence.rb +2 -6
  33. data/lib/rubocop/cop/rails/rake_environment.rb +17 -0
  34. data/lib/rubocop/cop/rails/redundant_foreign_key.rb +80 -0
  35. data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +0 -3
  36. data/lib/rubocop/cop/rails/refute_methods.rb +52 -26
  37. data/lib/rubocop/cop/rails/render_inline.rb +48 -0
  38. data/lib/rubocop/cop/rails/render_plain_text.rb +76 -0
  39. data/lib/rubocop/cop/rails/reversible_migration.rb +6 -1
  40. data/lib/rubocop/cop/rails/safe_navigation.rb +1 -1
  41. data/lib/rubocop/cop/rails/save_bang.rb +6 -7
  42. data/lib/rubocop/cop/rails/short_i18n.rb +76 -0
  43. data/lib/rubocop/cop/rails/skips_model_validations.rb +46 -8
  44. data/lib/rubocop/cop/rails/time_zone.rb +1 -3
  45. data/lib/rubocop/cop/rails/uniq_before_pluck.rb +12 -12
  46. data/lib/rubocop/cop/rails/unique_validation_without_index.rb +155 -0
  47. data/lib/rubocop/cop/rails/unknown_env.rb +7 -6
  48. data/lib/rubocop/cop/rails/where_exists.rb +68 -0
  49. data/lib/rubocop/cop/rails_cops.rb +22 -0
  50. data/lib/rubocop/rails/schema_loader.rb +61 -0
  51. data/lib/rubocop/rails/schema_loader/schema.rb +190 -0
  52. data/lib/rubocop/rails/version.rb +1 -1
  53. metadata +46 -8
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop checks that Active Support's `inquiry` method is not used.
7
+ #
8
+ # @example
9
+ # # bad - String#inquiry
10
+ # ruby = 'two'.inquiry
11
+ # ruby.two?
12
+ #
13
+ # # good
14
+ # ruby = 'two'
15
+ # ruby == 'two'
16
+ #
17
+ # # bad - Array#inquiry
18
+ # pets = %w(cat dog).inquiry
19
+ # pets.gopher?
20
+ #
21
+ # # good
22
+ # pets = %w(cat dog)
23
+ # pets.include? 'cat'
24
+ #
25
+ class Inquiry < Cop
26
+ MSG = "Prefer Ruby's comparison operators over Active Support's `inquiry`."
27
+
28
+ def on_send(node)
29
+ add_offense(node, location: :selector) if node.method?(:inquiry) && node.arguments.empty?
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -129,10 +129,6 @@ module RuboCop
129
129
  # @see https://guides.rubyonrails.org/association_basics.html#bi-directional-associations
130
130
  # @see https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#module-ActiveRecord::Associations::ClassMethods-label-Setting+Inverses
131
131
  class InverseOf < Cop
132
- extend TargetRailsVersion
133
-
134
- minimum_target_rails_version 4.1
135
-
136
132
  SPECIFY_MSG = 'Specify an `:inverse_of` option.'
137
133
  NIL_MSG = 'You specified `inverse_of: nil`, you probably meant to ' \
138
134
  'use `inverse_of: false`.'
@@ -35,6 +35,7 @@ module RuboCop
35
35
  (pair {(sym :rel) (str "rel")} (str _))
36
36
  PATTERN
37
37
 
38
+ # rubocop:disable Metrics/CyclomaticComplexity
38
39
  def on_send(node)
39
40
  return unless node.method?(:link_to)
40
41
 
@@ -42,11 +43,10 @@ module RuboCop
42
43
 
43
44
  option_nodes.map(&:children).each do |options|
44
45
  blank = options.find { |o| blank_target?(o) }
45
- if blank && options.none? { |o| includes_noopener?(o) }
46
- add_offense(blank)
47
- end
46
+ add_offense(blank) if blank && options.none? { |o| includes_noopener?(o) }
48
47
  end
49
48
  end
49
+ # rubocop:enable Metrics/CyclomaticComplexity
50
50
 
51
51
  def autocorrect(node)
52
52
  lambda do |corrector|
@@ -0,0 +1,80 @@
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 < Cop
27
+ MSG = 'Mailer should end with `Mailer` suffix.'
28
+
29
+ def_node_matcher :mailer_base_class?, <<~PATTERN
30
+ {
31
+ (const (const nil? :ActionMailer) :Base)
32
+ (const nil? :ApplicationMailer)
33
+ }
34
+ PATTERN
35
+
36
+ def_node_matcher :class_definition?, <<~PATTERN
37
+ (class $(const _ !#mailer_suffix?) #mailer_base_class? ...)
38
+ PATTERN
39
+
40
+ def_node_matcher :class_new_definition?, <<~PATTERN
41
+ (send (const nil? :Class) :new #mailer_base_class?)
42
+ PATTERN
43
+
44
+ def on_class(node)
45
+ class_definition?(node) do |name_node|
46
+ add_offense(name_node)
47
+ end
48
+ end
49
+
50
+ def on_send(node)
51
+ return unless class_new_definition?(node)
52
+
53
+ casgn_parent = node.each_ancestor(:casgn).first
54
+ return unless casgn_parent
55
+
56
+ name = casgn_parent.children[1]
57
+ add_offense(casgn_parent, location: :name) unless mailer_suffix?(name)
58
+ end
59
+
60
+ def autocorrect(node)
61
+ lambda do |corrector|
62
+ if node.casgn_type?
63
+ name = node.children[1]
64
+ corrector.replace(node.loc.name, "#{name}Mailer")
65
+ else
66
+ name = node.children.last
67
+ corrector.replace(node.source_range, "#{name}Mailer")
68
+ end
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def mailer_suffix?(mailer_name)
75
+ mailer_name.to_s.end_with?('Mailer')
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,117 @@
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 < Cop
24
+ MSG = 'Use `%<http_method>s` instead of `match` to define a route.'
25
+ HTTP_METHODS = %i[get post put patch delete].freeze
26
+
27
+ def_node_matcher :match_method_call?, <<~PATTERN
28
+ (send nil? :match $_ $(hash ...) ?)
29
+ PATTERN
30
+
31
+ def on_send(node)
32
+ match_method_call?(node) do |path_node, options_node|
33
+ return unless within_routes?(node)
34
+
35
+ options_node = path_node.hash_type? ? path_node : options_node.first
36
+
37
+ if options_node.nil?
38
+ message = format(MSG, http_method: 'get')
39
+ add_offense(node, message: message)
40
+ else
41
+ via = extract_via(options_node)
42
+ if via.size == 1 && http_method?(via.first)
43
+ message = format(MSG, http_method: via.first)
44
+ add_offense(node, message: message)
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ def autocorrect(node)
51
+ match_method_call?(node) do |path_node, options_node|
52
+ options_node = options_node.first
53
+
54
+ lambda do |corrector|
55
+ corrector.replace(node, replacement(path_node, options_node))
56
+ end
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def_node_matcher :routes_draw?, <<~PATTERN
63
+ (send (send _ :routes) :draw)
64
+ PATTERN
65
+
66
+ def within_routes?(node)
67
+ node.each_ancestor(:block).any? { |a| routes_draw?(a.send_node) }
68
+ end
69
+
70
+ def extract_via(node)
71
+ via_pair = via_pair(node)
72
+ return %i[get] unless via_pair
73
+
74
+ _, via = *via_pair
75
+
76
+ if via.basic_literal?
77
+ [via.value]
78
+ elsif via.array_type?
79
+ via.values.map(&:value)
80
+ end
81
+ end
82
+
83
+ def via_pair(node)
84
+ node.pairs.find { |p| p.key.value == :via }
85
+ end
86
+
87
+ def http_method?(method)
88
+ HTTP_METHODS.include?(method.to_sym)
89
+ end
90
+
91
+ def replacement(path_node, options_node)
92
+ if path_node.hash_type?
93
+ http_method, options = *http_method_and_options(path_node)
94
+ "#{http_method} #{options.map(&:source).join(', ')}"
95
+ elsif options_node.nil?
96
+ "get #{path_node.source}"
97
+ else
98
+ http_method, options = *http_method_and_options(options_node)
99
+
100
+ if options.any?
101
+ "#{http_method} #{path_node.source}, #{options.map(&:source).join(', ')}"
102
+ else
103
+ "#{http_method} #{path_node.source}"
104
+ end
105
+ end
106
+ end
107
+
108
+ def http_method_and_options(node)
109
+ via_pair = via_pair(node)
110
+ http_method = extract_via(node).first
111
+ rest_pairs = node.pairs - [via_pair]
112
+ [http_method, rest_pairs]
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,39 @@
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
+ # @example
10
+ # # bad
11
+ # !array.include?(2)
12
+ # !hash.include?(:key)
13
+ #
14
+ # # good
15
+ # array.exclude?(2)
16
+ # hash.exclude?(:key)
17
+ #
18
+ class NegateInclude < Cop
19
+ MSG = 'Use `.exclude?` and remove the negation part.'
20
+
21
+ def_node_matcher :negate_include_call?, <<~PATTERN
22
+ (send (send $_ :include? $_) :!)
23
+ PATTERN
24
+
25
+ def on_send(node)
26
+ add_offense(node) if negate_include_call?(node)
27
+ end
28
+
29
+ def autocorrect(node)
30
+ negate_include_call?(node) do |receiver, obj|
31
+ lambda do |corrector|
32
+ corrector.replace(node, "#{receiver.source}.exclude?(#{obj.source})")
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
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.
11
+ #
12
+ # @example
13
+ # # bad
14
+ # Model.pluck(:a).first
15
+ # [{ a: :b, c: :d }].pluck(:a, :b).first
16
+ #
17
+ # # good
18
+ # Model.pick(:a)
19
+ # [{ a: :b, c: :d }].pick(:a, :b)
20
+ class Pick < Cop
21
+ extend TargetRailsVersion
22
+
23
+ MSG = 'Prefer `pick(%<args>s)` over `pluck(%<args>s).first`.'
24
+
25
+ minimum_target_rails_version 6.0
26
+
27
+ def_node_matcher :pick_candidate?, <<~PATTERN
28
+ (send (send _ :pluck ...) :first)
29
+ PATTERN
30
+
31
+ def on_send(node)
32
+ pick_candidate?(node) do
33
+ range = node.receiver.loc.selector.join(node.loc.selector)
34
+ add_offense(node, location: range)
35
+ end
36
+ end
37
+
38
+ def autocorrect(node)
39
+ first_range = node.receiver.source_range.end.join(node.loc.selector)
40
+
41
+ lambda do |corrector|
42
+ corrector.remove(first_range)
43
+ corrector.replace(node.receiver.loc.selector, 'pick')
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def message(node)
50
+ format(MSG, args: node.receiver.arguments.map(&:source).join(', '))
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,59 @@
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 < Cop
21
+ extend TargetRailsVersion
22
+
23
+ MSG = 'Prefer `pluck(:%<value>s)` over `%<method>s { |%<argument>s| %<element>s[:%<value>s] }`.'
24
+
25
+ minimum_target_rails_version 5.0
26
+
27
+ def_node_matcher :pluck_candidate?, <<~PATTERN
28
+ (block (send _ ${:map :collect}) (args (arg $_argument)) (send (lvar $_element) :[] (sym $_value)))
29
+ PATTERN
30
+
31
+ def on_block(node)
32
+ pluck_candidate?(node) do |method, argument, element, value|
33
+ next unless argument == element
34
+
35
+ add_offense(node, location: offense_range(node), message: message(method, argument, element, value))
36
+ end
37
+ end
38
+
39
+ def autocorrect(node)
40
+ _method, _argument, _element, value = pluck_candidate?(node)
41
+
42
+ lambda do |corrector|
43
+ corrector.replace(offense_range(node), "pluck(:#{value})")
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def offense_range(node)
50
+ node.send_node.loc.selector.join(node.loc.end)
51
+ end
52
+
53
+ def message(method, argument, element, value)
54
+ format(MSG, method: method, argument: argument, element: element, value: value)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop enforces the use of `ids` over `pluck(:id)` and `pluck(primary_key)`.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # User.pluck(:id)
11
+ # user.posts.pluck(:id)
12
+ #
13
+ # def self.user_ids
14
+ # pluck(primary_key)
15
+ # end
16
+ #
17
+ # # good
18
+ # User.ids
19
+ # user.posts.ids
20
+ #
21
+ # def self.user_ids
22
+ # ids
23
+ # end
24
+ #
25
+ class PluckId < Cop
26
+ include RangeHelp
27
+ include ActiveRecordHelper
28
+
29
+ MSG = 'Use `ids` instead of `%<bad_method>s`.'
30
+
31
+ def_node_matcher :pluck_id_call?, <<~PATTERN
32
+ (send _ :pluck {(sym :id) (send nil? :primary_key)})
33
+ PATTERN
34
+
35
+ def on_send(node)
36
+ return if !pluck_id_call?(node) || in_where?(node)
37
+
38
+ range = offense_range(node)
39
+ message = format(MSG, bad_method: range.source)
40
+
41
+ add_offense(node, location: range, message: message)
42
+ end
43
+
44
+ def autocorrect(node)
45
+ lambda do |corrector|
46
+ corrector.replace(offense_range(node), 'ids')
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def offense_range(node)
53
+ range_between(node.loc.selector.begin_pos, node.loc.expression.end_pos)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end