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

Add new Style/CaseLikeIf cop #8266

Merged
merged 1 commit into from Jul 9, 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 @@ -5,6 +5,7 @@
### New features

* [#8242](https://github.com/rubocop-hq/rubocop/pull/8242): Internal profiling available with `bin/rubocop-profile` and rake tasks. ([@marcandre][])
* [#7736](https://github.com/rubocop-hq/rubocop/issues/7736): Add new `Style/CaseLikeIf` cop. ([@fatkodima][])
* [#4286](https://github.com/rubocop-hq/rubocop/issues/4286): Add new `Style/HashAsLastArrayItem` cop. ([@fatkodima][])

### Bug fixes
Expand Down
6 changes: 6 additions & 0 deletions config/default.yml
Expand Up @@ -2522,6 +2522,12 @@ Style/CaseEquality:
# String === "string"
AllowOnConstant: false

Style/CaseLikeIf:
Description: 'This cop identifies places where `if-elsif` constructions can be replaced with `case-when`.'
StyleGuide: '#case-vs-if-else'
Enabled: 'pending'
VersionAdded: '0.88'

Style/CharacterLiteral:
Description: 'Checks for uses of character literals.'
StyleGuide: '#no-character-literals'
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/cops.adoc
Expand Up @@ -326,6 +326,7 @@ In the following section you find all available cops:
* xref:cops_style.adoc#styleblockcomments[Style/BlockComments]
* xref:cops_style.adoc#styleblockdelimiters[Style/BlockDelimiters]
* xref:cops_style.adoc#stylecaseequality[Style/CaseEquality]
* xref:cops_style.adoc#stylecaselikeif[Style/CaseLikeIf]
* xref:cops_style.adoc#stylecharacterliteral[Style/CharacterLiteral]
* xref:cops_style.adoc#styleclassandmodulechildren[Style/ClassAndModuleChildren]
* xref:cops_style.adoc#styleclasscheck[Style/ClassCheck]
Expand Down
43 changes: 43 additions & 0 deletions docs/modules/ROOT/pages/cops_style.adoc
Expand Up @@ -826,6 +826,49 @@ some_string =~ /something/

* https://rubystyle.guide#no-case-equality

== Style/CaseLikeIf

|===
| Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged

| Pending
| Yes
| Yes
| 0.88
| -
|===

This cop identifies places where `if-elsif` constructions
can be replaced with `case-when`.

=== Examples

[source,ruby]
----
# bad
if status == :active
perform_action
elsif status == :inactive || status == :hibernating
check_timeout
else
final_action
end

# good
case status
when :active
perform_action
when :inactive, :hibernating
check_timeout
else
final_action
end
----

=== References

* https://rubystyle.guide#case-vs-if-else

== Style/CharacterLiteral

|===
Expand Down
1 change: 1 addition & 0 deletions lib/rubocop.rb
Expand Up @@ -370,6 +370,7 @@
require_relative 'rubocop/cop/style/block_comments'
require_relative 'rubocop/cop/style/block_delimiters'
require_relative 'rubocop/cop/style/case_equality'
require_relative 'rubocop/cop/style/case_like_if'
require_relative 'rubocop/cop/style/character_literal'
require_relative 'rubocop/cop/style/class_and_module_children'
require_relative 'rubocop/cop/style/class_check'
Expand Down
5 changes: 3 additions & 2 deletions lib/rubocop/cop/layout/end_alignment.rb
Expand Up @@ -150,9 +150,10 @@ def check_other_alignment(node)
end

def alignment_node(node)
if style == :keyword
case style
when :keyword
node
elsif style == :variable
when :variable
alignment_node_for_variable_style(node)
else
start_line_range(node)
Expand Down
5 changes: 3 additions & 2 deletions lib/rubocop/cop/layout/space_around_block_parameters.rb
Expand Up @@ -56,11 +56,12 @@ def style_parameter_name
def check_inside_pipes(arguments)
opening_pipe, closing_pipe = pipes(arguments)

if style == :no_space
case style
when :no_space
check_no_space_style_inside_pipes(arguments.children,
opening_pipe,
closing_pipe)
elsif style == :space
when :space
check_space_style_inside_pipes(arguments.children,
opening_pipe,
closing_pipe)
Expand Down
5 changes: 3 additions & 2 deletions lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb
Expand Up @@ -142,11 +142,12 @@ def line_and_column_for(token)
end

def issue_offenses(node, left, right, start_ok, end_ok)
if style == :no_space
case style
when :no_space
start_ok = next_to_comment?(node, left)
no_space_offenses(node, left, right, MSG, start_ok: start_ok,
end_ok: end_ok)
elsif style == :space
when :space
space_offenses(node, left, right, MSG, start_ok: start_ok,
end_ok: end_ok)
else
Expand Down
5 changes: 3 additions & 2 deletions lib/rubocop/cop/lint/implicit_string_concatenation.rb
Expand Up @@ -64,9 +64,10 @@ def each_bad_cons(node)

def ending_delimiter(str)
# implicit string concatenation does not work with %{}, etc.
if str.source[0] == "'"
case str.source[0]
when "'"
"'"
elsif str.source[0] == '"'
when '"'
'"'
end
end
Expand Down
217 changes: 217 additions & 0 deletions lib/rubocop/cop/style/case_like_if.rb
@@ -0,0 +1,217 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Style
# This cop identifies places where `if-elsif` constructions
# can be replaced with `case-when`.
#
# @example
# # bad
# if status == :active
# perform_action
# elsif status == :inactive || status == :hibernating
# check_timeout
# else
# final_action
# end
#
# # good
# case status
# when :active
# perform_action
# when :inactive, :hibernating
# check_timeout
# else
# final_action
# end
#
class CaseLikeIf < Cop
include RangeHelp

MSG = 'Convert `if-elsif` to `case-when`.'

def on_if(node)
return unless should_check?(node)

target = find_target(node.condition)
return unless target

conditions = []
convertible = true

branch_conditions(node).each do |branch_condition|
conditions << []
convertible = collect_conditions(branch_condition, target, conditions.last)
break unless convertible
end

add_offense(node) if convertible
end

def autocorrect(node)
target = find_target(node.condition)

lambda do |corrector|
corrector.insert_before(node, "case #{target.source}\n#{indent(node)}")

branch_conditions(node).each do |branch_condition|
conditions = []
collect_conditions(branch_condition, target, conditions)

range = correction_range(branch_condition)
branch_replacement = "when #{conditions.map(&:source).join(', ')}"
corrector.replace(range, branch_replacement)
end
end
end

private

def should_check?(node)
!node.unless? && !node.elsif? && !node.modifier_form? && !node.ternary? &&
node.elsif_conditional?
end

# rubocop:disable Metrics/MethodLength
def find_target(node)
case node.type
when :begin
find_target(node.children.first)
when :or
find_target(node.lhs)
when :match_with_lvasgn
lhs, rhs = *node
if lhs.regexp_type?
rhs
elsif rhs.regexp_type?
lhs
end
when :send
find_target_in_send_node(node)
end
end
# rubocop:enable Metrics/MethodLength

def find_target_in_send_node(node)
case node.method_name
when :is_a?
node.receiver
when :==, :eql?, :equal?
find_target_in_equality_node(node)
when :===
node.arguments.first
when :include?, :cover?
receiver = deparenthesize(node.receiver)
node.arguments.first if receiver.range_type?
when :match, :match?
find_target_in_match_node(node)
end
end

def find_target_in_equality_node(node)
argument = node.arguments.first
receiver = node.receiver

if argument.literal? || const_reference?(argument)
receiver
elsif receiver.literal? || const_reference?(receiver)
argument
end
end

def find_target_in_match_node(node)
argument = node.arguments.first
receiver = node.receiver

if receiver.regexp_type?
argument
elsif argument.regexp_type?
receiver
end
end

def collect_conditions(node, target, conditions)
condition =
case node.type
when :begin
return collect_conditions(node.children.first, target, conditions)
when :or
return collect_conditions(node.lhs, target, conditions) &&
collect_conditions(node.rhs, target, conditions)
when :match_with_lvasgn
lhs, rhs = *node
condition_from_binary_op(lhs, rhs, target)
when :send
condition_from_send_node(node, target)
end

conditions << condition if condition
end

# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/CyclomaticComplexity
def condition_from_send_node(node, target)
case node.method_name
when :is_a?
node.arguments.first if node.receiver == target
when :==, :eql?, :equal?, :=~, :match, :match?
lhs, _method, rhs = *node
condition_from_binary_op(lhs, rhs, target)
when :===
lhs, _method, rhs = *node
lhs if rhs == target
when :include?, :cover?
receiver = deparenthesize(node.receiver)
receiver if receiver.range_type? && node.arguments.first == target
end
end
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/AbcSize

def condition_from_binary_op(lhs, rhs, target)
lhs = deparenthesize(lhs)
rhs = deparenthesize(rhs)

if lhs == target
rhs
elsif rhs == target
lhs
end
end

def branch_conditions(node)
conditions = []
while node&.if_type?
conditions << node.condition
node = node.else_branch
end
conditions
end

def const_reference?(node)
return false unless node.const_type?

name = node.children[1].to_s

# We can no be sure if, e.g. `C`, represents a constant or a class reference
name.length > 1 &&
name == name.upcase
end

def deparenthesize(node)
node = node.children.last while node.begin_type?
node
end

def correction_range(node)
range_between(node.parent.loc.keyword.begin_pos, node.loc.expression.end_pos)
end

def indent(node)
' ' * node.loc.column
end
end
end
end
end
5 changes: 3 additions & 2 deletions lib/rubocop/cop/style/redundant_sort.rb
Expand Up @@ -135,9 +135,10 @@ def base(accessor, arg)
end

def suffix(sorter)
if sorter == :sort
case sorter
when :sort
''
elsif sorter == :sort_by
when :sort_by
'_by'
end
end
Expand Down
5 changes: 3 additions & 2 deletions lib/rubocop/cop/style/stabby_lambda_parentheses.rb
Expand Up @@ -34,9 +34,10 @@ def on_send(node)
end

def autocorrect(node)
if style == :require_parentheses
case style
when :require_parentheses
missing_parentheses_corrector(node)
elsif style == :require_no_parentheses
when :require_no_parentheses
unwanted_parentheses_corrector(node)
end
end
Expand Down