rubocop-rails 2.4.1 → 2.6.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +6 -2
  4. data/config/default.yml +71 -6
  5. data/lib/rubocop-rails.rb +3 -0
  6. data/lib/rubocop/cop/mixin/active_record_helper.rb +77 -0
  7. data/lib/rubocop/cop/mixin/index_method.rb +161 -0
  8. data/lib/rubocop/cop/rails/content_tag.rb +82 -0
  9. data/lib/rubocop/cop/rails/create_table_with_timestamps.rb +1 -3
  10. data/lib/rubocop/cop/rails/delegate.rb +1 -3
  11. data/lib/rubocop/cop/rails/dynamic_find_by.rb +40 -15
  12. data/lib/rubocop/cop/rails/environment_comparison.rb +60 -14
  13. data/lib/rubocop/cop/rails/exit.rb +2 -2
  14. data/lib/rubocop/cop/rails/file_path.rb +1 -0
  15. data/lib/rubocop/cop/rails/http_positional_arguments.rb +2 -2
  16. data/lib/rubocop/cop/rails/http_status.rb +2 -0
  17. data/lib/rubocop/cop/rails/index_by.rb +56 -0
  18. data/lib/rubocop/cop/rails/index_with.rb +59 -0
  19. data/lib/rubocop/cop/rails/inverse_of.rb +0 -4
  20. data/lib/rubocop/cop/rails/link_to_blank.rb +1 -3
  21. data/lib/rubocop/cop/rails/pick.rb +51 -0
  22. data/lib/rubocop/cop/rails/presence.rb +2 -6
  23. data/lib/rubocop/cop/rails/rake_environment.rb +24 -6
  24. data/lib/rubocop/cop/rails/redundant_foreign_key.rb +80 -0
  25. data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +0 -3
  26. data/lib/rubocop/cop/rails/refute_methods.rb +52 -26
  27. data/lib/rubocop/cop/rails/reversible_migration.rb +6 -1
  28. data/lib/rubocop/cop/rails/save_bang.rb +16 -9
  29. data/lib/rubocop/cop/rails/skips_model_validations.rb +4 -1
  30. data/lib/rubocop/cop/rails/time_zone.rb +1 -3
  31. data/lib/rubocop/cop/rails/uniq_before_pluck.rb +16 -16
  32. data/lib/rubocop/cop/rails/unique_validation_without_index.rb +155 -0
  33. data/lib/rubocop/cop/rails/unknown_env.rb +7 -6
  34. data/lib/rubocop/cop/rails_cops.rb +8 -0
  35. data/lib/rubocop/rails/inject.rb +1 -1
  36. data/lib/rubocop/rails/schema_loader.rb +61 -0
  37. data/lib/rubocop/rails/schema_loader/schema.rb +190 -0
  38. data/lib/rubocop/rails/version.rb +1 -1
  39. metadata +32 -8
@@ -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,50 +3,51 @@
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
  #
20
21
  # Autocorrect is disabled by default for this cop since it may generate
21
22
  # false positives.
22
23
  #
23
24
  # @example EnforcedStyle: conservative (default)
24
25
  # # bad
25
- # Model.pluck(:id).uniq
26
+ # Model.pluck(:id).distinct
26
27
  #
27
28
  # # good
28
- # Model.uniq.pluck(:id)
29
+ # Model.distinct.pluck(:id)
29
30
  #
30
31
  # @example EnforcedStyle: aggressive
31
32
  # # bad
32
33
  # # this will return a Relation that pluck is called on
33
- # Model.where(cond: true).pluck(:id).uniq
34
+ # Model.where(cond: true).pluck(:id).distinct
34
35
  #
35
36
  # # bad
36
37
  # # an association on an instance will return a CollectionProxy
37
- # instance.assoc.pluck(:id).uniq
38
+ # instance.assoc.pluck(:id).distinct
38
39
  #
39
40
  # # bad
40
- # Model.pluck(:id).uniq
41
+ # Model.pluck(:id).distinct
41
42
  #
42
43
  # # good
43
- # Model.uniq.pluck(:id)
44
+ # Model.distinct.pluck(:id)
44
45
  #
45
46
  class UniqBeforePluck < RuboCop::Cop::Cop
46
47
  include ConfigurableEnforcedStyle
47
48
  include RangeHelp
48
49
 
49
- MSG = 'Use `%<method>s` before `pluck`.'
50
+ MSG = 'Use `distinct` before `pluck`.'
50
51
  NEWLINE = "\n"
51
52
  PATTERN = '[!^block (send (send %<type>s :pluck ...) ' \
52
53
  '${:uniq :distinct} ...)]'
@@ -66,8 +67,7 @@ module RuboCop
66
67
 
67
68
  return unless method
68
69
 
69
- add_offense(node, location: :selector,
70
- message: format(MSG, method: method))
70
+ add_offense(node, location: :selector)
71
71
  end
72
72
 
73
73
  def autocorrect(node)
@@ -75,7 +75,7 @@ module RuboCop
75
75
  method = node.method_name
76
76
 
77
77
  corrector.remove(dot_method_with_whitespace(method, node))
78
- corrector.insert_before(node.receiver.loc.dot.begin, ".#{method}")
78
+ corrector.insert_before(node.receiver.loc.dot.begin, '.distinct')
79
79
  end
80
80
  end
81
81
 
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # When you define a uniqueness validation in Active Record model,
7
+ # you also should add a unique index for the column. There are two reasons
8
+ # First, duplicated records may occur even if Active Record's validation
9
+ # is defined.
10
+ # Second, it will cause slow queries. The validation executes a `SELECT`
11
+ # statement with the target column when inserting/updating a record.
12
+ # If the column does not have an index and the table is large,
13
+ # the query will be heavy.
14
+ #
15
+ # Note that the cop does nothing if db/schema.rb does not exist.
16
+ #
17
+ # @example
18
+ # # bad - if the schema does not have a unique index
19
+ # validates :account, uniqueness: true
20
+ #
21
+ # # good - if the schema has a unique index
22
+ # validates :account, uniqueness: true
23
+ #
24
+ # # good - even if the schema does not have a unique index
25
+ # validates :account, length: { minimum: MIN_LENGTH }
26
+ #
27
+ class UniqueValidationWithoutIndex < Cop
28
+ include ActiveRecordHelper
29
+
30
+ MSG = 'Uniqueness validation should be with a unique index.'
31
+
32
+ def on_send(node)
33
+ return unless node.method?(:validates)
34
+ return unless uniqueness_part(node)
35
+ return if condition_part?(node)
36
+ return unless schema
37
+ return if with_index?(node)
38
+
39
+ add_offense(node)
40
+ end
41
+
42
+ private
43
+
44
+ def with_index?(node)
45
+ klass = class_node(node)
46
+ return true unless klass # Skip analysis
47
+
48
+ table = schema.table_by(name: table_name(klass))
49
+ return true unless table # Skip analysis if it can't find the table
50
+
51
+ names = column_names(node)
52
+ return true unless names
53
+
54
+ # Compatibility for Rails 4.2.
55
+ add_indicies = schema.add_indicies_by(table_name: table_name(klass))
56
+
57
+ (table.indices + add_indicies).any? do |index|
58
+ index.unique &&
59
+ (index.columns.to_set == names ||
60
+ include_column_names_in_expression_index?(index, names))
61
+ end
62
+ end
63
+
64
+ def include_column_names_in_expression_index?(index, column_names)
65
+ return false unless (expression_index = index.expression)
66
+
67
+ column_names.all? do |column_name|
68
+ expression_index.include?(column_name)
69
+ end
70
+ end
71
+
72
+ def column_names(node)
73
+ arg = node.first_argument
74
+ return unless arg.str_type? || arg.sym_type?
75
+
76
+ ret = [arg.value]
77
+ names_from_scope = column_names_from_scope(node)
78
+ ret.concat(names_from_scope) if names_from_scope
79
+
80
+ ret.map! do |name|
81
+ klass = class_node(node)
82
+ resolve_relation_into_column(
83
+ name: name.to_s,
84
+ class_node: klass,
85
+ table: schema.table_by(name: table_name(klass))
86
+ )
87
+ end
88
+ ret.include?(nil) ? nil : ret.to_set
89
+ end
90
+
91
+ def column_names_from_scope(node)
92
+ uniq = uniqueness_part(node)
93
+ return unless uniq.hash_type?
94
+
95
+ scope = find_scope(uniq)
96
+ return unless scope
97
+
98
+ case scope.type
99
+ when :sym, :str
100
+ [scope.value]
101
+ when :array
102
+ array_node_to_array(scope)
103
+ end
104
+ end
105
+
106
+ def find_scope(pairs)
107
+ pairs.each_pair.find do |pair|
108
+ key = pair.key
109
+ next unless key.sym_type? && key.value == :scope
110
+
111
+ break pair.value
112
+ end
113
+ end
114
+
115
+ def class_node(node)
116
+ node.each_ancestor.find(&:class_type?)
117
+ end
118
+
119
+ def uniqueness_part(node)
120
+ pairs = node.arguments.last
121
+ return unless pairs.hash_type?
122
+
123
+ pairs.each_pair.find do |pair|
124
+ next unless pair.key.sym_type? && pair.key.value == :uniqueness
125
+
126
+ break pair.value
127
+ end
128
+ end
129
+
130
+ def condition_part?(node)
131
+ pairs = node.arguments.last
132
+ return unless pairs.hash_type?
133
+
134
+ pairs.each_pair.any? do |pair|
135
+ key = pair.key
136
+ next unless key.sym_type?
137
+
138
+ key.value == :if || key.value == :unless
139
+ end
140
+ end
141
+
142
+ def array_node_to_array(node)
143
+ node.values.map do |elm|
144
+ case elm.type
145
+ when :str, :sym
146
+ elm.value
147
+ else
148
+ return nil
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -15,8 +15,6 @@ module RuboCop
15
15
  # Rails.env.production?
16
16
  # Rails.env == 'production'
17
17
  class UnknownEnv < Cop
18
- include NameSimilarity
19
-
20
18
  MSG = 'Unknown environment `%<name>s`.'
21
19
  MSG_SIMILAR = 'Unknown environment `%<name>s`. ' \
22
20
  'Did you mean `%<similar>s`?'
@@ -57,11 +55,14 @@ module RuboCop
57
55
 
58
56
  def message(name)
59
57
  name = name.to_s.chomp('?')
60
- similar = find_similar_name(name, [])
61
- if similar
62
- format(MSG_SIMILAR, name: name, similar: similar)
63
- else
58
+
59
+ spell_checker = DidYouMean::SpellChecker.new(dictionary: environments)
60
+ similar_names = spell_checker.correct(name)
61
+
62
+ if similar_names.empty?
64
63
  format(MSG, name: name)
64
+ else
65
+ format(MSG_SIMILAR, name: name, similar: similar_names.join(', '))
65
66
  end
66
67
  end
67
68
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'mixin/active_record_helper'
4
+ require_relative 'mixin/index_method'
3
5
  require_relative 'mixin/target_rails_version'
4
6
 
5
7
  require_relative 'rails/action_filter'
@@ -14,6 +16,7 @@ require_relative 'rails/assert_not'
14
16
  require_relative 'rails/belongs_to'
15
17
  require_relative 'rails/blank'
16
18
  require_relative 'rails/bulk_change_table'
19
+ require_relative 'rails/content_tag'
17
20
  require_relative 'rails/create_table_with_timestamps'
18
21
  require_relative 'rails/date'
19
22
  require_relative 'rails/delegate'
@@ -32,18 +35,22 @@ require_relative 'rails/helper_instance_variable'
32
35
  require_relative 'rails/http_positional_arguments'
33
36
  require_relative 'rails/http_status'
34
37
  require_relative 'rails/ignored_skip_action_filter_option'
38
+ require_relative 'rails/index_by'
39
+ require_relative 'rails/index_with'
35
40
  require_relative 'rails/inverse_of'
36
41
  require_relative 'rails/lexically_scoped_action_filter'
37
42
  require_relative 'rails/link_to_blank'
38
43
  require_relative 'rails/not_null_column'
39
44
  require_relative 'rails/output'
40
45
  require_relative 'rails/output_safety'
46
+ require_relative 'rails/pick'
41
47
  require_relative 'rails/pluralization_grammar'
42
48
  require_relative 'rails/presence'
43
49
  require_relative 'rails/present'
44
50
  require_relative 'rails/rake_environment'
45
51
  require_relative 'rails/read_write_attribute'
46
52
  require_relative 'rails/redundant_allow_nil'
53
+ require_relative 'rails/redundant_foreign_key'
47
54
  require_relative 'rails/redundant_receiver_in_with_options'
48
55
  require_relative 'rails/reflection_class_name'
49
56
  require_relative 'rails/refute_methods'
@@ -57,5 +64,6 @@ require_relative 'rails/scope_args'
57
64
  require_relative 'rails/skips_model_validations'
58
65
  require_relative 'rails/time_zone'
59
66
  require_relative 'rails/uniq_before_pluck'
67
+ require_relative 'rails/unique_validation_without_index'
60
68
  require_relative 'rails/unknown_env'
61
69
  require_relative 'rails/validation'
@@ -8,7 +8,7 @@ module RuboCop
8
8
  def self.defaults!
9
9
  path = CONFIG_DEFAULT.to_s
10
10
  hash = ConfigLoader.send(:load_yaml_configuration, path)
11
- config = Config.new(hash, path)
11
+ config = Config.new(hash, path).tap(&:make_excludes_absolute)
12
12
  puts "configuration from #{path}" if ConfigLoader.debug?
13
13
  config = ConfigLoader.merge_with_default(config, path, unset_nil: false)
14
14
  ConfigLoader.instance_variable_set(:@default_configuration, config)
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Rails
5
+ # It loads db/schema.rb and return Schema object.
6
+ # Cops refers database schema information with this module.
7
+ module SchemaLoader
8
+ extend self
9
+
10
+ # It parses `db/schema.rb` and return it.
11
+ # It returns `nil` if it can't find `db/schema.rb`.
12
+ # So a cop that uses the loader should handle `nil` properly.
13
+ #
14
+ # @return [Schema, nil]
15
+ def load(target_ruby_version)
16
+ return @schema if defined?(@schema)
17
+
18
+ @schema = load!(target_ruby_version)
19
+ end
20
+
21
+ def reset!
22
+ return unless instance_variable_defined?(:@schema)
23
+
24
+ remove_instance_variable(:@schema)
25
+ end
26
+
27
+ def db_schema_path
28
+ path = Pathname.pwd
29
+ until path.root?
30
+ schema_path = path.join('db/schema.rb')
31
+ return schema_path if schema_path.exist?
32
+
33
+ path = path.join('../').cleanpath
34
+ end
35
+
36
+ nil
37
+ end
38
+
39
+ private
40
+
41
+ def load!(target_ruby_version)
42
+ path = db_schema_path
43
+ return unless path
44
+
45
+ ast = parse(path, target_ruby_version)
46
+ Schema.new(ast)
47
+ end
48
+
49
+ def parse(path, target_ruby_version)
50
+ klass_name = :"Ruby#{target_ruby_version.to_s.sub('.', '')}"
51
+ klass = ::Parser.const_get(klass_name)
52
+ parser = klass.new(RuboCop::AST::Builder.new)
53
+
54
+ buffer = Parser::Source::Buffer.new(path, 1)
55
+ buffer.source = path.read
56
+
57
+ parser.parse(buffer)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Rails
5
+ module SchemaLoader
6
+ # Represent db/schema.rb
7
+ class Schema
8
+ attr_reader :tables, :add_indicies
9
+
10
+ def initialize(ast)
11
+ @tables = []
12
+ @add_indicies = []
13
+
14
+ build!(ast)
15
+ end
16
+
17
+ def table_by(name:)
18
+ tables.find do |table|
19
+ table.name == name
20
+ end
21
+ end
22
+
23
+ def add_indicies_by(table_name:)
24
+ add_indicies.select do |add_index|
25
+ add_index.table_name == table_name
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def build!(ast)
32
+ raise "Unexpected type: #{ast.type}" unless ast.block_type?
33
+
34
+ each_table(ast) do |table_def|
35
+ @tables << Table.new(table_def)
36
+ end
37
+
38
+ # Compatibility for Rails 4.2.
39
+ each_add_index(ast) do |add_index_def|
40
+ @add_indicies << AddIndex.new(add_index_def)
41
+ end
42
+ end
43
+
44
+ def each_table(ast)
45
+ case ast.body.type
46
+ when :begin
47
+ ast.body.children.each do |node|
48
+ next unless node.block_type? && node.method?(:create_table)
49
+
50
+ yield(node)
51
+ end
52
+ else
53
+ yield ast.body
54
+ end
55
+ end
56
+
57
+ def each_add_index(ast)
58
+ ast.body.children.each do |node|
59
+ next if !node&.send_type? || !node.method?(:add_index)
60
+
61
+ yield(node)
62
+ end
63
+ end
64
+ end
65
+
66
+ # Represent a table
67
+ class Table
68
+ attr_reader :name, :columns, :indices
69
+
70
+ def initialize(node)
71
+ @name = node.send_node.first_argument.value
72
+ @columns = build_columns(node)
73
+ @indices = build_indices(node)
74
+ end
75
+
76
+ def with_column?(name:)
77
+ @columns.any? { |c| c.name == name }
78
+ end
79
+
80
+ private
81
+
82
+ def build_columns(node)
83
+ each_content(node).map do |child|
84
+ next unless child&.send_type?
85
+ next if child.method?(:index)
86
+
87
+ Column.new(child)
88
+ end.compact
89
+ end
90
+
91
+ def build_indices(node)
92
+ each_content(node).map do |child|
93
+ next unless child&.send_type?
94
+ next unless child.method?(:index)
95
+
96
+ Index.new(child)
97
+ end.compact
98
+ end
99
+
100
+ def each_content(node)
101
+ return enum_for(__method__, node) unless block_given?
102
+
103
+ case node.body&.type
104
+ when :begin
105
+ node.body.children.each do |child|
106
+ yield(child)
107
+ end
108
+ else
109
+ yield(node.body)
110
+ end
111
+ end
112
+ end
113
+
114
+ # Represent a column
115
+ class Column
116
+ attr_reader :name, :type, :not_null
117
+
118
+ def initialize(node)
119
+ @name = node.first_argument.value
120
+ @type = node.method_name
121
+ @not_null = nil
122
+
123
+ analyze_keywords!(node)
124
+ end
125
+
126
+ private
127
+
128
+ def analyze_keywords!(node)
129
+ pairs = node.arguments.last
130
+ return unless pairs.hash_type?
131
+
132
+ pairs.each_pair do |k, v|
133
+ if k.value == :null
134
+ @not_null = v.true_type? ? false : true
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ # Represent an index
141
+ class Index
142
+ attr_reader :name, :columns, :expression, :unique
143
+
144
+ def initialize(node)
145
+ @columns, @expression = build_columns_or_expr(node.first_argument)
146
+ @unique = nil
147
+
148
+ analyze_keywords!(node)
149
+ end
150
+
151
+ private
152
+
153
+ def build_columns_or_expr(columns)
154
+ if columns.array_type?
155
+ [columns.values.map(&:value), nil]
156
+ else
157
+ [[], columns.value]
158
+ end
159
+ end
160
+
161
+ def analyze_keywords!(node)
162
+ pairs = node.arguments.last
163
+ return unless pairs.hash_type?
164
+
165
+ pairs.each_pair do |k, v|
166
+ case k.value
167
+ when :name
168
+ @name = v.value
169
+ when :unique
170
+ @unique = true
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ # Represent an `add_index`
177
+ class AddIndex < Index
178
+ attr_reader :table_name
179
+
180
+ def initialize(node)
181
+ @table_name = node.first_argument.value
182
+ @columns, @expression = build_columns_or_expr(node.arguments[1])
183
+ @unique = nil
184
+
185
+ analyze_keywords!(node)
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end