Custom Checks

By default, the goodpractice() function and its alias gp() run all the checks available in the package (use all_checks() to show all checks implemented). In addition, users can provide their own custom checks.

What’s happening inside of gp()?

The gp() function essentially carries out two types of steps:

  • First, it runs preparation steps for the checks, such as calculating test coverage or cyclomatic complexity.
  • Then it carries out the checks, e.g., if cyclomatic complexity exceeds a threshold.

The results of the preparation steps and the checks are added to the return object, also referred to as the state. The print method accesses the check results and prints the advice for the failed checks - or praise if none fail.

Writing custom checks

Checks without corresponding preparation steps

Custom checks can be created with the make_check() function. As inputs it takes a short description of the check, the check itself, and the gp advice to be given in case the check fails. To illustrate this, let’s use a simplified version of the check on T and F instead of TRUE and FALSE.

The check argument is a function which takes the state as the input and carries out the check, returning TRUE if the check was successful. The state includes the path to the source code of the package and the checkTnF() function of the tools package can be used to check if T or F was used.

library(goodpractice)

# make a simple version of the T/F check
check_simple_tf <- make_check(
  
  description = "TRUE and FALSE is used, not T and F",
  gp = "avoid 'T' and 'F', use 'TRUE' and 'FALSE' instead.",
  check = function(state) {
      length(tools::checkTnF(dir = state$path)) == 0
  }
)

Additional checks can be used in gp() via the extra_checks argument. This should be a named list of check objects as returned by the make_check() function. All checks to be carried out, regardless of whether they are provided by the goodpractice package or custom checks, must be named in the checks argument to gp().

The check on T/F implemented in the package gives more helpful advice than this simplified version and returns which files contain T and F so let’s do a quick comparison and run both versions:

# get path to example package
pkg_path <- system.file("bad1", package = "goodpractice")
gp(pkg_path, checks = c("simple_tf", "truefalse_not_tf"),
   extra_checks = list(simple_tf = check_simple_tf))
#> ── GP badpackage ───────────────────────────────────────────────────────────────
#> 
#> It is good practice to
#> 
#>   ✖ avoid 'T' and 'F', use 'TRUE' and 'FALSE' instead.
#>   ✖ avoid 'T' and 'F', as they are just variables which are set to the logicals
#>     'TRUE' and 'FALSE' by default, but are not reserved words and hence can be
#>     overwritten by the user.  Hence, one should always use 'TRUE' and 'FALSE'
#>     for the logicals.
#> 
#>     'R/tf.R'
#>     'R/tf.R'
#>     'R/tf.R'
#>     'R/tf.R'
#>     'R/tf.R'
#>     ... and 4 more lines
#> 
#> ────────────────────────────────────────────────────────────────────────────────

Including a preparation step

Including a preparation step is optional but can be helpful if several checks require the same preparations upfront. In the following example we check for two different fields to be present in the DESCRIPTION file, the URL field and the BugReports field. Both checks can be carried out easily building on a preparation step with the desc package for handling DESCRIPTION files.

The checks are linked to the preparation via the prep name: it appears as the name argument to make_prep(), as the preps argument to make_check() and finally as the name in the list for the extra_preps argument to gp().

# prep: process DESCRIPTION file
desc_fun <- function(path, quiet) {
  desc::description$new(path)
}

prep_desc <- make_prep(name = "desc", func = desc_fun)

# check for an URL field
check_url <- make_check(
  description = "URL field in DESCRIPTION",
  preps = "desc",
  gp = "have a URL field in DESCRIPTION",
  check = function(state) state$desc$has_fields("URL")
)

# check for a BugReport field
check_bugreports <- make_check(
  description = "BugReports in DESCRIPTION",
  preps = "desc",
  gp = "add a BugReports field to DESCRIPTION",
  check = function(state) state$desc$has_fields('BugReports')
)

# run the two checks with their corresponding prep step
gp(pkg_path, checks = c("url", "bugreports"),
   extra_preps = list("desc" = prep_desc),
   extra_checks = list("url" = check_url, "bugreports" = check_bugreports))
#> ℹ Preparing: desc
#> ── GP badpackage ───────────────────────────────────────────────────────────────
#> 
#> It is good practice to
#> 
#>   ✖ have a URL field in DESCRIPTION
#>   ✖ add a BugReports field to DESCRIPTION
#> ────────────────────────────────────────────────────────────────────────────────

More examples for using custom checks can be found in the rOpenSci unconf 2017 project checkers for automated checking of best practices for research compendia.