rubocop-rails 2.4.2 → 2.5.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.
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: []