rubocop-rails 2.5.1 → 2.8.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +2 -2
  4. data/config/default.yml +192 -11
  5. data/lib/rubocop/cop/mixin/active_record_helper.rb +22 -0
  6. data/lib/rubocop/cop/mixin/index_method.rb +25 -1
  7. data/lib/rubocop/cop/rails/active_record_callbacks_order.rb +143 -0
  8. data/lib/rubocop/cop/rails/after_commit_override.rb +84 -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/exit.rb +2 -2
  15. data/lib/rubocop/cop/rails/file_path.rb +2 -1
  16. data/lib/rubocop/cop/rails/find_by_id.rb +103 -0
  17. data/lib/rubocop/cop/rails/has_many_or_has_one_dependent.rb +1 -5
  18. data/lib/rubocop/cop/rails/helper_instance_variable.rb +2 -0
  19. data/lib/rubocop/cop/rails/http_positional_arguments.rb +1 -1
  20. data/lib/rubocop/cop/rails/http_status.rb +2 -0
  21. data/lib/rubocop/cop/rails/index_by.rb +8 -0
  22. data/lib/rubocop/cop/rails/index_with.rb +8 -0
  23. data/lib/rubocop/cop/rails/inquiry.rb +38 -0
  24. data/lib/rubocop/cop/rails/inverse_of.rb +0 -4
  25. data/lib/rubocop/cop/rails/link_to_blank.rb +3 -3
  26. data/lib/rubocop/cop/rails/mailer_name.rb +80 -0
  27. data/lib/rubocop/cop/rails/match_route.rb +119 -0
  28. data/lib/rubocop/cop/rails/negate_include.rb +39 -0
  29. data/lib/rubocop/cop/rails/order_by_id.rb +53 -0
  30. data/lib/rubocop/cop/rails/pick.rb +55 -0
  31. data/lib/rubocop/cop/rails/pluck.rb +59 -0
  32. data/lib/rubocop/cop/rails/pluck_id.rb +58 -0
  33. data/lib/rubocop/cop/rails/pluck_in_where.rb +70 -0
  34. data/lib/rubocop/cop/rails/presence.rb +2 -6
  35. data/lib/rubocop/cop/rails/rake_environment.rb +17 -0
  36. data/lib/rubocop/cop/rails/redundant_foreign_key.rb +80 -0
  37. data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +0 -3
  38. data/lib/rubocop/cop/rails/reflection_class_name.rb +1 -1
  39. data/lib/rubocop/cop/rails/relative_date_constant.rb +5 -2
  40. data/lib/rubocop/cop/rails/render_inline.rb +40 -0
  41. data/lib/rubocop/cop/rails/render_plain_text.rb +76 -0
  42. data/lib/rubocop/cop/rails/reversible_migration.rb +79 -0
  43. data/lib/rubocop/cop/rails/safe_navigation.rb +1 -1
  44. data/lib/rubocop/cop/rails/save_bang.rb +8 -9
  45. data/lib/rubocop/cop/rails/short_i18n.rb +76 -0
  46. data/lib/rubocop/cop/rails/skips_model_validations.rb +46 -8
  47. data/lib/rubocop/cop/rails/squished_sql_heredocs.rb +83 -0
  48. data/lib/rubocop/cop/rails/time_zone.rb +1 -3
  49. data/lib/rubocop/cop/rails/uniq_before_pluck.rb +14 -12
  50. data/lib/rubocop/cop/rails/unique_validation_without_index.rb +15 -11
  51. data/lib/rubocop/cop/rails/unknown_env.rb +18 -6
  52. data/lib/rubocop/cop/rails/where_exists.rb +131 -0
  53. data/lib/rubocop/cop/rails/where_not.rb +106 -0
  54. data/lib/rubocop/cop/rails_cops.rb +21 -0
  55. data/lib/rubocop/rails/schema_loader.rb +10 -10
  56. data/lib/rubocop/rails/schema_loader/schema.rb +4 -4
  57. data/lib/rubocop/rails/version.rb +1 -1
  58. metadata +31 -10
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop checks that Active Record callbacks are declared
7
+ # in the order in which they will be executed.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # class Person < ApplicationRecord
12
+ # after_commit :after_commit_callback
13
+ # before_validation :before_validation_callback
14
+ # end
15
+ #
16
+ # # good
17
+ # class Person < ApplicationRecord
18
+ # before_validation :before_validation_callback
19
+ # after_commit :after_commit_callback
20
+ # end
21
+ #
22
+ class ActiveRecordCallbacksOrder < Cop
23
+ MSG = '`%<current>s` is supposed to appear before `%<previous>s`.'
24
+
25
+ CALLBACKS_IN_ORDER = %i[
26
+ after_initialize
27
+ before_validation
28
+ after_validation
29
+ before_save
30
+ around_save
31
+ before_create
32
+ around_create
33
+ after_create
34
+ before_update
35
+ around_update
36
+ after_update
37
+ before_destroy
38
+ around_destroy
39
+ after_destroy
40
+ after_save
41
+ after_commit
42
+ after_rollback
43
+ after_find
44
+ after_touch
45
+ ].freeze
46
+
47
+ CALLBACKS_ORDER_MAP = CALLBACKS_IN_ORDER.each_with_index.to_h.freeze
48
+
49
+ def on_class(class_node)
50
+ previous_index = -1
51
+ previous_callback = nil
52
+
53
+ defined_callbacks(class_node).each do |node|
54
+ callback = node.method_name
55
+ index = CALLBACKS_ORDER_MAP[callback]
56
+
57
+ if index < previous_index
58
+ message = format(MSG, current: callback,
59
+ previous: previous_callback)
60
+ add_offense(node, message: message)
61
+ end
62
+ previous_index = index
63
+ previous_callback = callback
64
+ end
65
+ end
66
+
67
+ # Autocorrect by swapping between two nodes autocorrecting them
68
+ def autocorrect(node)
69
+ previous = left_siblings_of(node).reverse_each.find do |sibling|
70
+ callback?(sibling)
71
+ end
72
+
73
+ current_range = source_range_with_comment(node)
74
+ previous_range = source_range_with_comment(previous)
75
+
76
+ lambda do |corrector|
77
+ corrector.insert_before(previous_range, current_range.source)
78
+ corrector.remove(current_range)
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def defined_callbacks(class_node)
85
+ class_def = class_node.body
86
+
87
+ if class_def
88
+ class_def.each_child_node.select { |c| callback?(c) }
89
+ else
90
+ []
91
+ end
92
+ end
93
+
94
+ def callback?(node)
95
+ node.send_type? && CALLBACKS_ORDER_MAP.key?(node.method_name)
96
+ end
97
+
98
+ def left_siblings_of(node)
99
+ siblings_of(node)[0, node.sibling_index]
100
+ end
101
+
102
+ def siblings_of(node)
103
+ node.parent.children
104
+ end
105
+
106
+ def source_range_with_comment(node)
107
+ begin_pos = begin_pos_with_comment(node)
108
+ end_pos = end_position_for(node)
109
+
110
+ Parser::Source::Range.new(buffer, begin_pos, end_pos)
111
+ end
112
+
113
+ def end_position_for(node)
114
+ end_line = buffer.line_for_position(node.loc.expression.end_pos)
115
+ buffer.line_range(end_line).end_pos
116
+ end
117
+
118
+ def begin_pos_with_comment(node)
119
+ annotation_line = node.first_line - 1
120
+ first_comment = nil
121
+
122
+ processed_source.comments_before_line(annotation_line)
123
+ .reverse_each do |comment|
124
+ if comment.location.line == annotation_line
125
+ first_comment = comment
126
+ annotation_line -= 1
127
+ end
128
+ end
129
+
130
+ start_line_position(first_comment || node)
131
+ end
132
+
133
+ def start_line_position(node)
134
+ buffer.line_range(node.loc.line).begin_pos - 1
135
+ end
136
+
137
+ def buffer
138
+ processed_source.buffer
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop enforces that there is only one call to `after_commit`
7
+ # (and its aliases - `after_create_commit`, `after_update_commit`,
8
+ # and `after_destroy_commit`) with the same callback name per model.
9
+ #
10
+ # @example
11
+ # # bad
12
+ # # This won't be triggered.
13
+ # after_create_commit :log_action
14
+ #
15
+ # # This will override the callback added by
16
+ # # after_create_commit.
17
+ # after_update_commit :log_action
18
+ #
19
+ # # bad
20
+ # # This won't be triggered.
21
+ # after_commit :log_action, on: :create
22
+ # # This won't be triggered.
23
+ # after_update_commit :log_action
24
+ # # This will override both previous callbacks.
25
+ # after_commit :log_action, on: :destroy
26
+ #
27
+ # # good
28
+ # after_save_commit :log_action
29
+ #
30
+ # # good
31
+ # after_create_commit :log_create_action
32
+ # after_update_commit :log_update_action
33
+ #
34
+ class AfterCommitOverride < Cop
35
+ MSG = 'There can only be one `after_*_commit :%<name>s` hook defined for a model.'
36
+
37
+ AFTER_COMMIT_CALLBACKS = %i[
38
+ after_commit
39
+ after_create_commit
40
+ after_update_commit
41
+ after_save_commit
42
+ after_destroy_commit
43
+ ].freeze
44
+
45
+ def on_class(class_node)
46
+ seen_callback_names = {}
47
+
48
+ each_after_commit_callback(class_node) do |node|
49
+ callback_name = node.arguments[0].value
50
+ if seen_callback_names.key?(callback_name)
51
+ add_offense(node, message: format(MSG, name: callback_name))
52
+ else
53
+ seen_callback_names[callback_name] = true
54
+ end
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def each_after_commit_callback(class_node)
61
+ class_send_nodes(class_node).each do |node|
62
+ yield node if after_commit_callback?(node)
63
+ end
64
+ end
65
+
66
+ def class_send_nodes(class_node)
67
+ class_def = class_node.body
68
+
69
+ return [] unless class_def
70
+
71
+ if class_def.send_type?
72
+ [class_def]
73
+ else
74
+ class_def.each_child_node(:send).to_a
75
+ end
76
+ end
77
+
78
+ def after_commit_callback?(node)
79
+ AFTER_COMMIT_CALLBACKS.include?(node.method_name)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop checks that `tag` is used instead of `content_tag`
7
+ # because `content_tag` is legacy syntax.
8
+ #
9
+ # NOTE: Allow `content_tag` when the first argument is a variable because
10
+ # `content_tag(name)` is simpler rather than `tag.public_send(name)`.
11
+ #
12
+ # @example
13
+ # # bad
14
+ # content_tag(:p, 'Hello world!')
15
+ # content_tag(:br)
16
+ #
17
+ # # good
18
+ # tag.p('Hello world!')
19
+ # tag.br
20
+ # content_tag(name, 'Hello world!')
21
+ class ContentTag < Cop
22
+ include RangeHelp
23
+ extend TargetRailsVersion
24
+
25
+ minimum_target_rails_version 5.1
26
+
27
+ MSG = 'Use `tag` instead of `content_tag`.'
28
+
29
+ def on_send(node)
30
+ return unless node.method?(:content_tag)
31
+
32
+ first_argument = node.first_argument
33
+ return unless first_argument
34
+
35
+ return if first_argument.variable? || first_argument.send_type? || first_argument.const_type?
36
+
37
+ add_offense(node)
38
+ end
39
+
40
+ def autocorrect(node)
41
+ lambda do |corrector|
42
+ if method_name?(node.first_argument)
43
+ range = correction_range(node)
44
+
45
+ rest_args = node.arguments.drop(1)
46
+ replacement = "tag.#{node.first_argument.value.to_s.underscore}(#{rest_args.map(&:source).join(', ')})"
47
+
48
+ corrector.replace(range, replacement)
49
+ else
50
+ corrector.replace(node.loc.selector, 'tag')
51
+ end
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def method_name?(node)
58
+ return false unless node.str_type? || node.sym_type?
59
+
60
+ /^[a-zA-Z_][a-zA-Z_\-0-9]*$/.match?(node.value)
61
+ end
62
+
63
+ def correction_range(node)
64
+ range_between(node.loc.selector.begin_pos, node.loc.expression.end_pos)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -70,9 +70,7 @@ module RuboCop
70
70
  parent = node.parent
71
71
 
72
72
  if create_table_with_block?(parent)
73
- if parent.body.nil? || !time_columns_included?(parent.body)
74
- add_offense(parent)
75
- end
73
+ add_offense(parent) if parent.body.nil? || !time_columns_included?(parent.body)
76
74
  elsif create_table_with_timestamps_proc?(node)
77
75
  # nothing to do
78
76
  else
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop looks for uses of `default_scope`.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # default_scope -> { where(hidden: false) }
11
+ #
12
+ # # good
13
+ # scope :published, -> { where(hidden: false) }
14
+ #
15
+ # # bad
16
+ # def self.default_scope
17
+ # where(hidden: false)
18
+ # end
19
+ #
20
+ # # good
21
+ # def self.published
22
+ # where(hidden: false)
23
+ # end
24
+ #
25
+ class DefaultScope < Cop
26
+ MSG = 'Avoid use of `default_scope`. It is better to use explicitly named scopes.'
27
+
28
+ def_node_matcher :method_call?, <<~PATTERN
29
+ (send nil? :default_scope ...)
30
+ PATTERN
31
+
32
+ def_node_matcher :class_method_definition?, <<~PATTERN
33
+ (defs _ :default_scope args ...)
34
+ PATTERN
35
+
36
+ def_node_matcher :eigenclass_method_definition?, <<~PATTERN
37
+ (sclass (self) $(def :default_scope args ...))
38
+ PATTERN
39
+
40
+ def on_send(node)
41
+ add_offense(node, location: :selector) if method_call?(node)
42
+ end
43
+
44
+ def on_defs(node)
45
+ add_offense(node, location: :name) if class_method_definition?(node)
46
+ end
47
+
48
+ def on_sclass(node)
49
+ eigenclass_method_definition?(node) { |default_scope| add_offense(default_scope, location: :name) }
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -71,9 +71,7 @@ module RuboCop
71
71
  delegation = ["delegate :#{node.body.method_name}",
72
72
  "to: :#{node.body.receiver.method_name}"]
73
73
 
74
- if node.method?(prefixed_method_name(node.body))
75
- delegation << ['prefix: true']
76
- end
74
+ delegation << ['prefix: true'] if node.method?(prefixed_method_name(node.body))
77
75
 
78
76
  lambda do |corrector|
79
77
  corrector.replace(node.source_range, delegation.join(', '))
@@ -124,7 +122,7 @@ module RuboCop
124
122
  end
125
123
 
126
124
  def private_or_protected_inline(line)
127
- processed_source[line - 1].strip =~ /\A(private )|(protected )/
125
+ processed_source[line - 1].strip.match?(/\A(private )|(protected )/)
128
126
  end
129
127
  end
130
128
  end
@@ -10,37 +10,41 @@ module RuboCop
10
10
  # @example
11
11
  # # bad
12
12
  # User.find_by_name(name)
13
- #
14
- # # bad
15
13
  # User.find_by_name_and_email(name)
16
- #
17
- # # bad
18
14
  # User.find_by_email!(name)
19
15
  #
20
16
  # # good
21
17
  # User.find_by(name: name)
18
+ # User.find_by(name: name, email: email)
19
+ # User.find_by!(email: email)
20
+ #
21
+ # @example AllowedMethods: find_by_sql
22
+ # # bad
23
+ # User.find_by_query(users_query)
22
24
  #
23
25
  # # good
24
- # User.find_by(name: name, email: email)
26
+ # User.find_by_sql(users_sql)
27
+ #
28
+ # @example AllowedReceivers: Gem::Specification
29
+ # # bad
30
+ # Specification.find_by_name('backend').gem_dir
25
31
  #
26
32
  # # good
27
- # User.find_by!(email: email)
33
+ # Gem::Specification.find_by_name('backend').gem_dir
28
34
  class DynamicFindBy < Cop
29
35
  MSG = 'Use `%<static_name>s` instead of dynamic `%<method>s`.'
30
36
  METHOD_PATTERN = /^find_by_(.+?)(!)?$/.freeze
31
37
 
32
38
  def on_send(node)
33
- method_name = node.method_name.to_s
34
-
35
- return if whitelist.include?(method_name)
39
+ return if allowed_invocation?(node)
36
40
 
41
+ method_name = node.method_name
37
42
  static_name = static_method_name(method_name)
38
-
39
43
  return unless static_name
40
44
 
41
45
  add_offense(node,
42
46
  message: format(MSG, static_name: static_name,
43
- method: node.method_name))
47
+ method: method_name))
44
48
  end
45
49
  alias on_csend on_send
46
50
 
@@ -57,6 +61,31 @@ module RuboCop
57
61
 
58
62
  private
59
63
 
64
+ def allowed_invocation?(node)
65
+ allowed_method?(node) || allowed_receiver?(node) ||
66
+ whitelisted?(node)
67
+ end
68
+
69
+ def allowed_method?(node)
70
+ return unless cop_config['AllowedMethods']
71
+
72
+ cop_config['AllowedMethods'].include?(node.method_name.to_s)
73
+ end
74
+
75
+ def allowed_receiver?(node)
76
+ return unless cop_config['AllowedReceivers'] && node.receiver
77
+
78
+ cop_config['AllowedReceivers'].include?(node.receiver.source)
79
+ end
80
+
81
+ # config option `WhiteList` will be deprecated soon
82
+ def whitelisted?(node)
83
+ whitelist_config = cop_config['Whitelist']
84
+ return unless whitelist_config
85
+
86
+ whitelist_config.include?(node.method_name.to_s)
87
+ end
88
+
60
89
  def autocorrect_method_name(corrector, node)
61
90
  corrector.replace(node.loc.selector,
62
91
  static_method_name(node.method_name.to_s))
@@ -68,10 +97,6 @@ module RuboCop
68
97
  end
69
98
  end
70
99
 
71
- def whitelist
72
- cop_config['Whitelist']
73
- end
74
-
75
100
  def column_keywords(method)
76
101
  keyword_string = method.to_s[METHOD_PATTERN, 1]
77
102
  keyword_string.split('_and_').map { |keyword| "#{keyword}: " }