rubocop-rails 2.5.2 → 2.8.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.
Files changed (57) 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 +9 -3
  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 +91 -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 +41 -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 +7 -1
  20. data/lib/rubocop/cop/rails/http_status.rb +2 -0
  21. data/lib/rubocop/cop/rails/index_by.rb +9 -1
  22. data/lib/rubocop/cop/rails/index_with.rb +9 -1
  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 +80 -1
  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 +16 -6
  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 +108 -0
  54. data/lib/rubocop/cop/rails_cops.rb +21 -0
  55. data/lib/rubocop/rails/schema_loader/schema.rb +4 -4
  56. data/lib/rubocop/rails/version.rb +1 -1
  57. metadata +31 -10
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ #
7
+ # Checks SQL heredocs to use `.squish`.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # <<-SQL
12
+ # SELECT * FROM posts;
13
+ # SQL
14
+ #
15
+ # <<-SQL
16
+ # SELECT * FROM posts
17
+ # WHERE id = 1
18
+ # SQL
19
+ #
20
+ # execute(<<~SQL, "Post Load")
21
+ # SELECT * FROM posts
22
+ # WHERE post_id = 1
23
+ # SQL
24
+ #
25
+ # # good
26
+ # <<-SQL.squish
27
+ # SELECT * FROM posts;
28
+ # SQL
29
+ #
30
+ # <<~SQL.squish
31
+ # SELECT * FROM table
32
+ # WHERE id = 1
33
+ # SQL
34
+ #
35
+ # execute(<<~SQL.squish, "Post Load")
36
+ # SELECT * FROM posts
37
+ # WHERE post_id = 1
38
+ # SQL
39
+ #
40
+ class SquishedSQLHeredocs < Cop
41
+ include Heredoc
42
+
43
+ SQL = 'SQL'
44
+ SQUISH = '.squish'
45
+ MSG = 'Use `%<expect>s` instead of `%<current>s`.'
46
+
47
+ def on_heredoc(node)
48
+ return unless offense_detected?(node)
49
+
50
+ add_offense(node)
51
+ end
52
+
53
+ def autocorrect(node)
54
+ lambda do |corrector|
55
+ corrector.insert_after(node, SQUISH)
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def offense_detected?(node)
62
+ sql_heredoc?(node) && !using_squish?(node)
63
+ end
64
+
65
+ def sql_heredoc?(node)
66
+ delimiter_string(node) == SQL
67
+ end
68
+
69
+ def using_squish?(node)
70
+ node.parent&.send_type? && node.parent&.method?(:squish)
71
+ end
72
+
73
+ def message(node)
74
+ format(
75
+ MSG,
76
+ expect: "#{node.source}#{SQUISH}",
77
+ current: node.source
78
+ )
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -85,9 +85,7 @@ module RuboCop
85
85
  end
86
86
 
87
87
  # prefer `Time` over `DateTime` class
88
- if strict?
89
- corrector.replace(node.children.first.source_range, 'Time')
90
- end
88
+ corrector.replace(node.children.first.source_range, 'Time') if strict?
91
89
  remove_redundant_in_time_zone(corrector, node)
92
90
  end
93
91
  end
@@ -3,20 +3,23 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Rails
6
- # Prefer the use of uniq (or distinct), before pluck instead of after.
6
+ # Prefer the use of distinct, before pluck instead of after.
7
7
  #
8
- # The use of uniq before pluck is preferred because it executes within
8
+ # The use of distinct before pluck is preferred because it executes within
9
9
  # the database.
10
10
  #
11
11
  # This cop has two different enforcement modes. When the EnforcedStyle
12
12
  # is conservative (the default) then only calls to pluck on a constant
13
- # (i.e. a model class) before uniq are added as offenses.
13
+ # (i.e. a model class) before distinct are added as offenses.
14
14
  #
15
15
  # When the EnforcedStyle is aggressive then all calls to pluck before
16
- # uniq are added as offenses. This may lead to false positives as the cop
17
- # cannot distinguish between calls to pluck on an ActiveRecord::Relation
18
- # vs a call to pluck on an ActiveRecord::Associations::CollectionProxy.
16
+ # distinct are added as offenses. This may lead to false positives
17
+ # as the cop cannot distinguish between calls to pluck on an
18
+ # ActiveRecord::Relation vs a call to pluck on an
19
+ # ActiveRecord::Associations::CollectionProxy.
19
20
  #
21
+ # This cop is unsafe because the behavior may change depending on the
22
+ # database collation.
20
23
  # Autocorrect is disabled by default for this cop since it may generate
21
24
  # false positives.
22
25
  #
@@ -25,7 +28,7 @@ module RuboCop
25
28
  # Model.pluck(:id).uniq
26
29
  #
27
30
  # # good
28
- # Model.uniq.pluck(:id)
31
+ # Model.distinct.pluck(:id)
29
32
  #
30
33
  # @example EnforcedStyle: aggressive
31
34
  # # bad
@@ -40,13 +43,13 @@ module RuboCop
40
43
  # Model.pluck(:id).uniq
41
44
  #
42
45
  # # good
43
- # Model.uniq.pluck(:id)
46
+ # Model.distinct.pluck(:id)
44
47
  #
45
48
  class UniqBeforePluck < RuboCop::Cop::Cop
46
49
  include ConfigurableEnforcedStyle
47
50
  include RangeHelp
48
51
 
49
- MSG = 'Use `%<method>s` before `pluck`.'
52
+ MSG = 'Use `distinct` before `pluck`.'
50
53
  NEWLINE = "\n"
51
54
  PATTERN = '[!^block (send (send %<type>s :pluck ...) ' \
52
55
  '${:uniq :distinct} ...)]'
@@ -66,8 +69,7 @@ module RuboCop
66
69
 
67
70
  return unless method
68
71
 
69
- add_offense(node, location: :selector,
70
- message: format(MSG, method: method))
72
+ add_offense(node, location: :selector)
71
73
  end
72
74
 
73
75
  def autocorrect(node)
@@ -75,7 +77,7 @@ module RuboCop
75
77
  method = node.method_name
76
78
 
77
79
  corrector.remove(dot_method_with_whitespace(method, node))
78
- corrector.insert_before(node.receiver.loc.dot.begin, ".#{method}")
80
+ corrector.insert_before(node.receiver.loc.dot.begin, '.distinct')
79
81
  end
80
82
  end
81
83
 
@@ -34,23 +34,27 @@ module RuboCop
34
34
  return unless uniqueness_part(node)
35
35
  return if condition_part?(node)
36
36
  return unless schema
37
- return if with_index?(node)
37
+
38
+ klass, table, names = find_schema_information(node)
39
+ return unless names
40
+ return if with_index?(klass, table, names)
38
41
 
39
42
  add_offense(node)
40
43
  end
41
44
 
42
45
  private
43
46
 
44
- def with_index?(node)
47
+ def find_schema_information(node)
45
48
  klass = class_node(node)
46
- return true unless klass # Skip analysis
49
+ return unless klass
47
50
 
48
51
  table = schema.table_by(name: table_name(klass))
49
- return true unless table # Skip analysis if it can't find the table
50
-
51
52
  names = column_names(node)
52
- return true unless names
53
53
 
54
+ [klass, table, names]
55
+ end
56
+
57
+ def with_index?(klass, table, names)
54
58
  # Compatibility for Rails 4.2.
55
59
  add_indicies = schema.add_indicies_by(table_name: table_name(klass))
56
60
 
@@ -95,6 +99,8 @@ module RuboCop
95
99
  scope = find_scope(uniq)
96
100
  return unless scope
97
101
 
102
+ scope = unfreeze_scope(scope)
103
+
98
104
  case scope.type
99
105
  when :sym, :str
100
106
  [scope.value]
@@ -112,6 +118,10 @@ module RuboCop
112
118
  end
113
119
  end
114
120
 
121
+ def unfreeze_scope(scope)
122
+ scope.send_type? && scope.method?(:freeze) ? scope.children.first : scope
123
+ end
124
+
115
125
  def class_node(node)
116
126
  node.each_ancestor.find(&:class_type?)
117
127
  end
@@ -5,6 +5,9 @@ module RuboCop
5
5
  module Rails
6
6
  # This cop checks that environments called with `Rails.env` predicates
7
7
  # exist.
8
+ # By default the cop allows three environments which Rails ships with:
9
+ # `development`, `test`, and `production`.
10
+ # More can be added to the `Environments` config parameter.
8
11
  #
9
12
  # @example
10
13
  # # bad
@@ -15,8 +18,6 @@ module RuboCop
15
18
  # Rails.env.production?
16
19
  # Rails.env == 'production'
17
20
  class UnknownEnv < Cop
18
- include NameSimilarity
19
-
20
21
  MSG = 'Unknown environment `%<name>s`.'
21
22
  MSG_SIMILAR = 'Unknown environment `%<name>s`. ' \
22
23
  'Did you mean `%<similar>s`?'
@@ -57,11 +58,22 @@ module RuboCop
57
58
 
58
59
  def message(name)
59
60
  name = name.to_s.chomp('?')
60
- similar = find_similar_name(name, [])
61
- if similar
62
- format(MSG_SIMILAR, name: name, similar: similar)
63
- else
61
+
62
+ # DidYouMean::SpellChecker is not available in all versions of Ruby,
63
+ # and even on versions where it *is* available (>= 2.3), it is not
64
+ # always required correctly. So we do a feature check first. See:
65
+ # https://github.com/rubocop-hq/rubocop/issues/7979
66
+ similar_names = if defined?(DidYouMean::SpellChecker)
67
+ spell_checker = DidYouMean::SpellChecker.new(dictionary: environments)
68
+ spell_checker.correct(name)
69
+ else
70
+ []
71
+ end
72
+
73
+ if similar_names.empty?
64
74
  format(MSG, name: name)
75
+ else
76
+ format(MSG_SIMILAR, name: name, similar: similar_names.join(', '))
65
77
  end
66
78
  end
67
79
 
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop enforces consistent style when using `exists?`.
7
+ #
8
+ # Two styles are supported for this cop. When EnforcedStyle is 'exists'
9
+ # then the cop enforces `exists?(...)` over `where(...).exists?`.
10
+ #
11
+ # When EnforcedStyle is 'where' then the cop enforces
12
+ # `where(...).exists?` over `exists?(...)`.
13
+ #
14
+ # @example EnforcedStyle: exists (default)
15
+ # # bad
16
+ # User.where(name: 'john').exists?
17
+ # User.where(['name = ?', 'john']).exists?
18
+ # User.where('name = ?', 'john').exists?
19
+ # user.posts.where(published: true).exists?
20
+ #
21
+ # # good
22
+ # User.exists?(name: 'john')
23
+ # User.where('length(name) > 10').exists?
24
+ # user.posts.exists?(published: true)
25
+ #
26
+ # @example EnforcedStyle: where
27
+ # # bad
28
+ # User.exists?(name: 'john')
29
+ # User.exists?(['name = ?', 'john'])
30
+ # User.exists?('name = ?', 'john')
31
+ # user.posts.exists?(published: true)
32
+ #
33
+ # # good
34
+ # User.where(name: 'john').exists?
35
+ # User.where(['name = ?', 'john']).exists?
36
+ # User.where('name = ?', 'john').exists?
37
+ # user.posts.where(published: true).exists?
38
+ # User.where('length(name) > 10').exists?
39
+ class WhereExists < Cop
40
+ include ConfigurableEnforcedStyle
41
+
42
+ MSG = 'Prefer `%<good_method>s` over `%<bad_method>s`.'
43
+
44
+ def_node_matcher :where_exists_call?, <<~PATTERN
45
+ (send (send _ :where $...) :exists?)
46
+ PATTERN
47
+
48
+ def_node_matcher :exists_with_args?, <<~PATTERN
49
+ (send _ :exists? $...)
50
+ PATTERN
51
+
52
+ def on_send(node)
53
+ find_offenses(node) do |args|
54
+ return unless convertable_args?(args)
55
+
56
+ range = correction_range(node)
57
+ message = format(MSG, good_method: build_good_method(args), bad_method: range.source)
58
+ add_offense(node, location: range, message: message)
59
+ end
60
+ end
61
+
62
+ def autocorrect(node)
63
+ args = find_offenses(node)
64
+
65
+ lambda do |corrector|
66
+ corrector.replace(
67
+ correction_range(node),
68
+ build_good_method(args)
69
+ )
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def where_style?
76
+ style == :where
77
+ end
78
+
79
+ def exists_style?
80
+ style == :exists
81
+ end
82
+
83
+ def find_offenses(node, &block)
84
+ if exists_style?
85
+ where_exists_call?(node, &block)
86
+ elsif where_style?
87
+ exists_with_args?(node, &block)
88
+ end
89
+ end
90
+
91
+ def convertable_args?(args)
92
+ return false if args.empty?
93
+
94
+ args.size > 1 || args[0].hash_type? || args[0].array_type?
95
+ end
96
+
97
+ def correction_range(node)
98
+ if exists_style?
99
+ node.receiver.loc.selector.join(node.loc.selector)
100
+ elsif where_style?
101
+ node.loc.selector.with(end_pos: node.loc.expression.end_pos)
102
+ end
103
+ end
104
+
105
+ def build_good_method(args)
106
+ if exists_style?
107
+ build_good_method_exists(args)
108
+ elsif where_style?
109
+ build_good_method_where(args)
110
+ end
111
+ end
112
+
113
+ def build_good_method_exists(args)
114
+ if args.size > 1
115
+ "exists?([#{args.map(&:source).join(', ')}])"
116
+ else
117
+ "exists?(#{args[0].source})"
118
+ end
119
+ end
120
+
121
+ def build_good_method_where(args)
122
+ if args.size > 1
123
+ "where(#{args.map(&:source).join(', ')}).exists?"
124
+ else
125
+ "where(#{args[0].source}).exists?"
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop identifies places where manually constructed SQL
7
+ # in `where` can be replaced with `where.not(...)`.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # User.where('name != ?', 'Gabe')
12
+ # User.where('name != :name', name: 'Gabe')
13
+ # User.where('name <> ?', 'Gabe')
14
+ # User.where('name <> :name', name: 'Gabe')
15
+ # User.where('name IS NOT NULL')
16
+ # User.where('name NOT IN (?)', ['john', 'jane'])
17
+ # User.where('name NOT IN (:names)', names: ['john', 'jane'])
18
+ #
19
+ # # good
20
+ # User.where.not(name: 'Gabe')
21
+ # User.where.not(name: nil)
22
+ # User.where.not(name: ['john', 'jane'])
23
+ #
24
+ class WhereNot < Cop
25
+ include RangeHelp
26
+
27
+ MSG = 'Use `%<good_method>s` instead of manually constructing negated SQL in `where`.'
28
+
29
+ def_node_matcher :where_method_call?, <<~PATTERN
30
+ {
31
+ (send _ :where (array $str_type? $_ ?))
32
+ (send _ :where $str_type? $_ ?)
33
+ }
34
+ PATTERN
35
+
36
+ def on_send(node)
37
+ where_method_call?(node) do |template_node, value_node|
38
+ value_node = value_node.first
39
+
40
+ range = offense_range(node)
41
+
42
+ column_and_value = extract_column_and_value(template_node, value_node)
43
+ return unless column_and_value
44
+
45
+ good_method = build_good_method(*column_and_value)
46
+ message = format(MSG, good_method: good_method)
47
+
48
+ add_offense(node, location: range, message: message)
49
+ end
50
+ end
51
+
52
+ def autocorrect(node)
53
+ where_method_call?(node) do |template_node, value_node|
54
+ value_node = value_node.first
55
+
56
+ lambda do |corrector|
57
+ range = offense_range(node)
58
+
59
+ column, value = *extract_column_and_value(template_node, value_node)
60
+ replacement = build_good_method(column, value)
61
+
62
+ corrector.replace(range, replacement)
63
+ end
64
+ end
65
+ end
66
+
67
+ NOT_EQ_ANONYMOUS_RE = /\A([\w.]+)\s+(?:!=|<>)\s+\?\z/.freeze # column != ?, column <> ?
68
+ NOT_IN_ANONYMOUS_RE = /\A([\w.]+)\s+NOT\s+IN\s+\(\?\)\z/i.freeze # column NOT IN (?)
69
+ NOT_EQ_NAMED_RE = /\A([\w.]+)\s+(?:!=|<>)\s+:(\w+)\z/.freeze # column != :column, column <> :column
70
+ NOT_IN_NAMED_RE = /\A([\w.]+)\s+NOT\s+IN\s+\(:(\w+)\)\z/i.freeze # column NOT IN (:column)
71
+ IS_NOT_NULL_RE = /\A([\w.]+)\s+IS\s+NOT\s+NULL\z/i.freeze # column IS NOT NULL
72
+
73
+ private
74
+
75
+ def offense_range(node)
76
+ range_between(node.loc.selector.begin_pos, node.loc.expression.end_pos)
77
+ end
78
+
79
+ def extract_column_and_value(template_node, value_node)
80
+ value =
81
+ case template_node.value
82
+ when NOT_EQ_ANONYMOUS_RE, NOT_IN_ANONYMOUS_RE
83
+ value_node.source
84
+ when NOT_EQ_NAMED_RE, NOT_IN_NAMED_RE
85
+ return unless value_node.hash_type?
86
+
87
+ pair = value_node.pairs.find { |p| p.key.value.to_sym == Regexp.last_match(2).to_sym }
88
+ pair.value.source
89
+ when IS_NOT_NULL_RE
90
+ 'nil'
91
+ else
92
+ return
93
+ end
94
+
95
+ [Regexp.last_match(1), value]
96
+ end
97
+
98
+ def build_good_method(column, value)
99
+ if column.include?('.')
100
+ "where.not('#{column}' => #{value})"
101
+ else
102
+ "where.not(#{column}: #{value})"
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end