Skip to contents
library(rpkgkit)
#> ✔ rpkgkit v0.1.24 loaded [2 ms]

Qualify package function calls

make_func_call_explicit() adds namespace qualifiers to calls from selected packages. package_func_call_explicit() applies the same operation throughout a package.

file <- tempfile(fileext = ".R")
writeLines(
  c(
    "starwars |>",
    "  mutate(name, bmi = mass / ((height / 100)^2)) |>",
    "  select(name:mass, bmi)"
  ),
  file
)

make_func_call_explicit(file, use_packages = "dplyr")
#> ℹ Retrieving function calls from dplyr
#> ✔ Successfully made function call explicit in /tmp/RtmprZoMVF/file1f9ab097532.R
readLines(file) |> cli::cli_code()
#> dplyr::starwars |>
#>   dplyr::mutate(name, bmi = mass / ((height / 100)^2)) |>
#>   dplyr::select(name:mass, bmi)

Find unmatched glue braces

detect_lost_glue_brace() checks one file; package_lost_glue_brace() checks an entire package. Both support glue and cli expressions, and print the offending source line with cli syntax highlighting.

file <- tempfile(fileext = ".R")
writeLines(
  c(
    'name <- "world"',
    'msg <- glue::glue("Hello, {name!")',
    'cli::cli_alert_warning("{.field warning}}")'
  ),
  file
)

detect_lost_glue_brace(file)
#> msg <- glue::glue("Hello, {name!")
#>                           ^^^^^^
#> cli::cli_alert_warning("{.field warning}}")
#>                         ^^^^^^^^^^^^^^^^^
#> ✖ Found 2 lines with mismatched braces: 2 and 3

Name positional arguments

make_func_arg_explicit() makes arguments in one file explicit, and package_func_arg_explicit() applies this transformation across a package.

file <- tempfile(fileext = ".R")
writeLines("vapply(1:9, function(x) x * 2, numeric(1))", file)
make_func_arg_explicit(file)
#> ✔ Made function arguments explicit in /tmp/RtmprZoMVF/file1f9a61d94446.R
readLines(file) |> cli::cli_code()
#> vapply(X = 1:9, FUN = function(x) x * 2, FUN.VALUE = numeric(length = 1))

Rename functions

Use rename_func() to apply a naming style to function definitions in a file.

file <- tempfile(fileext = ".R")
writeLines("this_is_a_function <- function() message('Hello, world')", file)
rename_func(file, style = "camelCase")
#> ✔ Renamed 1 function to "camelCase" style in /tmp/RtmprZoMVF/file1f9a28000128.R
readLines(file) |> cli::cli_code()
#> thisIsAFunction <- function() message('Hello, world')

Detect print() and cat()

detect_print_and_cat() reports print() and cat() calls in one file. package_print_and_cat() checks an entire package. Set fix = TRUE to replace those calls in the selected file with message(). As for the glue brace detection, the reported source line is syntax highlighted.

file <- tempfile(fileext = ".R")
writeLines("print('Hello, world')", file)
detect_print_and_cat(file)
#> print('Hello, world') [message]
#> ^^^^^^
#> ✖ Found 1 unsupported call on line 1.
detect_print_and_cat(file, fix = TRUE)
#> ✔ Fixed 1 line in file1f9a14d478c.R.
#> print('Hello, world') [message]
#> ^^^^^^
#> ✖ Found 1 unsupported call on line 1.
readLines(file) |> cli::cli_code()
#> message('Hello, world')

Detect unmatched nocov markers

detect_unmatched_nocov_marker() checks the # nocov start and # nocov end markers of one file, and package_unmatched_nocov_marker() checks a whole package. A file may hold at most one marker of each kind, so every further start or end is reported with its line and column, together with a start or an end that has no counterpart and an end placed before the start. Both counts must therefore match: an unclosed start is reported as well, because covr::package_coverage() aborts on such a range with a “range start but only range ends” error.

Detection is purely textual, so a marker inside a string literal or a roxygen comment is reported as well (as covr does); such a marker has no effect on coverage, so it is usually a leftover to remove. Nothing is rewritten — the offending comment is pointed at and the fix is left to you.

file <- tempfile(fileext = ".R")
writeLines(
  c(
    "# nocov start",
    "f <- function(x) x + 1",
    "# nocov start",
    "# nocov end"
  ),
  file
)

detect_unmatched_nocov_marker(file)
#> ✖ `nocov start` at line 3, column 1: only one `nocov start` is allowed per file, the first is `nocov start` at line 1, column 1.
#> # nocov start
#> ^^^^^^^^^^^^^
#> ✖ Found 1 mismatched `nocov` marker on line 3.

A # nocov start that is never closed is reported as well:

writeLines(c("# nocov start", "g <- function(y) y"), file)
detect_unmatched_nocov_marker(file)
#> ✖ `nocov start` at line 1, column 1 has no matching `nocov end`.
#> # nocov start
#> ^^^^^^^^^^^^^
#> ✖ Found 1 mismatched `nocov` marker on line 1.
pkg <- file.path(tempdir(), "demo")
dir.create(file.path(pkg, "R"), recursive = TRUE, showWarnings = FALSE)
writeLines(
  c("Package: demo", "Version: 0.0.1"),
  file.path(pkg, "DESCRIPTION")
)
writeLines(c("g <- function(y) y", "# nocov end"), file.path(pkg, "R", "g.R"))

package_unmatched_nocov_marker(pkg)
#> R/g.R: 2:
#> ✖ `nocov end` at line 2, column 1 has no matching `nocov start`.
#> # nocov end
#> ^^^^^^^^^^^
#> ✖ Found 1 mismatched `nocov` marker in 1 of 1 file.

Detect default paths in exported arguments

CRAN does not allow a path-like argument of an exported function to default to a concrete location such as "." or "~/data.csv". detect_fn_arg_path() checks one file and package_fn_arg_path() checks every .R file of a package, using the exports declared in NAMESPACE.

file <- tempfile(fileext = ".R")
writeLines(
  c(
    'read_csv <- function(path = "~/data.csv", n = 1L) NULL',
    'read_all <- function(file = "data.txt") NULL',
    'read_tsv <- function(path = NULL, n = 1L) NULL'
  ),
  file
)

detect_fn_arg_path(file)
#> /tmp/RtmprZoMVF/file1f9a3718be4e.R:1:22 read_csv(): `path` defaults to
#> "~/data.csv"
#> read_csv <- function(path = "~/data.csv", n = 1L) NULL
#>                      ^^^^^^^^^^^^^^^^^^^
#> /tmp/RtmprZoMVF/file1f9a3718be4e.R:2:22 read_all(): `file` defaults to
#> "data.txt"
#> read_all <- function(file = "data.txt") NULL
#>                      ^^^^^^^^^^^^^^^^^
#> ✖ Found 2 arguments with a default path in /tmp/RtmprZoMVF/file1f9a3718be4e.R

An argument is reported when its name matches arg_pattern and its default is not listed in allow, so read_tsv() above is clean: its default is NULL. An argument without a default is always fine. Narrow the pattern when a loose match is not wanted:

detect_fn_arg_path(file, arg_pattern = "^path$")
#> /tmp/RtmprZoMVF/file1f9a3718be4e.R:1:22 read_csv(): `path` defaults to
#> "~/data.csv"
#> read_csv <- function(path = "~/data.csv", n = 1L) NULL
#>                      ^^^^^^^^^^^^^^^^^^^
#> ✖ Found 1 argument with a default path in /tmp/RtmprZoMVF/file1f9a3718be4e.R

detect_fn_arg_path() uses the active document when path is NULL, and checks every top-level function of that file unless exports or export_patterns say which ones count as exported. Use the package-wide function to see all the occurrences at once:

Convert function syntax

Convert between function() and the short \() syntax.

file <- tempfile(fileext = ".R")
writeLines("f <- function(x) x^2", file)
convert_func_syntax(file, "to_lambda")
#> ✔ Converted function definitions in /tmp/RtmprZoMVF/file1f9a26eb1b49.R to "to_lambda"
readLines(file) |> cli::cli_code()
#> f <- \(x) x^2
convert_func_syntax(file, "to_explicit")
#> ✔ Converted function definitions in /tmp/RtmprZoMVF/file1f9a26eb1b49.R to "to_explicit"
readLines(file) |> cli::cli_code()
#> f <- function(x) x^2

Modernize knitr chunk headers

convert_knitr_chunk_header() converts legacy R Markdown chunk options to the current comment-based syntax.

file <- tempfile(fileext = ".Rmd")
writeLines(c("```{r, echo=TRUE, fig.width=10}", "x <- 1", "```"), file)
convert_knitr_chunk_header(file)
#> ℹ Converting knitr chunk headers in /tmp/RtmprZoMVF/file1f9a30f7181c.Rmd
readLines(file) |> cli::cli_code()
#> ```{r}
#> #| echo = TRUE,
#> #| fig.width = 10
#> x <- 1
#> ```

Switch pipe operators

convert_pipe() rewrites pipes in one file between %>% and |>. Strings, comments, and the other magrittr operators (%<>%, %$%) are left unchanged.

file <- tempfile(fileext = ".R")
writeLines("mtcars %>% dplyr::filter(cyl > 4)", file)
convert_pipe(file, direction = "to_base")
#> ✔ Converted pipes in /tmp/RtmprZoMVF/file1f9a3fa9a86.R ("to_base")
readLines(file) |> cli::cli_code()
#> mtcars |> dplyr::filter(cyl > 4)

Use package_convert_pipe() to process .R and .r files below a package’s R/ directory.

package_convert_pipe(".", direction = "to_base")

Add integer suffixes

convert_int_literals() adds L to bare integer literals while leaving strings, comments, floating-point values, and existing suffixes unchanged.

file <- tempfile(fileext = ".R")
writeLines("x <- seq_len(10) # length 10", file)
convert_int_literals(file)
#> ✔ Added explicit integer suffixes in /tmp/RtmprZoMVF/file1f9a7133c461.R
readLines(file) |> cli::cli_code()
#> x <- seq_len(10L) # length 10

Use package_convert_int_literals() to process .R and .r files below a package’s R/ and tests/ directories.

Convert non-ASCII source code

convert_nonascii_code() converts non-ASCII source to Unicode escape sequences when needed for CRAN-compatible source files. It accepts either a file path or an R expression.

file <- tempfile()
writeLines("foo <- \\() message('滚滚长江东逝水')", file)
convert_nonascii_code(file, overwrite = TRUE)
#> ℹ Converted content written to /tmp/RtmprZoMVF/file1f9a4e3e82b4
readLines(file) |> cli::cli_code()
#> foo <- \() message('\u6eda\u6eda\u957f\u6c5f\u4e1c\u901d\u6c34')
convert_nonascii_code({
  cli::cli_alert_info("明月几时有")
  cli::cli_alert_warning("把酒问青天")
})
#> ℹ Converted code (copy from console):
#> {
#>   cli::cli_alert_info("\u660e\u6708\u51e0\u65f6\u6709")
#>   cli::cli_alert_warning("\u628a\u9152\u95ee\u9752\u5929")
#> }

Lay out long vectors and lists as tables

Air 0.8.0 and newer can align the elements of a c() or list() call column by column, giving the layout of tibble::tribble(). The feature is switched on by the experimental # fmt: table directive placed right above the call, but air never re-flows the elements on its own: whatever sits on one source line stays there.

use_air_table() does the two things air does not do by itself. It runs air format, inserts the directive above every unnamed c()/list() call that holds more than min_length elements, and re-flows those elements so that each row holds ncol of them; a second air format run then pads the cells and aligns the columns.

file <- tempfile(fileext = ".R")
writeLines(
  'x <- c("alpha", "beta", "gamma", "delta", "epsilon", "zeta")',
  file
)
use_air_table(file, ncol = 3L, min_length = 5L)
readLines(file) |> cli::cli_code()

Keep in mind that:

  • air must be installed and on the PATH; check_air_installed() reports the installation command for each operating system.
  • Only calls whose elements are all unnamed are rewritten: air expands named elements one per line whatever the directive says, so a named call is skipped instead of being decorated with a directive that has no effect.
  • A call that already carries the directive is left untouched, so the function is idempotent.
  • Only the outermost call of a statement is converted, because air ignores the directive on nested calls.