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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new Lint/DuplicateBranch cop #8404

Merged
merged 1 commit into from Nov 6, 2020

Conversation

fatkodima
Copy link
Contributor

This cop checks that there are no repeated bodies within if/unless, case-when and rescue constructs. Those can be as a result of refactoring, coding mistake or just badly repeated code.

# bad
if foo
  do_foo
  do_something_else
elsif bar
  do_foo
  do_something_else
end

# good
if foo && bar
  do_foo
  do_something_else
end

It does not consider simple repeated bodies, like basic literals, variable references, constant references and simple method calls without arguments. As otherwise this would force to probably make code uglier. Consider this example: https://github.com/rubocop-hq/rubocop/blob/974b2b1d28d66725407df9a9a0927ad50ced1d00/lib/rubocop/cop/layout/block_alignment.rb#L210-L217

Fun fact: I have unintentionally repeated branch within on_if method and this cop helped me to identify this 馃槃 So it is actually useful.

Inspired by similar checks from 2 different java linters: https://pmd.github.io/latest/pmd_rules_java_codestyle.html#identicalcatchbranches and https://spotbugs.readthedocs.io/en/latest/bugDescriptions.html#db-method-uses-the-same-code-for-two-branches-db-duplicate-branches

@marcandre
Copy link
Contributor

I thought we already had such a cop?

Copy link
Contributor

@marcandre marcandre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We really should have a all_branches (or just branches) on if and case nodes, and you could have one single method that does check(all_branches.compact) and that's it.

I think currently your logic can be simplified and would avoid potential issues. I didn't check, but I think:

case
when 1
foo
when 2
bar
else
bar  # undetected
end

Also, stuff like this is not spec'ed:

case
when 1
42
else
42 # => offense
end 
# but
case
when 1
42
when 2
42 # => no offense, is that what you want?
end

@fatkodima
Copy link
Contributor Author

I thought we already had such a cop?

I had the same thought, but no.

We really should have a all_branches (or just branches) on if and case nodes, and you could have one single method that does check(all_branches.compact) and that's it.

Would you like to make a PR into rubocop-ast? 馃槃

I didn't check, but I think:

case
when 1
foo
when 2
bar
else
bar  # undetected
end

Do you mean that when 2 branch should be merged into else in this case? I would prefer to stay with developer's choice to be explicit in this case and do not add offenses.

case
when 1
42
else
42 # => offense
end 

Yes, there is an offense in this case.

# but
case
when 1
42
when 2
42 # => no offense, is that what you want?
end

There is also an offense in this case too.

@marcandre
Copy link
Contributor

I thought we already had such a cop?

I had the same thought, but no.

What about Style/IdenticalConditionalBranches?

We really should have a all_branches (or just branches) on if and case nodes, and you could have one single method that does check(all_branches.compact) and that's it.

Would you like to make a PR into rubocop-ast? 馃槃

Sure

I didn't check, but I think:

case
when 1
foo
when 2
bar
else
bar  # undetected
end

Do you mean that when 2 branch should be merged into else in this case? I would prefer to stay with developer's choice to be explicit in this case and do not add offenses.

Ok

# but
case
when 1
42
when 2
42 # => no offense, is that what you want?
end

There is also an offense in this case too.

Are you sure? Won't it be excluded because of ignore_body?

@fatkodima
Copy link
Contributor Author

Both examples will be excluded because of ignore_body. I implicitly extended your examples to something more complex, like

# but
case
when 1
foo(42)
when 2
foo(42) # => no offense, is that what you want?
end

And in this case it will detect offense.

But for original example with basic literal as a body:

# but
case
when 1
42
when 2
42 # => no offense, is that what you want?
end

I think, I would prefer to not detect an offense because when branches will have complex (or just grouped) conditions and it will be easier to separate them, making that simple body repeated, like

case
when :several, :grouped, :conditions
  42
when :a, :couple, :of, :other, :related, :conditions
  42
when :something, :else
  50
end

@marcandre
Copy link
Contributor

marcandre commented Jul 26, 2020

I realize I write sometimes too precisely, but please do not simply assume that I intended something (say foo(42)) when I wrote something else (say 42) without enquiring first 馃槄. It's possible I didn't mean the distinction but often I do...

So, let me repeat differently what I wrote (although I only read the code quickly and could very well be mistaken)

  1. You have no specs for basic literals & al, which are treated differently, right?

  2. Looks like you treat differently case / when / else / end than case when / when / end with respect to basic literals & al. which seem wrong. You wrote "both examples will be excluded because of ignore_body." but I don't see that in the code, am I missing something?

Also, the question with how this relates to Style/IdenticalConditionalBranches remains (for example does that cop also treat basic literals differently)

@fatkodima
Copy link
Contributor Author

  1. I have only spec for call without arguments - https://github.com/rubocop-hq/rubocop/pull/8404/files#diff-132ee8b5e630ca664f5759368e630d9aR67-R75
    Yes, they are treated differently. I should add more specs to test them all.

  2. My mistake, will correct.

Also, the question with how this relates to Style/IdenticalConditionalBranches remains (for example does that cop also treat basic literals differently)

Seems like that cop is a little bit poorly named. It checks if all branches have the same start line or end line, which can be extracted altogether from conditional. New cop checks if any two or more branches have identical bodies. It doesn't matter for Style/IdenticalConditionalBranches which line to extract, if it is repeated, so it treats all lines the same.

@bbatsov
Copy link
Collaborator

bbatsov commented Nov 4, 2020

Where do we stand with this cop?

@fatkodima
Copy link
Contributor Author

@marcandre Please, review this again.

  1. I initially decided to handle simple bodies (like the bodies consisting only of basic literal, const reference, etc) differently and we had a long discussion about it. I decided agains differentiating between simple and non-simple bodies and handle them the same.
  2. You suggested

We really should have a all_branches (or just branches) on if and case nodes, and you could have one single method that does check(all_branches.compact) and that's it.

But seems like it is not a good idea to find duplicated bodies amongst normal branches and else branch at the same time:

if foo # 1. this condition is true
  do_foo
elsif bar # 2. and this condition is true
  do_bar
elsif baz
  do_foo
else
  do_foo
end

If we deduplicate branches in this case and merge 1 into else, then it will incorrectly execute 2 (instead of 1) condition branch.

And when we have multiple branches with the same body as else, then it is hard to deduplicate branches and really not needed.
The same goes for case-when and rescue-else.

@marcandre
Copy link
Contributor

But seems like it is not a good idea to find duplicated bodies amongst normal branches and else branch at the same time:

if foo # 1. this condition is true
  do_foo
elsif bar # 2. and this condition is true
  do_bar
elsif baz
  do_foo
else
  do_foo
end

To me the first question is: what should be flag. Then we can ask what can we reasonably auto-correct (and not auto-correcting complex cases is fine by me).
I still feel that (most) repeated bodies are a code smell (maybe trivial bodies like nil / true / false could be excluded, I'm not even sure)

I imagine your above example has a typo, as the if baz / else with identical bodies should obviously be simplified, and the whole case should IMO be simplified to:

if !foo && bar
  do_bar
else
  do_foo
end

@fatkodima fatkodima force-pushed the duplicate_branch-cop branch 2 times, most recently from 5c96f43 to 33af048 Compare November 4, 2020 22:16
@fatkodima
Copy link
Contributor Author

fatkodima commented Nov 4, 2020

I still feel that (most) repeated bodies are a code smell

Ok, I'm convinced. Updated the code to reflect that. Let's deliver this and see for some external feedback from other users?

For CI to pass, we need rubocop/rubocop-ast#146 to be released.

@fatkodima fatkodima force-pushed the duplicate_branch-cop branch 2 times, most recently from c6a17f3 to 1aa1e9d Compare November 5, 2020 15:57
Copy link
Contributor

@marcandre marcandre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 馃憤

|===

This cop checks that there are no repeated bodies
within `if/unless`, `case-when` and `rescue` constructs.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add some examples with case and rescue as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

Lint/DuplicateBranch:
Description: Checks that there are no repeated bodies within `if/unless`, `case-when` and `rescue` constructs.
Enabled: pending
VersionAdded: '1.2'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be 1.3.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, sorry, forgot to update this as well. So many new rubocop versions 馃槃

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have been on 馃敟 lately 馃槈

@bbatsov bbatsov merged commit b9b5853 into rubocop:master Nov 6, 2020
@bbatsov
Copy link
Collaborator

bbatsov commented Nov 6, 2020

Thanks!

@marckohlbrugge
Copy link

marckohlbrugge commented Nov 29, 2020

What happens if the order of the branches matters?

For example, I'm trying to determine what the commitment of a job listing is, based on its title. Here's a simplified example example:

    case title
    when /full/i
      Commitment::FULL_TIME
    when /\bcontract|speculative|freelanc/i
      Commitment::CONTRACTOR
    when /\bpermanent\b|employee|\bhire/i
      Commitment::FULL_TIME
    end

There are two duplicate code branches, but they serve a logical purpose:

If the string matches on both /full/i and/\bcontract|speculative|freelanc/i I consider it a full-time job listing. If it matches on /\bcontract|speculative|freelanc/i and /\bpermanent\b|employee|\bhire/i I consider it a contractor job. If it only matches on /\bpermanent\b|employee|\bhire/i I consider it a full-time job.

In other words, some matches are more sure than others. This level of certainty is encoded in the order of the different branches. So while the contents may be duplicated, the order of those branches plays a vital role in the code's behavior.

Is there a better way to write my code such that it doesn't violate this new RuboCop rule? (keep in mind that my actual code has a bunch more branches and commitment levels) Or should I just disable it here?

@marcandre
Copy link
Contributor

It would probably be a good idea to accept a case where all the branches' bodies are "enums" (i.e. constants / true / false / nil / symbols?), possibly with some (non-repeated) raise ... statemenets to be acceptable, even if repeated.

Either by default, or with a config option.

@drnic
Copy link

drnic commented Dec 2, 2020

Update: I understand now -- the when "update" branch is not required; not the else block referenced (by line number) in the warning.

I am getting a new warning for a case/else statement but its not obvious what rubocop is trying to say is wrong with my code:

module AnnouncementsHelper
  # Use the explicit class names so purgecss can find them
  def announcement_color(announcement)
    case announcement.kind
    when "new"
      "announcement-new"
    when "update"
      "announcement-update"
    when "improvement"
      "announcement-improvement"
    when "fix"
      "announcement-fix"
    else
      "announcement-update"
    end
  end

It does not like the else:

  app/helpers/announcements_helper.rb:13:5: Lint/DuplicateBranch: Duplicate branch body detected.

If this case/else is bad, what should it be refactored to?

@marckohlbrugge
Copy link

@drnic I was going to reply, but then saw you already updated your own comment with the answer. One thing I'll add though, is that while technically your "update"-branch is a duplicate, you might still decide to leave it in if you feel that better describes your intentions. If your goal is readability, then this minor duplication might be worth it.

@codesnik
Copy link

codesnik commented Mar 3, 2022

This cop somehow ignores the fact that when checks not for equality, but for ===, and evaluated in order. when could be used to check just for paraphyletic group, and it's more readable than explicit ifs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants