Skip to content

Commit

Permalink
Add new Style/CaseLikeIf cop
Browse files Browse the repository at this point in the history
  • Loading branch information
fatkodima committed Jul 9, 2020
1 parent 62c5849 commit 202fec8
Show file tree
Hide file tree
Showing 13 changed files with 599 additions and 12 deletions.
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

0 comments on commit 202fec8

Please sign in to comment.