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

Linter for explicit/implicit returns #2271

Merged
merged 47 commits into from
Nov 24, 2023

Conversation

MEO265
Copy link
Contributor

@MEO265 MEO265 commented Nov 10, 2023

Closes #1100.

I have designed the implementation in such a way that as far as possible no false positive errors occur. This leads to a relatively large number of false negatives, but I would like to accept that.
If anyone can construct false positive cases, I would be grateful if you would write them in the comments.

Inline functions have been ignored so far, this is to a certain extent intentional, as I personally am a supporter of explicit returns, but find them unnecessary in inline functions. On the other hand, this case would have become much more complex.
If someone ever wants to implement it, I think an lint_inline parameter would be nice.

FYI:
There are 10 explicit returns (that the linter finds) in the linter package and although I prefer them, they should probably be removed for consistency.

@codecov-commenter
Copy link

codecov-commenter commented Nov 10, 2023

Codecov Report

All modified and coverable lines are covered by tests ✅

Comparison is base (97182b7) 99.40% compared to head (63e0e2b) 99.41%.

❗ Current head 63e0e2b differs from pull request most recent head 325205c. Consider uploading reports for the commit 325205c to get more accurate results

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #2271   +/-   ##
=======================================
  Coverage   99.40%   99.41%           
=======================================
  Files         123      124    +1     
  Lines        5590     5640   +50     
=======================================
+ Hits         5557     5607   +50     
  Misses         33       33           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@AshesITR
Copy link
Collaborator

The warnings stem from exclusions of linters that you removed by providing linters=.
To avoid them, add return_linter() to our .lintr instead.

@MichaelChirico
Copy link
Collaborator

MichaelChirico commented Nov 10, 2023

Hmm, we do already have an explicit_return_linter() [our default conflicts with the Tidyverse guide & we require all returns to be explicit]. Seems like we've got some duplicate effort here. OTOH yours already goes further by trying to support both styles (implicit + explicit)

We have tested ours pretty extensively (no false positives on 10,000+ files) -- here's all the tests we've written:

https://gist.github.com/MichaelChirico/9164672a4e762627c09ad6b521e54964

Please ensure these tests pass under use_implicit_returns = FALSE

Here's our XPath:

https://gist.github.com/MichaelChirico/084bc163c01ad0debba8b1b945b75009

The major issue I'm aware of with our implementation is it doesn't handle arbitrary nesting in terminal if/else branches -- we only look at top-level if/else (i.e. depth=1).

See #884 for linters we've already written that are just a matter of porting over.

@MichaelChirico
Copy link
Collaborator

Inline functions have been ignored so far

I agree we shouldn't require return() on inline functions -- that's how we implemented it as well.

@MichaelChirico
Copy link
Collaborator

There are 10 explicit returns (that the linter finds) in the linter package and although I prefer them, they should probably be removed for consistency.

Most likely, these are from copy-pasting our internal code where the styles are opposite. Yes, we should remove them here for consistency (and because we're meant to enforce the tidyverse guide)

@MEO265
Copy link
Contributor Author

MEO265 commented Nov 10, 2023

We have tested ours pretty extensively (no false positives on 10,000+ files) -- here's all the tests we've written:

https://gist.github.com/MichaelChirico/9164672a4e762627c09ad6b521e54964

Please ensure these tests pass under use_implicit_returns = FALSE

Just by looking at it I can tell that my linter would produce lots of false positives. Many that one should definitely pay attention to, e.g. invokeRestart, tryInvokeRestart, UseMethod, etc. and others that I would say are actually marked correctly, namely message, stopifnot and maybe even warning. And when it comes to controls like for and while, my code is also a little stricter.

Maybe in addition to a fixed number of basic functions that are taken into account in the code, there should be the option to accept more functions (from other packages).

Here's our XPath:

https://gist.github.com/MichaelChirico/084bc163c01ad0debba8b1b945b75009

The major issue I'm aware of with our implementation is it doesn't handle arbitrary nesting in terminal if/else branches -- we only look at top-level if/else (i.e. depth=1).

Thanks for access to the code. I see that you have already paid a lot more attention. What is the best way to proceed? I can try to bring the two together and make sure everything passes, although that might take a while (which doesn't bother me), but if you want to take over that would be ok too.

See #884 for linters we've already written that are just a matter of porting over.

Is there a way to see the code of all linters still to be transferred or to get access to certain ones? If so, I would like to try to support whenever my time allows.

@MEO265
Copy link
Contributor Author

MEO265 commented Nov 10, 2023

The warnings stem from exclusions of linters that you removed by providing linters=. To avoid them, add return_linter() to our .lintr instead.

Thanks for the answer, what surprised me is that not a single warning came when I called up my linter without argument

@MichaelChirico
Copy link
Collaborator

Is there a way to see the code of all linters still to be transferred or to get access to certain ones?

Not easily... I have to strip some internal-only pieces from the code before uploading them here. If you'd like to see specific linters I am happy to do so one a one-by-one basis.

@MichaelChirico
Copy link
Collaborator

when it comes to controls like for and while, my code is also a little stricter.

I think I'd be open to simplifying things a bit -- always lint on a terminal for/while/repeat, requiring the function to terminate with return(invisible()) instead (or perhaps a stop() like you mentioned in case the function is meant to return early). I think I didn't realize at the time we initially implemented this that those control flows always return NULL implicitly:

foo <- function() {
  for (ii in 1:10) {
    ii
  }
}
x <- foo()
dput(x)
# NULL

So under explicit return style, IMO it's always preferable to make that behavior more explicit:

foo <- function() {
  for (ii in 1:10) {
    ii
  }
  return(invisible())
}

@MichaelChirico
Copy link
Collaborator

Maybe in addition to a fixed number of basic functions that are taken into account in the code, there should be the option to accept more functions (from other packages).

Yes, that's what I would do... one issue is the list is already a bit long, the signature will get pretty messy if the default list is huge. So I would propose the parameter being extra functions to skip.

Something like

# User has no control here; all of these come from base
default_allowed_functions <- c(
  # Normal calls
  "return", "stop", "warning", "message", "stopifnot", "q", "quit",
  "invokeRestart", "tryInvokeRestart",

  # Functions related to S3 methods
  "UseMethod", "NextMethod",

  # Functions related to S4 methods
  "standardGeneric", "callNextMethod",

  # Functions related to C interfaces
  ".C", ".Call", ".External", ".Fortran"
)

# User supplies these; internally we would use this
extra_allowed_functions <- c(
  # Normal calls from non-default libraries
  "LOG", "abort",

  # tests in the RUnit framework are functions ending with a call to one
  #   of the below. would rather users just use a different framework
  #   (e.g. testthat or tinytest), but already 250+ BUILD files depend
  #   on RUnit, so just cater to that. confirmed the efficiency impact
  #   of including these is minimal.
  # RUnit tests look like 'TestInCamelCase <- function()'
  #   NB: check for starts-with(text(), 'Test') below is not sufficient, e.g.
  #   in cases of a "driver" test function taking arguments and the main unit
  #   test iterating over those.
  "checkEquals", "checkEqualsNumeric", "checkException", "checkIdentical",
  "checkStop", "checkTrue", "checkWarnings",
)

@AshesITR
Copy link
Collaborator

I'm assuming "LOG" is internal to google?
In that case, I'd remove it from the official defaults.

Agree we should silently add all base R exceptions and maybe rlang (abort)?
Or keep "abort" in the signature to hint at the usage, dropping RUnit support out of the box?

@MichaelChirico
Copy link
Collaborator

I'm assuming "LOG" is internal to google? In that case, I'd remove it from the official defaults.

Agree we should silently add all base R exceptions and maybe rlang (abort)? Or keep "abort" in the signature to hint at the usage, dropping RUnit support out of the box?

SGTM, though maybe should add any other signalling calls from rlang as well? not super familiar with what's offered, I think warn()?

@AshesITR
Copy link
Collaborator

Actually, why allow warning() as the last call? It quietly returns the message as a string.

@MichaelChirico
Copy link
Collaborator

MichaelChirico commented Nov 12, 2023

Actually, why allow warning() as the last call? It quietly returns the message as a string.

Hmm, not sure we noticed that at the time. I do find it very strange that behavior differs from message() (which returns NULL).

Anyway, for our case I would still skip return(). I think requiring it for warning() would make tryCatch()/withCallingHandlers() usage overly verbose, e.g.

tryCatch(
  foo(),
  error = \(e) {
    cat(Sys.time(), file="log")
    cat(e$message, file="log", append=TRUE)
    warning(e)
  }
)

Treating all signalling functions the same makes more sense to me.

That said, for {lintr}'s defaults, we can consider something different.

R/return_linter.R Outdated Show resolved Hide resolved
@AshesITR
Copy link
Collaborator

Maybe instead of allowing warning() etc. we should carve out functions passed to withCallingHandlers() and friends?

@MEO265
Copy link
Contributor Author

MEO265 commented Nov 20, 2023

@MichaelChirico I think it would be really useful, even outside of this PR, if we included some of the functions you use for glue, like .XpTextInTable, in the package here too. Would make some linter easier to read.
Or is it already in the package under a different name and I just overlooked it?

@MichaelChirico
Copy link
Collaborator

@MichaelChirico I think it would be really useful, even outside of this PR, if we included some of the functions you use for glue, like .XpTextInTable, in the package here too. Would make some linter easier to read. Or is it already in the package under a different name and I just overlooked it?

in {lintr} that's xp_text_in_table, see xp_utils.R for similar helpers

@MichaelChirico
Copy link
Collaborator

  • Dropped RUnit support for now, to be handled in follow-up
  • Renamed all three parameters

@MEO265
Copy link
Contributor Author

MEO265 commented Nov 23, 2023

@MichaelChirico as I see it, you've already done everything or is there still something to do for me?
Thanks anyway.

@MichaelChirico
Copy link
Collaborator

@MichaelChirico as I see it, you've already done everything or is there still something to do for me? Thanks anyway.

yes, I tried to clear the pending tasks. good for another round of review in case there's anything pending needed for initial merge.

@MEO265
Copy link
Contributor Author

MEO265 commented Nov 23, 2023

@MichaelChirico as I see it, you've already done everything or is there still something to do for me? Thanks anyway.

yes, I tried to clear the pending tasks. good for another round of review in case there's anything pending needed for initial merge.

Okay, I don't have any more comments from my side

@AshesITR
Copy link
Collaborator

Nice. I'll try to review later this day.

} else {
# See `?.onAttach`; these functions are all exclusively used for their
# side-effects, so implicit return is generally acceptable
default_except <- c(".onLoad", ".onUnload", ".onAttach", ".onDetach", ".Last.lib")
Copy link
Collaborator

Choose a reason for hiding this comment

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

just noticed we could probably re-use this:

special_funs <- c(
".onLoad",
".onAttach",
".onUnload",
".onDetach",
".Last.lib",
".First",
".Last"
)

* remove incorrect comment in default_linter_testcode.R
* fix NEWS entry (argument is called `return_style`)
* reuse `special_funs` constant
* convert all tests from lines <- c(...) to trim_some()
@AshesITR
Copy link
Collaborator

@MichaelChirico PTAL, I've fixed what I found directly on the branch.
I think we can merge now.

AshesITR
AshesITR previously approved these changes Nov 23, 2023
@@ -20,6 +25,7 @@ f = function (x,y = 1){}
# object_name
# object_usage
# open_curly
# return
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is inaccurate. return_linter() doesn't lint the following function.

@@ -25,7 +25,6 @@ g <- function(x) {
# object_name
# object_usage
# open_curly
# return
Copy link
Collaborator

Choose a reason for hiding this comment

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

oh! in some commit I added an explicit return here. Must not have survived. I see someone (possibly me?) added a return_linter() test case to this file in another commit.

" FALSE",
"}"
expect_lint(
trim_some("
Copy link
Collaborator

Choose a reason for hiding this comment

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

thanks for cleaning these up to use trim_some()!

@MichaelChirico
Copy link
Collaborator

Great work everyone and thanks for the patience! 70 comments, that may be a record :)

@MichaelChirico MichaelChirico merged commit 30c7d70 into r-lib:main Nov 24, 2023
20 checks passed
@MEO265 MEO265 deleted the feature/return_linter branch November 25, 2023 09:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

New return_linter to enforce consistent return style
4 participants