rubocop-rails 2.4.2 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0cf251ed7e29a11391396009eb82eed3fa4684e100b26b9041d469da2db04133
4
- data.tar.gz: 440778968fbafa4dd3415741d8ef3c49e6025bee7ec85454a5b7012859e657c4
3
+ metadata.gz: fd5bcec81363b7952d67bc476f82e25fbe0e0a8e4eb62e490712ddb5cfb58807
4
+ data.tar.gz: 0eeec3b789aed712f41d0f842620ec0fd871dc884fe791819256b5777a0edb9d
5
5
  SHA512:
6
- metadata.gz: fe54c2657de98b317f8df85618a4a00708be71dc8c7c924a43734773a1f3d31da6d85e44cbb983f5c10ecb235bf420ac86a4e67ac86a45b70e7c9b8403f4623e
7
- data.tar.gz: 4c9111285fe0211b5b1da14f8b31d2023a20e3b223dee3a1a3fcec0c66e622a0116ec783059a5161042640f92799b4df2b7585a4dd0ac96f12b06fa776886960
6
+ metadata.gz: 4fb1e35ca0fab64ed16959e29d2144b5cb9aafaf5064d04c67f1bee3cba9eaa4cc2c7c87b71dc240d10c1d11a8361ccacb3cb7002b9e14ef0a3908bfe271f79d
7
+ data.tar.gz: 108de4501d9f50c50d7d3e23c93574a1a2eee389d5abef80b7e9982d3d7dba524dc263cf76986ae3caa1ab202fc4c7b20ce8106ac82dab5c43e7a578f4fe04ef
data/README.md CHANGED
@@ -76,6 +76,10 @@ Rails/FindBy:
76
76
  - lib/example.rb
77
77
  ```
78
78
 
79
+ ## Documentation
80
+
81
+ You can read a lot more about RuboCop Rails in its [official docs](https://docs.rubocop.org/projects/rails/).
82
+
79
83
  ## Compatibility
80
84
 
81
85
  Rails cops support the following versions:
data/config/default.yml CHANGED
@@ -268,6 +268,16 @@ Rails/IgnoredSkipActionFilterOption:
268
268
  Include:
269
269
  - app/controllers/**/*.rb
270
270
 
271
+ Rails/IndexBy:
272
+ Description: 'Prefer `index_by` over `each_with_object` or `map`.'
273
+ Enabled: true
274
+ VersionAdded: '2.5'
275
+
276
+ Rails/IndexWith:
277
+ Description: 'Prefer `index_with` over `each_with_object` or `map`.'
278
+ Enabled: true
279
+ VersionAdded: '2.5'
280
+
271
281
  Rails/InverseOf:
272
282
  Description: 'Checks for associations where the inverse cannot be determined automatically.'
273
283
  Enabled: true
@@ -346,6 +356,8 @@ Rails/RakeEnvironment:
346
356
  Include:
347
357
  - '**/Rakefile'
348
358
  - '**/*.rake'
359
+ Exclude:
360
+ - 'lib/capistrano/tasks/**/*.rake'
349
361
 
350
362
  Rails/ReadWriteAttribute:
351
363
  Description: >-
@@ -381,6 +393,10 @@ Rails/RefuteMethods:
381
393
  Description: 'Use `assert_not` methods instead of `refute` methods.'
382
394
  Enabled: true
383
395
  VersionAdded: '0.56'
396
+ EnforcedStyle: assert_not
397
+ SupportedStyles:
398
+ - assert_not
399
+ - refute
384
400
  Include:
385
401
  - '**/test/**/*'
386
402
 
@@ -496,6 +512,13 @@ Rails/UniqBeforePluck:
496
512
  - aggressive
497
513
  AutoCorrect: false
498
514
 
515
+ Rails/UniqueValidationWithoutIndex:
516
+ Description: 'Uniqueness validation should be with a unique index.'
517
+ Enabled: true
518
+ VersionAdded: '2.5'
519
+ Include:
520
+ - app/models/**/*.rb
521
+
499
522
  Rails/UnknownEnv:
500
523
  Description: 'Use correct environment name.'
501
524
  Enabled: true
data/lib/rubocop-rails.rb CHANGED
@@ -2,10 +2,13 @@
2
2
 
3
3
  require 'rubocop'
4
4
  require 'rack/utils'
5
+ require 'active_support/inflector'
5
6
 
6
7
  require_relative 'rubocop/rails'
7
8
  require_relative 'rubocop/rails/version'
8
9
  require_relative 'rubocop/rails/inject'
10
+ require_relative 'rubocop/rails/schema_loader'
11
+ require_relative 'rubocop/rails/schema_loader/schema'
9
12
 
10
13
  RuboCop::Rails::Inject.defaults!
11
14
 
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ # A mixin to extend cops for Active Record features
6
+ module ActiveRecordHelper
7
+ extend NodePattern::Macros
8
+
9
+ def_node_search :find_set_table_name, <<~PATTERN
10
+ (send self :table_name= {str sym})
11
+ PATTERN
12
+
13
+ def_node_search :find_belongs_to, <<~PATTERN
14
+ (send nil? :belongs_to {str sym} ...)
15
+ PATTERN
16
+
17
+ def table_name(class_node)
18
+ table_name = find_set_table_name(class_node).to_a.last&.first_argument
19
+ return table_name.value.to_s if table_name
20
+
21
+ namespaces = class_node.each_ancestor(:class, :module)
22
+ [class_node, *namespaces]
23
+ .reverse
24
+ .map { |klass| klass.identifier.children[1] }.join('_')
25
+ .tableize
26
+ end
27
+
28
+ # Resolve relation into column name.
29
+ # It just returns column_name if the column exists.
30
+ # Or it tries to resolve column_name as a relation.
31
+ # It returns `nil` if it can't resolve.
32
+ #
33
+ # @param name [String]
34
+ # @param class_node [RuboCop::AST::Node]
35
+ # @param table [RuboCop::Rails::SchemaLoader::Table]
36
+ # @return [String, nil]
37
+ def resolve_relation_into_column(name:, class_node:, table:)
38
+ return name if table.with_column?(name: name)
39
+
40
+ find_belongs_to(class_node) do |belongs_to|
41
+ next unless belongs_to.first_argument.value.to_s == name
42
+
43
+ fk = foreign_key_of(belongs_to) || "#{name}_id"
44
+ return fk if table.with_column?(name: fk)
45
+ end
46
+ nil
47
+ end
48
+
49
+ def foreign_key_of(belongs_to)
50
+ options = belongs_to.last_argument
51
+ return unless options.hash_type?
52
+
53
+ options.each_pair.find do |pair|
54
+ next unless pair.key.sym_type? && pair.key.value == :foreign_key
55
+ next unless pair.value.sym_type? || pair.value.str_type?
56
+
57
+ break pair.value.value.to_s
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ # Common functionality for Rails/IndexBy and Rails/IndexWith
6
+ module IndexMethod # rubocop:disable Metrics/ModuleLength
7
+ def on_block(node)
8
+ on_bad_each_with_object(node) do |*match|
9
+ handle_possible_offense(node, match, 'each_with_object')
10
+ end
11
+ end
12
+
13
+ def on_send(node)
14
+ on_bad_map_to_h(node) do |*match|
15
+ handle_possible_offense(node, match, 'map { ... }.to_h')
16
+ end
17
+
18
+ on_bad_hash_brackets_map(node) do |*match|
19
+ handle_possible_offense(node, match, 'Hash[map { ... }]')
20
+ end
21
+ end
22
+
23
+ def on_csend(node)
24
+ on_bad_map_to_h(node) do |*match|
25
+ handle_possible_offense(node, match, 'map { ... }.to_h')
26
+ end
27
+ end
28
+
29
+ def autocorrect(node)
30
+ lambda do |corrector|
31
+ correction = prepare_correction(node)
32
+ execute_correction(corrector, node, correction)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ # @abstract Implemented with `def_node_matcher`
39
+ def on_bad_each_with_object(_node)
40
+ raise NotImplementedError
41
+ end
42
+
43
+ # @abstract Implemented with `def_node_matcher`
44
+ def on_bad_map_to_h(_node)
45
+ raise NotImplementedError
46
+ end
47
+
48
+ # @abstract Implemented with `def_node_matcher`
49
+ def on_bad_hash_brackets_map(_node)
50
+ raise NotImplementedError
51
+ end
52
+
53
+ def handle_possible_offense(node, match, match_desc)
54
+ captures = extract_captures(match)
55
+
56
+ return if captures.noop_transformation?
57
+
58
+ add_offense(
59
+ node,
60
+ message: "Prefer `#{new_method_name}` over `#{match_desc}`."
61
+ )
62
+ end
63
+
64
+ def extract_captures(match)
65
+ argname, body_expr = *match
66
+ Captures.new(argname, body_expr)
67
+ end
68
+
69
+ def new_method_name
70
+ raise NotImplementedError
71
+ end
72
+
73
+ def prepare_correction(node)
74
+ if (match = on_bad_each_with_object(node))
75
+ Autocorrection.from_each_with_object(node, match)
76
+ elsif (match = on_bad_map_to_h(node))
77
+ Autocorrection.from_map_to_h(node, match)
78
+ elsif (match = on_bad_hash_brackets_map(node))
79
+ Autocorrection.from_hash_brackets_map(node, match)
80
+ else
81
+ raise 'unreachable'
82
+ end
83
+ end
84
+
85
+ def execute_correction(corrector, node, correction)
86
+ correction.strip_prefix_and_suffix(node, corrector)
87
+ correction.set_new_method_name(new_method_name, corrector)
88
+
89
+ captures = extract_captures(correction.match)
90
+ correction.set_new_arg_name(captures.transformed_argname, corrector)
91
+ correction.set_new_body_expression(
92
+ captures.transforming_body_expr,
93
+ corrector
94
+ )
95
+ end
96
+
97
+ # Internal helper class to hold match data
98
+ Captures = Struct.new(
99
+ :transformed_argname,
100
+ :transforming_body_expr
101
+ ) do
102
+ def noop_transformation?
103
+ transforming_body_expr.lvar_type? &&
104
+ transforming_body_expr.children == [transformed_argname]
105
+ end
106
+ end
107
+
108
+ # Internal helper class to hold autocorrect data
109
+ Autocorrection = Struct.new(:match, :block_node, :leading, :trailing) do # rubocop:disable Metrics/BlockLength
110
+ def self.from_each_with_object(node, match)
111
+ new(match, node, 0, 0)
112
+ end
113
+
114
+ def self.from_map_to_h(node, match)
115
+ strip_trailing_chars = node.parent&.block_type? ? 0 : '.to_h'.length
116
+ new(match, node.children.first, 0, strip_trailing_chars)
117
+ end
118
+
119
+ def self.from_hash_brackets_map(node, match)
120
+ new(match, node.children.last, 'Hash['.length, ']'.length)
121
+ end
122
+
123
+ def strip_prefix_and_suffix(node, corrector)
124
+ expression = node.loc.expression
125
+ corrector.remove_leading(expression, leading)
126
+ corrector.remove_trailing(expression, trailing)
127
+ end
128
+
129
+ def set_new_method_name(new_method_name, corrector)
130
+ range = block_node.send_node.loc.selector
131
+ if (send_end = block_node.send_node.loc.end)
132
+ # If there are arguments (only true in the `each_with_object` case)
133
+ range = range.begin.join(send_end)
134
+ end
135
+ corrector.replace(range, new_method_name)
136
+ end
137
+
138
+ def set_new_arg_name(transformed_argname, corrector)
139
+ corrector.replace(
140
+ block_node.arguments.loc.expression,
141
+ "|#{transformed_argname}|"
142
+ )
143
+ end
144
+
145
+ def set_new_body_expression(transforming_body_expr, corrector)
146
+ corrector.replace(
147
+ block_node.body.loc.expression,
148
+ transforming_body_expr.loc.expression.source
149
+ )
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -16,52 +16,98 @@ module RuboCop
16
16
  # # good
17
17
  # Rails.env.production?
18
18
  class EnvironmentComparison < Cop
19
- MSG = "Favor `Rails.env.%<env>s?` over `Rails.env == '%<env>s'`."
19
+ MSG = 'Favor `%<bang>sRails.env.%<env>s?` over `%<source>s`.'
20
20
 
21
21
  SYM_MSG = 'Do not compare `Rails.env` with a symbol, it will always ' \
22
22
  'evaluate to `false`.'
23
23
 
24
- def_node_matcher :environment_str_comparison?, <<~PATTERN
24
+ def_node_matcher :comparing_str_env_with_rails_env_on_lhs?, <<~PATTERN
25
25
  (send
26
26
  (send (const {nil? cbase} :Rails) :env)
27
- :==
27
+ {:== :!=}
28
28
  $str
29
29
  )
30
30
  PATTERN
31
31
 
32
- def_node_matcher :environment_sym_comparison?, <<~PATTERN
32
+ def_node_matcher :comparing_str_env_with_rails_env_on_rhs?, <<~PATTERN
33
+ (send
34
+ $str
35
+ {:== :!=}
36
+ (send (const {nil? cbase} :Rails) :env)
37
+ )
38
+ PATTERN
39
+
40
+ def_node_matcher :comparing_sym_env_with_rails_env_on_lhs?, <<~PATTERN
33
41
  (send
34
42
  (send (const {nil? cbase} :Rails) :env)
35
- :==
43
+ {:== :!=}
36
44
  $sym
37
45
  )
38
46
  PATTERN
39
47
 
48
+ def_node_matcher :comparing_sym_env_with_rails_env_on_rhs?, <<~PATTERN
49
+ (send
50
+ $sym
51
+ {:== :!=}
52
+ (send (const {nil? cbase} :Rails) :env)
53
+ )
54
+ PATTERN
55
+
56
+ def_node_matcher :content, <<~PATTERN
57
+ ({str sym} $_)
58
+ PATTERN
59
+
40
60
  def on_send(node)
41
- environment_str_comparison?(node) do |env_node|
61
+ if (env_node = comparing_str_env_with_rails_env_on_lhs?(node) ||
62
+ comparing_str_env_with_rails_env_on_rhs?(node))
42
63
  env, = *env_node
43
- add_offense(node, message: format(MSG, env: env))
64
+ bang = node.method?(:!=) ? '!' : ''
65
+
66
+ add_offense(node, message: format(
67
+ MSG, bang: bang, env: env, source: node.source
68
+ ))
44
69
  end
45
- environment_sym_comparison?(node) do |_|
70
+
71
+ if comparing_sym_env_with_rails_env_on_lhs?(node) ||
72
+ comparing_sym_env_with_rails_env_on_rhs?(node)
46
73
  add_offense(node, message: SYM_MSG)
47
74
  end
48
75
  end
49
76
 
50
77
  def autocorrect(node)
51
78
  lambda do |corrector|
52
- corrector.replace(node.source_range, replacement(node))
79
+ replacement = build_predicate_method(node)
80
+
81
+ corrector.replace(node.source_range, replacement)
53
82
  end
54
83
  end
55
84
 
56
85
  private
57
86
 
58
- def replacement(node)
59
- "#{node.receiver.source}.#{content(node.first_argument)}?"
87
+ def build_predicate_method(node)
88
+ if rails_env_on_lhs?(node)
89
+ build_predicate_method_for_rails_env_on_lhs(node)
90
+ else
91
+ build_predicate_method_for_rails_env_on_rhs(node)
92
+ end
60
93
  end
61
94
 
62
- def_node_matcher :content, <<~PATTERN
63
- ({str sym} $_)
64
- PATTERN
95
+ def rails_env_on_lhs?(node)
96
+ comparing_str_env_with_rails_env_on_lhs?(node) ||
97
+ comparing_sym_env_with_rails_env_on_lhs?(node)
98
+ end
99
+
100
+ def build_predicate_method_for_rails_env_on_lhs(node)
101
+ bang = node.method?(:!=) ? '!' : ''
102
+
103
+ "#{bang}#{node.receiver.source}.#{content(node.first_argument)}?"
104
+ end
105
+
106
+ def build_predicate_method_for_rails_env_on_rhs(node)
107
+ bang = node.method?(:!=) ? '!' : ''
108
+
109
+ "#{bang}#{node.first_argument.source}.#{content(node.receiver)}?"
110
+ end
65
111
  end
66
112
  end
67
113
  end
@@ -22,7 +22,7 @@ module RuboCop
22
22
  MSG = 'Use keyword arguments instead of ' \
23
23
  'positional arguments for http call: `%<verb>s`.'
24
24
  KEYWORD_ARGS = %i[
25
- method params session body flash xhr as headers env
25
+ method params session body flash xhr as headers env to
26
26
  ].freeze
27
27
  HTTP_METHODS = %i[get post put patch delete head].freeze
28
28
 
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop looks for uses of `each_with_object({}) { ... }`,
7
+ # `map { ... }.to_h`, and `Hash[map { ... }]` that are transforming
8
+ # an enumerable into a hash where the values are the original elements.
9
+ # Rails provides the `index_by` method for this purpose.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # [1, 2, 3].each_with_object({}) { |el, h| h[foo(el)] = el }
14
+ # [1, 2, 3].map { |el| [foo(el), el] }.to_h
15
+ # Hash[[1, 2, 3].collect { |el| [foo(el), el] }]
16
+ #
17
+ # # good
18
+ # [1, 2, 3].index_by { |el| foo(el) }
19
+ class IndexBy < Cop
20
+ include IndexMethod
21
+
22
+ def_node_matcher :on_bad_each_with_object, <<~PATTERN
23
+ (block
24
+ ({send csend} _ :each_with_object (hash))
25
+ (args (arg $_el) (arg _memo))
26
+ ({send csend} (lvar _memo) :[]= $_ (lvar _el)))
27
+ PATTERN
28
+
29
+ def_node_matcher :on_bad_map_to_h, <<~PATTERN
30
+ ({send csend}
31
+ (block
32
+ ({send csend} _ {:map :collect})
33
+ (args (arg $_el))
34
+ (array $_ (lvar _el)))
35
+ :to_h)
36
+ PATTERN
37
+
38
+ def_node_matcher :on_bad_hash_brackets_map, <<~PATTERN
39
+ (send
40
+ (const _ :Hash)
41
+ :[]
42
+ (block
43
+ ({send csend} _ {:map :collect})
44
+ (args (arg $_el))
45
+ (array $_ (lvar _el))))
46
+ PATTERN
47
+
48
+ private
49
+
50
+ def new_method_name
51
+ 'index_by'
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop looks for uses of `each_with_object({}) { ... }`,
7
+ # `map { ... }.to_h`, and `Hash[map { ... }]` that are transforming
8
+ # an enumerable into a hash where the keys are the original elements.
9
+ # Rails provides the `index_with` method for this purpose.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # [1, 2, 3].each_with_object({}) { |el, h| h[el] = foo(el) }
14
+ # [1, 2, 3].map { |el| [el, foo(el)] }.to_h
15
+ # Hash[[1, 2, 3].collect { |el| [el, foo(el)] }]
16
+ #
17
+ # # good
18
+ # [1, 2, 3].index_with { |el| foo(el) }
19
+ class IndexWith < Cop
20
+ extend TargetRailsVersion
21
+ include IndexMethod
22
+
23
+ minimum_target_rails_version 6.0
24
+
25
+ def_node_matcher :on_bad_each_with_object, <<~PATTERN
26
+ (block
27
+ ({send csend} _ :each_with_object (hash))
28
+ (args (arg $_el) (arg _memo))
29
+ ({send csend} (lvar _memo) :[]= (lvar _el) $_))
30
+ PATTERN
31
+
32
+ def_node_matcher :on_bad_map_to_h, <<~PATTERN
33
+ ({send csend}
34
+ (block
35
+ ({send csend} _ {:map :collect})
36
+ (args (arg $_el))
37
+ (array (lvar _el) $_))
38
+ :to_h)
39
+ PATTERN
40
+
41
+ def_node_matcher :on_bad_hash_brackets_map, <<~PATTERN
42
+ (send
43
+ (const _ :Hash)
44
+ :[]
45
+ (block
46
+ ({send csend} _ {:map :collect})
47
+ (args (arg $_el))
48
+ (array (lvar _el) $_)))
49
+ PATTERN
50
+
51
+ private
52
+
53
+ def new_method_name
54
+ 'index_with'
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -6,7 +6,7 @@ module RuboCop
6
6
  #
7
7
  # Use `assert_not` methods instead of `refute` methods.
8
8
  #
9
- # @example
9
+ # @example EnforcedStyle: assert_not (default)
10
10
  # # bad
11
11
  # refute false
12
12
  # refute_empty [1, 2, 3]
@@ -17,29 +17,43 @@ module RuboCop
17
17
  # assert_not_empty [1, 2, 3]
18
18
  # assert_not_equal true, false
19
19
  #
20
+ # @example EnforcedStyle: refute
21
+ # # bad
22
+ # assert_not false
23
+ # assert_not_empty [1, 2, 3]
24
+ # assert_not_equal true, false
25
+ #
26
+ # # good
27
+ # refute false
28
+ # refute_empty [1, 2, 3]
29
+ # refute_equal true, false
30
+ #
20
31
  class RefuteMethods < Cop
21
- MSG = 'Prefer `%<assert_method>s` over `%<refute_method>s`.'
32
+ include ConfigurableEnforcedStyle
33
+
34
+ MSG = 'Prefer `%<good_method>s` over `%<bad_method>s`.'
22
35
 
23
36
  CORRECTIONS = {
24
- refute: 'assert_not',
25
- refute_empty: 'assert_not_empty',
26
- refute_equal: 'assert_not_equal',
27
- refute_in_delta: 'assert_not_in_delta',
28
- refute_in_epsilon: 'assert_not_in_epsilon',
29
- refute_includes: 'assert_not_includes',
30
- refute_instance_of: 'assert_not_instance_of',
31
- refute_kind_of: 'assert_not_kind_of',
32
- refute_nil: 'assert_not_nil',
33
- refute_operator: 'assert_not_operator',
34
- refute_predicate: 'assert_not_predicate',
35
- refute_respond_to: 'assert_not_respond_to',
36
- refute_same: 'assert_not_same',
37
- refute_match: 'assert_no_match'
37
+ refute: :assert_not,
38
+ refute_empty: :assert_not_empty,
39
+ refute_equal: :assert_not_equal,
40
+ refute_in_delta: :assert_not_in_delta,
41
+ refute_in_epsilon: :assert_not_in_epsilon,
42
+ refute_includes: :assert_not_includes,
43
+ refute_instance_of: :assert_not_instance_of,
44
+ refute_kind_of: :assert_not_kind_of,
45
+ refute_nil: :assert_not_nil,
46
+ refute_operator: :assert_not_operator,
47
+ refute_predicate: :assert_not_predicate,
48
+ refute_respond_to: :assert_not_respond_to,
49
+ refute_same: :assert_not_same,
50
+ refute_match: :assert_no_match
38
51
  }.freeze
39
52
 
40
- OFFENSIVE_METHODS = CORRECTIONS.keys.freeze
53
+ REFUTE_METHODS = CORRECTIONS.keys.freeze
54
+ ASSERT_NOT_METHODS = CORRECTIONS.values.freeze
41
55
 
42
- def_node_matcher :offensive?, '(send nil? #refute_method? ...)'
56
+ def_node_matcher :offensive?, '(send nil? #bad_method? ...)'
43
57
 
44
58
  def on_send(node)
45
59
  return unless offensive?(node)
@@ -49,27 +63,39 @@ module RuboCop
49
63
  end
50
64
 
51
65
  def autocorrect(node)
66
+ bad_method = node.method_name
67
+ good_method = convert_good_method(bad_method)
68
+
52
69
  lambda do |corrector|
53
- corrector.replace(
54
- node.loc.selector,
55
- CORRECTIONS[node.method_name]
56
- )
70
+ corrector.replace(node.loc.selector, good_method.to_s)
57
71
  end
58
72
  end
59
73
 
60
74
  private
61
75
 
62
- def refute_method?(method_name)
63
- OFFENSIVE_METHODS.include?(method_name)
76
+ def bad_method?(method_name)
77
+ if style == :assert_not
78
+ REFUTE_METHODS.include?(method_name)
79
+ else
80
+ ASSERT_NOT_METHODS.include?(method_name)
81
+ end
64
82
  end
65
83
 
66
84
  def offense_message(method_name)
67
85
  format(
68
86
  MSG,
69
- refute_method: method_name,
70
- assert_method: CORRECTIONS[method_name]
87
+ bad_method: method_name,
88
+ good_method: convert_good_method(method_name)
71
89
  )
72
90
  end
91
+
92
+ def convert_good_method(bad_method)
93
+ if style == :assert_not
94
+ CORRECTIONS.fetch(bad_method)
95
+ else
96
+ CORRECTIONS.invert.fetch(bad_method)
97
+ end
98
+ end
73
99
  end
74
100
  end
75
101
  end
@@ -90,6 +90,11 @@ module RuboCop
90
90
  # remove_foreign_key :accounts, :branches
91
91
  # end
92
92
  #
93
+ # # good
94
+ # def change
95
+ # remove_foreign_key :accounts, to_table: :branches
96
+ # end
97
+ #
93
98
  # @example
94
99
  # # change_table
95
100
  #
@@ -210,7 +215,7 @@ module RuboCop
210
215
 
211
216
  def check_remove_foreign_key_node(node)
212
217
  remove_foreign_key_call(node) do |arg|
213
- if arg.hash_type?
218
+ if arg.hash_type? && !all_hash_key?(arg, :to_table)
214
219
  add_offense(
215
220
  node,
216
221
  message: format(MSG,
@@ -0,0 +1,133 @@
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 unless schema
36
+ return if with_index?(node)
37
+
38
+ add_offense(node)
39
+ end
40
+
41
+ private
42
+
43
+ def with_index?(node)
44
+ klass = class_node(node)
45
+ return true unless klass # Skip analysis
46
+
47
+ table = schema.table_by(name: table_name(klass))
48
+ return true unless table # Skip analysis if it can't find the table
49
+
50
+ names = column_names(node)
51
+ return true unless names
52
+
53
+ table.indices.any? do |index|
54
+ index.unique && index.columns.to_set == names
55
+ end
56
+ end
57
+
58
+ def column_names(node)
59
+ arg = node.first_argument
60
+ return unless arg.str_type? || arg.sym_type?
61
+
62
+ ret = [arg.value]
63
+ names_from_scope = column_names_from_scope(node)
64
+ ret.concat(names_from_scope) if names_from_scope
65
+
66
+ ret.map! do |name|
67
+ klass = class_node(node)
68
+ resolve_relation_into_column(
69
+ name: name.to_s,
70
+ class_node: klass,
71
+ table: schema.table_by(name: table_name(klass))
72
+ )
73
+ end
74
+ ret.include?(nil) ? nil : ret.to_set
75
+ end
76
+
77
+ def column_names_from_scope(node)
78
+ uniq = uniqueness_part(node)
79
+ return unless uniq.hash_type?
80
+
81
+ scope = find_scope(uniq)
82
+ return unless scope
83
+
84
+ case scope.type
85
+ when :sym, :str
86
+ [scope.value]
87
+ when :array
88
+ array_node_to_array(scope)
89
+ end
90
+ end
91
+
92
+ def find_scope(pairs)
93
+ pairs.each_pair.find do |pair|
94
+ key = pair.key
95
+ next unless key.sym_type? && key.value == :scope
96
+
97
+ break pair.value
98
+ end
99
+ end
100
+
101
+ def class_node(node)
102
+ node.each_ancestor.find(&:class_type?)
103
+ end
104
+
105
+ def uniqueness_part(node)
106
+ pairs = node.arguments.last
107
+ return unless pairs.hash_type?
108
+
109
+ pairs.each_pair.find do |pair|
110
+ next unless pair.key.sym_type? && pair.key.value == :uniqueness
111
+
112
+ break pair.value
113
+ end
114
+ end
115
+
116
+ def array_node_to_array(node)
117
+ node.values.map do |elm|
118
+ case elm.type
119
+ when :str, :sym
120
+ elm.value
121
+ else
122
+ return nil
123
+ end
124
+ end
125
+ end
126
+
127
+ def schema
128
+ RuboCop::Rails::SchemaLoader.load(target_ruby_version)
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -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'
@@ -32,6 +34,8 @@ require_relative 'rails/helper_instance_variable'
32
34
  require_relative 'rails/http_positional_arguments'
33
35
  require_relative 'rails/http_status'
34
36
  require_relative 'rails/ignored_skip_action_filter_option'
37
+ require_relative 'rails/index_by'
38
+ require_relative 'rails/index_with'
35
39
  require_relative 'rails/inverse_of'
36
40
  require_relative 'rails/lexically_scoped_action_filter'
37
41
  require_relative 'rails/link_to_blank'
@@ -57,5 +61,6 @@ require_relative 'rails/scope_args'
57
61
  require_relative 'rails/skips_model_validations'
58
62
  require_relative 'rails/time_zone'
59
63
  require_relative 'rails/uniq_before_pluck'
64
+ require_relative 'rails/unique_validation_without_index'
60
65
  require_relative 'rails/unknown_env'
61
66
  require_relative 'rails/validation'
@@ -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
+ private
28
+
29
+ def load!(target_ruby_version)
30
+ path = db_schema_path
31
+ return unless path
32
+
33
+ ast = parse(path, target_ruby_version)
34
+ Schema.new(ast)
35
+ end
36
+
37
+ def db_schema_path
38
+ path = Pathname.pwd
39
+ until path.root?
40
+ schema_path = path.join('db/schema.rb')
41
+ return schema_path if schema_path.exist?
42
+
43
+ path = path.join('../').cleanpath
44
+ end
45
+
46
+ nil
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,158 @@
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
9
+
10
+ def initialize(ast)
11
+ @tables = []
12
+ build!(ast)
13
+ end
14
+
15
+ def table_by(name:)
16
+ tables.find do |table|
17
+ table.name == name
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def build!(ast)
24
+ raise "Unexpected type: #{ast.type}" unless ast.block_type?
25
+
26
+ each_table(ast) do |table_def|
27
+ @tables << Table.new(table_def)
28
+ end
29
+ end
30
+
31
+ def each_table(ast)
32
+ case ast.body.type
33
+ when :begin
34
+ ast.body.children.each do |node|
35
+ next unless node.block_type? && node.method?(:create_table)
36
+
37
+ yield(node)
38
+ end
39
+ else
40
+ yield ast.body
41
+ end
42
+ end
43
+ end
44
+
45
+ # Reprecent a table
46
+ class Table
47
+ attr_reader :name, :columns, :indices
48
+
49
+ def initialize(node)
50
+ @name = node.send_node.first_argument.value
51
+ @columns = build_columns(node)
52
+ @indices = build_indices(node)
53
+ end
54
+
55
+ def with_column?(name:)
56
+ @columns.any? { |c| c.name == name }
57
+ end
58
+
59
+ private
60
+
61
+ def build_columns(node)
62
+ each_content(node).map do |child|
63
+ next unless child.send_type?
64
+ next if child.method?(:index)
65
+
66
+ Column.new(child)
67
+ end.compact
68
+ end
69
+
70
+ def build_indices(node)
71
+ each_content(node).map do |child|
72
+ next unless child.send_type?
73
+ next unless child.method?(:index)
74
+
75
+ Index.new(child)
76
+ end.compact
77
+ end
78
+
79
+ def each_content(node)
80
+ return enum_for(__method__, node) unless block_given?
81
+
82
+ case node.body.type
83
+ when :begin
84
+ node.body.children.each do |child|
85
+ yield(child)
86
+ end
87
+ else
88
+ yield(node.body)
89
+ end
90
+ end
91
+ end
92
+
93
+ # Reprecent a column
94
+ class Column
95
+ attr_reader :name, :type, :not_null
96
+
97
+ def initialize(node)
98
+ @name = node.first_argument.value
99
+ @type = node.method_name
100
+ @not_null = nil
101
+
102
+ analyze_keywords!(node)
103
+ end
104
+
105
+ private
106
+
107
+ def analyze_keywords!(node)
108
+ pairs = node.arguments.last
109
+ return unless pairs.hash_type?
110
+
111
+ pairs.each_pair do |k, v|
112
+ if k.value == :null
113
+ @not_null = v.true_type? ? false : true
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ # Reprecent an index
120
+ class Index
121
+ attr_reader :name, :columns, :expression, :unique
122
+
123
+ def initialize(node)
124
+ node.first_argument
125
+ @columns, @expression = build_columns_or_expr(node)
126
+ @unique = nil
127
+
128
+ analyze_keywords!(node)
129
+ end
130
+
131
+ private
132
+
133
+ def build_columns_or_expr(node)
134
+ arg = node.first_argument
135
+ if arg.array_type?
136
+ [arg.values.map(&:value), nil]
137
+ else
138
+ [[], arg.value]
139
+ end
140
+ end
141
+
142
+ def analyze_keywords!(node)
143
+ pairs = node.arguments.last
144
+ return unless pairs.hash_type?
145
+
146
+ pairs.each_pair do |k, v|
147
+ case k.value
148
+ when :name
149
+ @name = v.value
150
+ when :unique
151
+ @unique = true
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -4,7 +4,7 @@ module RuboCop
4
4
  module Rails
5
5
  # This module holds the RuboCop Rails version information.
6
6
  module Version
7
- STRING = '2.4.2'
7
+ STRING = '2.5.0'
8
8
  end
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.2
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bozhidar Batsov
@@ -10,8 +10,22 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2020-01-26 00:00:00.000000000 Z
13
+ date: 2020-03-23 00:00:00.000000000 Z
14
14
  dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activesupport
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '0'
15
29
  - !ruby/object:Gem::Dependency
16
30
  name: rack
17
31
  requirement: !ruby/object:Gem::Requirement
@@ -55,6 +69,8 @@ files:
55
69
  - bin/setup
56
70
  - config/default.yml
57
71
  - lib/rubocop-rails.rb
72
+ - lib/rubocop/cop/mixin/active_record_helper.rb
73
+ - lib/rubocop/cop/mixin/index_method.rb
58
74
  - lib/rubocop/cop/mixin/target_rails_version.rb
59
75
  - lib/rubocop/cop/rails/action_filter.rb
60
76
  - lib/rubocop/cop/rails/active_record_aliases.rb
@@ -86,6 +102,8 @@ files:
86
102
  - lib/rubocop/cop/rails/http_positional_arguments.rb
87
103
  - lib/rubocop/cop/rails/http_status.rb
88
104
  - lib/rubocop/cop/rails/ignored_skip_action_filter_option.rb
105
+ - lib/rubocop/cop/rails/index_by.rb
106
+ - lib/rubocop/cop/rails/index_with.rb
89
107
  - lib/rubocop/cop/rails/inverse_of.rb
90
108
  - lib/rubocop/cop/rails/lexically_scoped_action_filter.rb
91
109
  - lib/rubocop/cop/rails/link_to_blank.rb
@@ -111,20 +129,23 @@ files:
111
129
  - lib/rubocop/cop/rails/skips_model_validations.rb
112
130
  - lib/rubocop/cop/rails/time_zone.rb
113
131
  - lib/rubocop/cop/rails/uniq_before_pluck.rb
132
+ - lib/rubocop/cop/rails/unique_validation_without_index.rb
114
133
  - lib/rubocop/cop/rails/unknown_env.rb
115
134
  - lib/rubocop/cop/rails/validation.rb
116
135
  - lib/rubocop/cop/rails_cops.rb
117
136
  - lib/rubocop/rails.rb
118
137
  - lib/rubocop/rails/inject.rb
138
+ - lib/rubocop/rails/schema_loader.rb
139
+ - lib/rubocop/rails/schema_loader/schema.rb
119
140
  - lib/rubocop/rails/version.rb
120
141
  homepage: https://github.com/rubocop-hq/rubocop-rails
121
142
  licenses:
122
143
  - MIT
123
144
  metadata:
124
- homepage_uri: https://docs.rubocop.org/projects/rails
145
+ homepage_uri: https://docs.rubocop.org/projects/rails/
125
146
  changelog_uri: https://github.com/rubocop-hq/rubocop-rails/blob/master/CHANGELOG.md
126
147
  source_code_uri: https://github.com/rubocop-hq/rubocop-rails/
127
- documentation_uri: https://docs.rubocop.org/projects/rails
148
+ documentation_uri: https://docs.rubocop.org/projects/rails/
128
149
  bug_tracker_uri: https://github.com/rubocop-hq/rubocop-rails/issues
129
150
  post_install_message:
130
151
  rdoc_options: []