Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle to_h with block in Style/HashTransformKeys and Style/HashTransformValues #8517

Merged
merged 1 commit into from Aug 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,7 @@

* [#8362](https://github.com/rubocop-hq/rubocop/issues/8362): Add numbers of correctable offenses to summary. ([@nguyenquangminh0711][])
* [#8513](https://github.com/rubocop-hq/rubocop/pull/8513): Clarify the ruby warning mentioned in the `Lint/ShadowingOuterLocalVariable` documentation. ([@chocolateboy][])
* [#8517](https://github.com/rubocop-hq/rubocop/pull/8517): Make `Style/HashTransformKeys` and `Style/HashTransformValues` aware of `to_h` with block. ([@eugeneius][])

## 0.89.1 (2020-08-10)

Expand Down
6 changes: 4 additions & 2 deletions config/default.yml
Expand Up @@ -3101,15 +3101,17 @@ Style/HashSyntax:
PreferHashRocketsForNonAlnumEndingSymbols: false

Style/HashTransformKeys:
Description: 'Prefer `transform_keys` over `each_with_object` and `map`.'
Description: 'Prefer `transform_keys` over `each_with_object`, `map`, or `to_h`.'
Enabled: 'pending'
VersionAdded: '0.80'
VersionChanged: '0.90'
Safe: false

Style/HashTransformValues:
Description: 'Prefer `transform_values` over `each_with_object` and `map`.'
Description: 'Prefer `transform_values` over `each_with_object`, `map`, or `to_h`.'
Enabled: 'pending'
VersionAdded: '0.80'
VersionChanged: '0.90'
Safe: false

Style/IdenticalConditionalBranches:
Expand Down
12 changes: 8 additions & 4 deletions docs/modules/ROOT/pages/cops_style.adoc
Expand Up @@ -3794,7 +3794,7 @@ NOTE: Required Ruby version: 2.5
| No
| Yes (Unsafe)
| 0.80
| -
| 0.90
|===

This cop looks for uses of `_.each_with_object({}) {...}`,
Expand All @@ -3815,7 +3815,9 @@ This cop should only be enabled on Ruby version 2.5 or newer
----
# bad
{a: 1, b: 2}.each_with_object({}) { |(k, v), h| h[foo(k)] = v }
{a: 1, b: 2}.map { |k, v| [k.to_s, v] }
Hash[{a: 1, b: 2}.collect { |k, v| [foo(k), v] }]
{a: 1, b: 2}.map { |k, v| [k.to_s, v] }.to_h
{a: 1, b: 2}.to_h { |k, v| [k.to_s, v] }

# good
{a: 1, b: 2}.transform_keys { |k| foo(k) }
Expand All @@ -3831,7 +3833,7 @@ This cop should only be enabled on Ruby version 2.5 or newer
| No
| Yes (Unsafe)
| 0.80
| -
| 0.90
|===

This cop looks for uses of `_.each_with_object({}) {...}`,
Expand All @@ -3852,7 +3854,9 @@ This cop should only be enabled on Ruby version 2.4 or newer
----
# bad
{a: 1, b: 2}.each_with_object({}) { |(k, v), h| h[k] = foo(v) }
{a: 1, b: 2}.map { |k, v| [k, v * v] }
Hash[{a: 1, b: 2}.collect { |k, v| [k, foo(v)] }]
{a: 1, b: 2}.map { |k, v| [k, v * v] }.to_h
{a: 1, b: 2}.to_h { |k, v| [k, v * v] }

# good
{a: 1, b: 2}.transform_values { |v| foo(v) }
Expand Down
17 changes: 17 additions & 0 deletions lib/rubocop/cop/mixin/hash_transform_method.rb
Expand Up @@ -9,6 +9,12 @@ def on_block(node)
on_bad_each_with_object(node) do |*match|
handle_possible_offense(node, match, 'each_with_object')
end

return if target_ruby_version < 2.6

on_bad_to_h(node) do |*match|
handle_possible_offense(node, match, 'to_h {...}')
end
end

def on_send(node)
Expand Down Expand Up @@ -43,6 +49,11 @@ def on_bad_map_to_h(_node)
raise NotImplementedError
end

# @abstract Implemented with `def_node_matcher`
def on_bad_to_h(_node)
raise NotImplementedError
end

def handle_possible_offense(node, match, match_desc)
captures = extract_captures(match)

Expand Down Expand Up @@ -82,6 +93,8 @@ def prepare_correction(node)
Autocorrection.from_hash_brackets_map(node, match)
elsif (match = on_bad_map_to_h(node))
Autocorrection.from_map_to_h(node, match)
elsif (match = on_bad_to_h(node))
Autocorrection.from_to_h(node, match)
else
raise 'unreachable'
end
Expand Down Expand Up @@ -137,6 +150,10 @@ def self.from_map_to_h(node, match)
new(match, node.children.first, 0, strip_trailing_chars)
end

def self.from_to_h(node, match)
new(match, node, 0, 0)
end

def strip_prefix_and_suffix(node, corrector)
expression = node.loc.expression
corrector.remove_leading(expression, leading)
Expand Down
15 changes: 14 additions & 1 deletion lib/rubocop/cop/style/hash_transform_keys.rb
Expand Up @@ -18,7 +18,9 @@ module Style
# @example
# # bad
# {a: 1, b: 2}.each_with_object({}) { |(k, v), h| h[foo(k)] = v }
# {a: 1, b: 2}.map { |k, v| [k.to_s, v] }
# Hash[{a: 1, b: 2}.collect { |k, v| [foo(k), v] }]
# {a: 1, b: 2}.map { |k, v| [k.to_s, v] }.to_h
# {a: 1, b: 2}.to_h { |k, v| [k.to_s, v] }
#
# # good
# {a: 1, b: 2}.transform_keys { |k| foo(k) }
Expand Down Expand Up @@ -68,6 +70,17 @@ class HashTransformKeys < Base
:to_h)
PATTERN

def_node_matcher :on_bad_to_h, <<~PATTERN
(block
({send csend}
!{(send _ :each_with_index) (array ...)}
:to_h)
(args
(arg $_)
(arg _val))
(array $_ $(lvar _val)))
PATTERN

private

def extract_captures(match)
Expand Down
15 changes: 14 additions & 1 deletion lib/rubocop/cop/style/hash_transform_values.rb
Expand Up @@ -18,7 +18,9 @@ module Style
# @example
# # bad
# {a: 1, b: 2}.each_with_object({}) { |(k, v), h| h[k] = foo(v) }
# {a: 1, b: 2}.map { |k, v| [k, v * v] }
# Hash[{a: 1, b: 2}.collect { |k, v| [k, foo(v)] }]
# {a: 1, b: 2}.map { |k, v| [k, v * v] }.to_h
# {a: 1, b: 2}.to_h { |k, v| [k, v * v] }
#
# # good
# {a: 1, b: 2}.transform_values { |v| foo(v) }
Expand Down Expand Up @@ -65,6 +67,17 @@ class HashTransformValues < Base
:to_h)
PATTERN

def_node_matcher :on_bad_to_h, <<~PATTERN
(block
({send csend}
!{(send _ :each_with_index) (array ...)}
:to_h)
(args
(arg _key)
(arg $_))
(array $(lvar _key) $_))
PATTERN

private

def extract_captures(match)
Expand Down
21 changes: 21 additions & 0 deletions spec/rubocop/cop/style/hash_transform_keys_spec.rb
Expand Up @@ -171,4 +171,25 @@
expect_no_offenses('x.each_with_object({}) {|(k, v), h| h[foo(k)] = v}')
end
end

context 'when using Ruby 2.6 or newer', :ruby26 do
it 'flags _.to_h{...} when transform_keys could be used' do
expect_offense(<<~RUBY)
x.to_h {|k, v| [k.to_sym, v]}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `transform_keys` over `to_h {...}`.
RUBY

expect_correction(<<~RUBY)
x.transform_keys {|k| k.to_sym}
RUBY
end
end

context 'below Ruby 2.6', :ruby25 do
it 'does not flag _.to_h{...}' do
expect_no_offenses(<<~RUBY)
x.to_h {|k, v| [k.to_sym, v]}
RUBY
end
end
end
21 changes: 21 additions & 0 deletions spec/rubocop/cop/style/hash_transform_values_spec.rb
Expand Up @@ -133,4 +133,25 @@
{a: 1, b: 2}.transform_values {|v| foo(v)}.to_h {|k, v| [v, k]}
RUBY
end

context 'when using Ruby 2.6 or newer', :ruby26 do
it 'flags _.to_h{...} when transform_values could be used' do
expect_offense(<<~RUBY)
x.to_h {|k, v| [k, foo(v)]}
^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `transform_values` over `to_h {...}`.
RUBY

expect_correction(<<~RUBY)
x.transform_values {|v| foo(v)}
RUBY
end
end

context 'below Ruby 2.6', :ruby25 do
it 'does not flag _.to_h{...}' do
expect_no_offenses(<<~RUBY)
x.to_h {|k, v| [k, foo(v)]}
RUBY
end
end
end