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,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,25 @@ 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
47
-
48
49
  table = schema.table_by(name: table_name(klass))
49
- return true unless table # Skip analysis if it can't find the table
50
-
51
50
  names = column_names(node)
52
- return true unless names
53
51
 
52
+ [klass, table, names]
53
+ end
54
+
55
+ def with_index?(klass, table, names)
54
56
  # Compatibility for Rails 4.2.
55
57
  add_indicies = schema.add_indicies_by(table_name: table_name(klass))
56
58
 
@@ -95,6 +97,8 @@ module RuboCop
95
97
  scope = find_scope(uniq)
96
98
  return unless scope
97
99
 
100
+ scope = unfreeze_scope(scope)
101
+
98
102
  case scope.type
99
103
  when :sym, :str
100
104
  [scope.value]
@@ -112,6 +116,10 @@ module RuboCop
112
116
  end
113
117
  end
114
118
 
119
+ def unfreeze_scope(scope)
120
+ scope.send_type? && scope.method?(:freeze) ? scope.children.first : scope
121
+ end
122
+
115
123
  def class_node(node)
116
124
  node.each_ancestor.find(&:class_type?)
117
125
  end
@@ -149,10 +157,6 @@ module RuboCop
149
157
  end
150
158
  end
151
159
  end
152
-
153
- def schema
154
- RuboCop::Rails::SchemaLoader.load(target_ruby_version)
155
- end
156
160
  end
157
161
  end
158
162
  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,106 @@
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 IS NOT NULL')
14
+ # User.where('name NOT IN (?)', ['john', 'jane'])
15
+ # User.where('name NOT IN (:names)', names: ['john', 'jane'])
16
+ #
17
+ # # good
18
+ # User.where.not(name: 'Gabe')
19
+ # User.where.not(name: nil)
20
+ # User.where.not(name: ['john', 'jane'])
21
+ #
22
+ class WhereNot < Cop
23
+ include RangeHelp
24
+
25
+ MSG = 'Use `%<good_method>s` instead of manually constructing negated SQL in `where`.'
26
+
27
+ def_node_matcher :where_method_call?, <<~PATTERN
28
+ {
29
+ (send _ :where (array $str_type? $_ ?))
30
+ (send _ :where $str_type? $_ ?)
31
+ }
32
+ PATTERN
33
+
34
+ def on_send(node)
35
+ where_method_call?(node) do |template_node, value_node|
36
+ value_node = value_node.first
37
+
38
+ range = offense_range(node)
39
+
40
+ column_and_value = extract_column_and_value(template_node, value_node)
41
+ return unless column_and_value
42
+
43
+ good_method = build_good_method(*column_and_value)
44
+ message = format(MSG, good_method: good_method)
45
+
46
+ add_offense(node, location: range, message: message)
47
+ end
48
+ end
49
+
50
+ def autocorrect(node)
51
+ where_method_call?(node) do |template_node, value_node|
52
+ value_node = value_node.first
53
+
54
+ lambda do |corrector|
55
+ range = offense_range(node)
56
+
57
+ column, value = *extract_column_and_value(template_node, value_node)
58
+ replacement = build_good_method(column, value)
59
+
60
+ corrector.replace(range, replacement)
61
+ end
62
+ end
63
+ end
64
+
65
+ NOT_EQ_ANONYMOUS_RE = /\A([\w.]+)\s+!=\s+\?\z/.freeze # column != ?
66
+ NOT_IN_ANONYMOUS_RE = /\A([\w.]+)\s+NOT\s+IN\s+\(\?\)\z/i.freeze # column NOT IN (?)
67
+ NOT_EQ_NAMED_RE = /\A([\w.]+)\s+!=\s+:(\w+)\z/.freeze # column != :column
68
+ NOT_IN_NAMED_RE = /\A([\w.]+)\s+NOT\s+IN\s+\(:(\w+)\)\z/i.freeze # column NOT IN (:column)
69
+ IS_NOT_NULL_RE = /\A([\w.]+)\s+IS\s+NOT\s+NULL\z/i.freeze # column IS NOT NULL
70
+
71
+ private
72
+
73
+ def offense_range(node)
74
+ range_between(node.loc.selector.begin_pos, node.loc.expression.end_pos)
75
+ end
76
+
77
+ def extract_column_and_value(template_node, value_node)
78
+ value =
79
+ case template_node.value
80
+ when NOT_EQ_ANONYMOUS_RE, NOT_IN_ANONYMOUS_RE
81
+ value_node.source
82
+ when NOT_EQ_NAMED_RE, NOT_IN_NAMED_RE
83
+ return unless value_node.hash_type?
84
+
85
+ pair = value_node.pairs.find { |p| p.key.value.to_sym == Regexp.last_match(2).to_sym }
86
+ pair.value.source
87
+ when IS_NOT_NULL_RE
88
+ 'nil'
89
+ else
90
+ return
91
+ end
92
+
93
+ [Regexp.last_match(1), value]
94
+ end
95
+
96
+ def build_good_method(column, value)
97
+ if column.include?('.')
98
+ "where.not('#{column}' => #{value})"
99
+ else
100
+ "where.not(#{column}: #{value})"
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end