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 IgnoreLiteralBranches and IgnoreConstantBranches options to Lint/DuplicateBranch #9193

Merged
merged 2 commits into from
Dec 16, 2020

Conversation

dvandersluis
Copy link
Member

@dvandersluis dvandersluis commented Dec 8, 2020

I noticed that the standard gem disabled Lint/DuplicateBranch and the straw that broke the camel's back for them was that this kind of case layout registered an offense:

case size
when "small" then 100
when "medium" then 250
when "large" then 1000
else 250
end

While it's true that the medium case and the else case are duplicates, I'd argue that this duplication is beneficial because it clearly outlines what possible values are expected.

Introduced a new configuration, IgnoreLiteralBranches, which when true will not consider any branches that just return a basic literal value, or a composite literal (array, hash, etc.) that only contains basic literals (see test for examples). As well, added IgnoreConstantBranches to allow branches that just return a constant to be ignored.


Before submitting the PR make sure the following are checked:

  • The PR relates to only one subject with a clear title and description in grammatically correct, complete sentences.
  • Wrote good commit messages.
  • Commit message starts with [Fix #issue-number] (if the related issue exists).
  • Feature branch is up-to-date with master (if not - rebase it).
  • Squashed related commits together.
  • Added tests.
  • Ran bundle exec rake default. It executes all tests and runs RuboCop on its own code.
  • Added an entry (file) to the changelog folder named {change_type}_{change_description}.md if the new code introduces user-observable changes. See changelog entry format for details.

@bbatsov
Copy link
Collaborator

bbatsov commented Dec 8, 2020

I'm fine with the proposed change, although we might want to limit it only to case that dispatches on some value, as you can write a very if-like case.

@bbatsov
Copy link
Collaborator

bbatsov commented Dec 8, 2020

I noticed that the standard gem disabled Lint/DuplicateBranch and the straw that broke the camel's back for them was that this kind of case layout registered an offense:

It's great you've spotted this, but it's a pitty they don't reach out to share their concerns directly.

@dvandersluis
Copy link
Member Author

Yeah even though I wouldn't want to be forced into a config personally, I take a peek there every once in a while to see if there's anything we can fix.

@dvandersluis
Copy link
Member Author

@bbatsov updated, cases without a value are now still offenses even with the option set.

@marcandre
Copy link
Contributor

marcandre commented Dec 8, 2020

What I dislike about that cop is that it doesn't differentiate between simple literals and the rest.}

E.g.: I think this code would benefit from a refactor:

case size
when "small" then do_something(100)
when "medium" then do_something_else(250)
when "large" then extra_processing
else do_something_else(250)
end

I have no objection with the following, even though the repetition is not in the else. Assume that the predicate order can not be changed.

case
when some_test? then :ignore
when some_sub_case? then :critical
when most_other_cases? then :ignore
else :normal_priority
end

In short: I'm not convinced that the best config is distinguishing the else vs other branches. I also think that a IgnoreSimpleLiterals or similar might be better, and make the AllowCaseElseDuplication config not needed.

@bbatsov
Copy link
Collaborator

bbatsov commented Dec 9, 2020

Hmm, I think I like this idea better, although I'd probably name the config "IgnoreTrivial/LiteralBranches". It's true that if a branch is just a literal probably the duplication is not that big of a deal.

@dvandersluis
Copy link
Member Author

@marcandre @bbatsov thanks for the input! I think there is more to be done to this cop to make it more useful for a wider variety of cases.

  1. IgnoreLiteralBranches sounds good but also there's an argument here that refactoring the duplicate branches into a single branch would be an improvement:

    case size
    when 'small' then 10
    when 'medium' then 10
    when 'large' then 100
    end
  2. @marcandre's example of:

    case
    when some_test? then :ignore
    when some_sub_case? then :critical
    when most_other_cases? then :ignore
    else :normal_priority
    end

    would still register an offense with IgnoreLiteralBranches (if I'm not misunderstanding the intention there; method calls are not literals). I think an additional configuration (IgnoreValuelessCase? Enabled by default?) to disable the cop fully for non-value cases would help here. Given that the method calls could also have side effects, rewriting this code would be unsafe.

  3. I think having a way to disable duplicate checking for else branches is still useful with the above two. It's plausible someone would want IgnoreLiteralBranches: false but still have the original example be acceptable.

If there's no objection I'll update the cop to allow for all three configurations.

@marcandre
Copy link
Contributor

1. `IgnoreLiteralBranches` sounds good but also there's an argument here that refactoring the duplicate branches into a single branch would be an improvement [...]
2. @marcandre's example of [...]  would still register an offense with `IgnoreLiteralBranches`

Sorry I wasn't clear. My first example => offense. Second example => ok. The duplication checks the branch bodies, not the conditions. So my second example is a case that might not be easy / clearer to reorder the branches (complex conditions) but where branch bodies are basic literals.

I see no harm in duplicated basic literals bodies.

3. I think having a way to disable duplicate checking for else branches is _still_ useful with the above two. It's plausible someone would want `IgnoreLiteralBranches: false` but still have the original example be acceptable.

I don't, and I have a problem with duplicated non basic body branches (else or other), like my first example. @bbatsov what's your take?

@bbatsov
Copy link
Collaborator

bbatsov commented Dec 11, 2020

I also don't think that duplicated literal branches are big problem.

I think an additional configuration (IgnoreValuelessCase? Enabled by default?) to disable the cop fully for non-value cases would help here.

I can see some value there, but it's probably going to be an overkill. What I like about the literal branches idea is that it's simple, solves the original problem and doesn't require specialn handling for case.

@searls @rsanheim Feel free to share your thoughts as well.

@dvandersluis
Copy link
Member Author

dvandersluis commented Dec 11, 2020

Thanks, I think I mixed up the condition and body in my previous message. I'll implement IgnoreLiteralBranches, I think that's a good improvement.

Are we still ok with an offense being registered here:

case
when method_a then respond(:a)
when method_b then respond(:b)
when method_c then respond(:a)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Duplicate branch body detected.
end

@marcandre
Copy link
Contributor

Thanks, I think I mixed up the condition and body in my previous message. I'll implement IgnoreLiteralBranches, I think that's a good improvement.

Are we still ok with an offense being registered here:

case
when method_a then respond(:a)
when method_b then respond(:b)
when method_c then respond(:a)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Duplicate branch body detected.
end

Yes, and even if when method_c was an else.

@dvandersluis
Copy link
Member Author

I've rewritten my changes to implement IgnoreLiteralBranches instead now.

@dvandersluis dvandersluis changed the title Add AllowCaseElseDuplication option to Lint/DuplicateBranch. Add IgnoreLiteralBranches option to Lint/DuplicateBranch. Dec 14, 2020
return false if branch.basic_literal?
return true if branch.xstr_type?

branch.each_descendant.any? do |node|
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not 100% sure, but I would exclude hash literals however simple they are (same as arrays).
Are you allowing constants? I think we should.

Copy link
Member Author

Choose a reason for hiding this comment

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

I added IgnoreConstantBranches to ignore constants now.

I think it's more consistent to let IgnoreLiteralBranches ignore all literals including hashes and arrays. @bbatsov what do you think?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree. It's uncommon to have calculations done in literals, so I'd stick with a simple and consistent definition.

Copy link
Contributor

Choose a reason for hiding this comment

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

I really don't think there should be a separate setting for constants.

Thinking about it more, in my mind it's actually not about being a literal but an already simple and well-defined object. As such a variable (not allowed currently, right?) / constant / integer / symbol are all acceptable. These involve no processing and no memory allocation. There's no good reason to factorize them, i.e. to store them / label them in one place before using them in multiple places. I'm not sure I have a name for this though. Atom? Interned? Simple values?

Just a last example, before you decide to keep hashes and arrays:

case
when method_a
  { hello: 'world',
    :does_it? => 'make sense',
    to: :accept_this,
  }
when method_b
  { hi: 'world',
    :does_it? => 'make sense',
    to: :accept_this,
  }
when method_c then
  { hello: 'world',
    :does_it? => 'make sense',
    to: :accept_this,
  }
end

Compare this to:

case
when method_a then A
when method_b then B
when method_c then C
else B
end

Same with A/B/C/B being variables.

Copy link
Collaborator

@bbatsov bbatsov Dec 16, 2020

Choose a reason for hiding this comment

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

Yeah, I totally get that those scenarios would be possible, but they are not very common and anything we define as "trivial" or whatever would carry some cognitive overhead as people have to understand our definition of trivial. I'd rather go with something that leaves no room for interpretation and solves the problem more or less. Plus, we're not changing the defaults. :-)

Copy link
Member Author

Choose a reason for hiding this comment

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

Agree with @bbatsov. The initial idea here was just to make this less restrictive based on standard's issue, which I thought was valid. I've iterated on this three times now but I think we're getting into nitpicking so I'd rather scope hammer this back to the original goal.

@dvandersluis dvandersluis changed the title Add IgnoreLiteralBranches option to Lint/DuplicateBranch. Add IgnoreLiteralBranches and IgnoreConstantBranches options to Lint/DuplicateBranch Dec 16, 2020
@bbatsov bbatsov merged commit b01dcfc into rubocop:master Dec 16, 2020
@bbatsov
Copy link
Collaborator

bbatsov commented Dec 16, 2020

Thanks for tackling this! 🙇‍♂️

@dvandersluis dvandersluis deleted the duplicate-branch branch December 16, 2020 17:20
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

3 participants