rubocop-rails 2.5.2 → 2.8.1

Sign up to get free protection for your applications and to get access to all the features.
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