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

Added new Lint/TopLevelReturnWithArgument cop #8377

Merged
merged 18 commits into from Jul 24, 2020
Merged

Added new Lint/TopLevelReturnWithArgument cop #8377

merged 18 commits into from Jul 24, 2020

Conversation

IamRaviTejaG
Copy link
Contributor

Fixes #7425

This cop detects top level return statements with argument(s).


Before submitting the PR make sure the following are checked:

  • 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.
  • Added an entry to the Changelog if the new code introduces user-observable changes. See changelog entry format.
  • The PR relates to only one subject with a clear title and description in grammatically correct, complete sentences.
  • Run bundle exec rake default. It executes all tests and RuboCop for itself, and generates the documentation.

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.

Good PR!
A few comments. Biggest issue is that return 42 if condition, or module X; return 42 won't be caught

MSG = 'Top level return with argument detected.'

def on_return(return_node)
add_offense(return_node) if ancestors_valid?(return_node) && !return_node.arguments.empty?
Copy link
Contributor

Choose a reason for hiding this comment

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

return_node.arguments.empty? can also be written return_node.arguments?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Aah yes!

def ancestors_valid?(return_node)
return true if return_node.parent.nil?

if return_node.parent.instance_of?(AST::Node) && return_node.parent.parent.nil?
Copy link
Contributor

Choose a reason for hiding this comment

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

Won't return_node.parent.instance_of?(AST::Node) always be true?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

return_node.parent.is_a?(AST::Node) will always return true, but return_node.parent.instance_of?(AST::Node) would be true only if that object is strictly of that exact class, and not the subclass.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, right, my bad. Then I would say you need to tweak this test; you should not rely on the class of Node, only on their type. Here anyways I believe it is not the best anyways; you're excluding if for example.

return true
end

false
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 surprised there's no cop against if condition; return true; end; return false => return condition

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did write it in one line, but then it exceeded the 100 characters limit, so had to change it to this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@marcandre There actually is a cop that suggests this, one suggests for single line if and the other suggests to remove redundant false.

Copy link
Contributor

Choose a reason for hiding this comment

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

In this case here, you can remove the if, the return true\n end\n false completely...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The if condition checks for the types and returns things appropriately, if we remove that, the cop will report false positives.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated this! Thanks for the idea. 😄

@IamRaviTejaG
Copy link
Contributor Author

@marcandre Updated the PR. Now return 42 if a == b are being caught.

@marcandre
Copy link
Contributor

@marcandre Updated the PR. Now return 42 if a == b are being caught.

RIght, good, but it won't catch if a == b; warn 'hey'; return 42; end...

I was trying to hint at a more general logic. Unless I'm missing something, you can check all ancestors for either a block, def or defs; if you don't find any then you are still at the top level scope.

@IamRaviTejaG
Copy link
Contributor Author

@marcandre Updated the PR. Now return 42 if a == b are being caught.

RIght, good, but it won't catch if a == b; warn 'hey'; return 42; end...

I was trying to hint at a more general logic. Unless I'm missing something, you can check all ancestors for either a block, def or defs; if you don't find any then you are still at the top level scope.

Now this checks for the ancestors to not belong to the AST::BlockNode or AST::DefNode class.

@marcandre
Copy link
Contributor

Now this checks for the ancestors to not belong to the AST::BlockNode or AST::DefNode class.

Much better. Please do not rely on classes ever, as this is subject to change. You should rely on the result of Node#type or Node#block_type? / def_type? / defs_type? In this case, each_ancestor already accepts a list of types...

@IamRaviTejaG
Copy link
Contributor Author

Now this checks for the ancestors to not belong to the AST::BlockNode or AST::DefNode class.

Much better. Please do not rely on classes ever, as this is subject to change. You should rely on the result of Node#type or Node#block_type? / def_type? / defs_type? In this case, each_ancestor already accepts a list of types...

Aah, this suggestion and the resulting changes made the code so compact! Can't thank you enough for this.


def ancestors_valid?(return_node)
prohibited_ancestors = return_node.each_ancestor(:block, :def, :defs)
return false if prohibited_ancestors.any?
Copy link
Contributor

Choose a reason for hiding this comment

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

Personal preference, I'd simply write prohibited_ancestors.none?, no return, no if...

MSG = 'Top level return with argument detected.'

def on_return(return_node)
add_offense(return_node) if ancestors_valid?(return_node) && return_node.arguments?
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd switch the conditions' order, as arguments? is very quick while ancestors_valid? might be slower

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this also makes a lot of sense given Ruby's short circuit evaluation.

@marcandre
Copy link
Contributor

marcandre commented Jul 22, 2020

Aah, this suggestion and the resulting changes made the code so compact! Can't thank you enough for this.

Cool, I'm happy you're happy about the resulting code. I have two last remarks for styling/performance...

Also you'll have to rebase your branch

@IamRaviTejaG
Copy link
Contributor Author

Cool, I'm happy you're happy about the resulting code. I have two last remarks for styling/performance...

Also you'll have to rebase your branch

Made performance & styling related changes, and also rebased my branch. 😄

Comment on lines 3377 to 3379
This cop works by validating the ancestors of the return node. A
top-level return node's ancestors will never belong to `AST::BlockNode`
or `AST::DefNode` class.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd remove this comment; implementation detail is too technical and not useful to cop users.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense to me, removed this. Added this inside the class instead.


[1, 2, 3, 4, 5].each { |n| return n }

return # Should raise a `top level return with argument detected` offense
Copy link
Contributor

@marcandre marcandre Jul 22, 2020

Choose a reason for hiding this comment

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

Change comment or remove...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed.

Comment on lines 60 to 67
it 'Expects no offense from the method-level return statement' do
expect_no_offenses(<<~RUBY)
def method
return 'Hello World'
end
RUBY
end

Copy link
Contributor

Choose a reason for hiding this comment

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

Remove this spec, it is redundant with the next spec.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Comment on lines 125 to 130
it 'Expects an offense from the return with arguments' do
expect_offense(<<~RUBY)
if a == b; warn 'hey'; return 42; end
^^^^^^^^^ Top level return with argument detected.
RUBY
end
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems redundant with previous spec

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, removed this spec.

Comment on lines 132 to 137
it 'Expects no offense from the return without arguments' do
expect_no_offenses(<<~RUBY)
if a == b; warn 'hey'; return; end
RUBY
end

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems redundant with next spec

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, removed this.

@marcandre
Copy link
Contributor

marcandre commented Jul 22, 2020

Implementation is perfect now 🎉. I think some specs can be removed and some doc can be modified.

Comment on lines 10 to 13
# This cop works by validating the ancestors of the return node. A
# top-level return node's ancestors will never belong to `AST::BlockNode`
# or `AST::DefNode` class.
#
Copy link
Contributor

Choose a reason for hiding this comment

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

Please update and put inside the class so it doesn't show in the doc

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated this and added it inside class.

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, I'll let @bbatsov check naming & al.
(Will need to be squash-merged)

@IamRaviTejaG
Copy link
Contributor Author

Hey @bbatsov, can you please review this PR. 😄

@marcandre
Copy link
Contributor

It's ok, I probably should have simply merged it, looks really good. Looks like there are conflicts again, could you rebase and I'll merge it?

@IamRaviTejaG
Copy link
Contributor Author

@marcandre One test failed erroneously, and I cannot seem to re-run it, can you please re-run it? Once that passes this should be good to merge.

@marcandre marcandre merged commit b8eaa55 into rubocop:master Jul 24, 2020
@marcandre
Copy link
Contributor

Thanks @IamRaviTejaG 🎉

koic added a commit to koic/rubocop that referenced this pull request Aug 28, 2020
Follow rubocop#7868, rubocop#8377, and rubocop#8395

This PR uses `Cop::Base` API for `Lint/TopLevelReturnWithArgument`.
@IamRaviTejaG IamRaviTejaG deleted the lint-top-level-return-with-arguments branch January 8, 2021 02:10
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.

New cop: Detect return with argument on the top-level
2 participants