fs icon indicating copy to clipboard operation
fs copied to clipboard

Throw error on base R functions

Open hadley opened this issue 6 years ago • 15 comments

base_error <- function(env = parent.frame()) {
  err <- function(...) stop("Use fs instead!", call. = FALSE)
  
  env$file.rename <- err
  env$file.create <- err
  env$file.exists <- err
  ...

}

If you want to make sure that you've eliminated all base R file system operations.

hadley avatar Mar 12 '18 18:03 hadley

An alternative would be a scanner tool that is not active while your package is running in production. That would allow false positives, and the odd cases when you actually want some base calls. People could run it fro their package tests.

gaborcsardi avatar Apr 05 '18 11:04 gaborcsardi

Yeah, we could make a linter for it pretty easily at least for the simple cases; e.g. regular calls and possibly special case do.call() as well?

I guess we already have this right? https://github.com/MangoTheCat/goodpractice/blob/master/R/my_linters.R#L114

jimhester avatar Apr 05 '18 15:04 jimhester

e.g. when run from the devtools project directory.

lintr::lint_package(
  linters = list(base_fs_linter = function(source_file) {
    goodpractice:::dangerous_functions_linter(source_file,
      funcs = c("file.rename", "file.create", "file.exists"),
      type = "warning",
      msg = "Avoid base filesystem functions",
      linter = "base_fs_linter") 
  }))
R/check-devtools.r:86:8: warning: Avoid base filesystem functions
  if (!file.exists(news_path))
       ^~~~~~~~~~~
R/check-devtools.r:90:8: warning: Avoid base filesystem functions
  if (!file.exists(ignore_path)) {
       ^~~~~~~~~~~
R/check-devtools.r:106:6: warning: Avoid base filesystem functions
    !file.exists(news_rd_path),
     ^~~~~~~~~~~
R/create.r:10:8: warning: Avoid base filesystem functions
  if (!file.exists(parent_dir)) {
       ^~~~~~~~~~~
R/create.r:16:8: warning: Avoid base filesystem functions
  if (!file.exists(path)) {
       ^~~~~~~~~~~
R/create.r:69:7: warning: Avoid base filesystem functions
  if (file.exists(desc_path)) return(FALSE)
      ^~~~~~~~~~~
...

jimhester avatar Apr 05 '18 15:04 jimhester

We could probably define this linter in fs, to make it easy for people to use.

jimhester avatar Apr 05 '18 15:04 jimhester

Yeah, I think the generic "dangerous function linter" could be in lintr, probably. It does not find do.call(), etc. currently, of course.

On a second thought, I like the original base_error() idea as well, but maybe I would only turn these errors on for the test cases?

gaborcsardi avatar Apr 05 '18 15:04 gaborcsardi

Btw. this would be also great for rlang, and probably other packages as well.

gaborcsardi avatar Apr 05 '18 15:04 gaborcsardi

@jimhester We're seriously considering doing this in usethis.

I remember you helping me lint some package for base R file handling functions. Probably usethis?

But I can't turn up a branch for that. Do you remember / have a record of it? Because it contained the necessary "dataset" of functions.

jennybc avatar Dec 04 '18 20:12 jennybc

It is at https://github.com/r-lib/fs/compare/base_fs_funs

jimhester avatar Dec 04 '18 20:12 jimhester

The list of base functions linked above is missing basename() and dirname().

jennybc avatar Jan 31 '19 01:01 jennybc

This served me pretty well today:

base_functions <- c(
  "Sys.chmod",
  "Sys.readlink",
  "Sys.setFileTime",
  "Sys.umask",
  "dir",
  "dir.create",
  "dir.exists",
  "file.access",
  "file.append",
  "file.copy",
  "file.create",
  "file.exists",
  "file.info",
  "file.link",
  "file.mode",
  "file.mtime",
  "file.path",
  "file.remove",
  "file.rename",
  "file.size",
  "file.symlink",
  "list.files",
  "list.dirs",
  "normalizePath",
  "path.expand",
  "tempdir",
  "tempfile",
  "unlink",
  "basename",
  "dirname")

function_linter <- function(source_file, funcs, type,
                            msg, linter) {
  
  bad <- which(
    source_file$parsed_content$token == "SYMBOL_FUNCTION_CALL" &
      source_file$parsed_content$text %in% funcs
  )
  
  lapply(
    bad,
    function(line) {
      parsed <- source_file$parsed_content[line, ]
      msg <- gsub("%s", source_file$parsed_content$text[line], msg)
      lintr::Lint(
        filename = source_file$filename,
        line_number = parsed$line1,
        column_number = parsed$col1,
        type = type,
        message = msg,
        line = source_file$lines[as.character(parsed$line1)],
        ranges = list(c(parsed$col1, parsed$col2)),
        linter = linter
      )
    }
  )
}

base_fs_funs <- function(path = ".") {
  lintr::lint_package(path = path,
                      linters = list(base_fs_linter = function(source_file) {
                        function_linter(source_file,
                                        funcs = base_functions,
                                        type = "warning",
                                        msg = "Avoid base filesystem functions (%s)",
                                        linter = "base_fs_linter") 
                      }))
}

jennybc avatar Jan 31 '19 01:01 jennybc

I don't think a linter is quite strong enough; for packages I'd prefer to get an error message rather than having to remember to run a linter. (i.e. we should do both)

hadley avatar Feb 01 '19 22:02 hadley

Given that we now have github actions, I'm now in favour of the lintr approach.

hadley avatar Mar 20 '20 19:03 hadley

usethis is now using the lintr approach with GHA:

https://github.com/r-lib/usethis/blob/master/.github/workflows/lint-undesirable-functions.yaml

~Currently using a branch in my fork of lintr, but presumably I will eventually be able to use a (dev) version of lintr itself.~

jennybc avatar Nov 04 '20 01:11 jennybc

We seem to have agreed that linting is the way to go and usethis provides a good list of which base R functions to look for. However, the way @Hadley wired linting into devtools is better than what I've currently got in usethis. The devtools linting is about using cli, not fs, but the approach transfers regardless. So I just dropped by here to note that the current best solution to this issue would be to plug the undesirable function info from usethis into the linting approach from devtools.

jennybc avatar Jun 09 '22 05:06 jennybc

Maybe the list of undesirably functions could be an exported function in fs? That way we have the data in a central location. (And we could put in a little more work to recommend the specific replacements)

hadley avatar Jun 09 '22 12:06 hadley