---
title: "goodpractice"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{goodpractice}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
```{r setup, include = FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>"
)
```
## Why goodpractice?
A robust R package needs more than passing `R CMD check`.
Tests give you confidence that things work.
A consistent coding style makes your code readable to others.
Keeping functions short and focused reduces the risk of bugs.
Good metadata — a BugReports URL, properly declared dependencies, documented return values — helps users and reviewers understand what they're working with.
goodpractice checks all of this in one pass.
It bundles `R CMD check` with code coverage ([covr](https://cran.r-project.org/package=covr)), source linting ([lintr](https://cran.r-project.org/package=lintr)), cyclomatic complexity ([cyclocomp](https://cran.r-project.org/package=cyclocomp)), and its own checks for documentation, package structure, and common pitfalls.
Each message tells you _what_ to fix, _why_ it matters, and _where_ in your code to look.
You can also add [custom checks](custom_checks.html) for your team's own conventions.
## Quick start
The main function is `gp()` (short for `goodpractice()`).
Point it at a package directory and it runs the default set of checks:
```{r library}
library(goodpractice)
# goodpractice ships with an example package that has some issues
pkg_path <- system.file("bad1", package = "goodpractice")
```
```{r initial-gp-call-dummy, echo = TRUE, eval = FALSE}
g <- gp(pkg_path)
```
```{r initial-gp-call, echo = FALSE, eval = TRUE}
g <- suppressWarnings(gp(pkg_path))
```
Printing the result shows only the checks that failed:
```{r gp-print}
g
```
Each line starting with a cross is one failed check.
The text after the cross explains what to fix and why.
The indented file paths below it show exactly where in your code the problem was found — in terminals and IDEs that support it, these paths are clickable.
If every check passes, you get a short praise message instead.
## What gp() actually does
When you call `gp()`, two things happen in sequence:
1. **Gather data** — goodpractice runs a set of preparation steps, one per check group.
One step runs `R CMD check`.
Another computes code coverage by installing your package and exercising the tests.
Another lints your source files.
Each step runs exactly once and stores its results for the checks to use.
2. **Run checks** — each check reads from the stored results and returns pass, fail, or skip.
A single preparation step can feed many checks — the `rcmdcheck` step alone powers over 200 individual checks, all drawn from a single `R CMD check` run.
This two-step design is why goodpractice can run 250+ checks without repeating expensive work.
It also means you can skip an entire category of checks by excluding its group — more on that below.
## Choosing what to run
By default, `gp()` runs everything in `default_checks()` — about 250 checks covering documentation, code style, test coverage, namespace hygiene, and CRAN compliance:
```{r default-checks-length}
length(default_checks())
```
If you only need a specific check, pass its name to the `checks` argument.
You can find check names with `all_checks()` and filter with `grep()`:
```{r find-url-checks}
# find checks related to URLs
grep("url", all_checks(), value = TRUE)
```
Then run just the ones you care about:
```{r gp-desc-url-only}
g_url <- gp(pkg_path, checks = "description_url")
g_url
```
To go the other direction and run _more_ than the defaults, combine check sets.
For example, to add the optional tidyverse style checks on top of the defaults:
```{r gp-tidyverse-extra-dummy, eval = FALSE}
g <- gp(".", checks = c(default_checks(), tidyverse_checks()))
```
Three helper functions give you the available check names:
- `default_checks()` — the standard set, run by default
- `tidyverse_checks()` — opt-in style checks following [tidyverse conventions](https://style.tidyverse.org/)
- `all_checks()` — both combined
```{r check-lengths}
length(tidyverse_checks())
length(all_checks())
```
## Check groups
Every check belongs to at least one _check group_.
Groups let you work with categories of checks instead of individual names:
```{r all-check-groups}
all_check_groups()
```
To see which checks belong to a group, use `checks_by_group()`:
```{r desc-check-group}
checks_by_group("description")
```
You can select multiple groups at once:
```{r desc-namespace-only-dummy, eval = FALSE}
# run only DESCRIPTION and namespace checks
gp(".", checks = checks_by_group("description", "namespace"))
```
There is also a `describe_check_groups()` function to return full descriptions
of all checks (modified here from default screen output to look nice):
```{r describe-check-groups-dummy, eval = FALSE}
describe_check_groups()
```
```{r kable, results="asis", echo = FALSE}
descs <- describe_check_groups()
df <- data.frame(Group = names(descs), Description = unlist(descs), row.names = NULL)
tbl <- knitr::kable(df)
if (knitr::is_html_output()) {
tbl <- kableExtra::column_spec(tbl, 1L, extra_css = "white-space: nowrap")
tbl <- kableExtra::column_spec(tbl, 2L, width = "100%")
}
tbl
```
## Excluding check groups
The checks themselves run in milliseconds — what takes time is the data gathering.
Computing code coverage with `covr` requires installing your package and running all your tests.
Running `R CMD check` via `rcmdcheck` exercises documentation, examples, vignettes, and compilation.
On a large package, these two steps alone can take several minutes.
If you only care about code style or DESCRIPTION metadata right now, you can skip the slow groups entirely.
Set the `goodpractice.exclude_check_groups` option to a character vector of group names:
```{r gp-skip-dummy, eval = FALSE}
# skip coverage and R CMD check — just run the fast checks
options(goodpractice.exclude_check_groups = c("covr", "rcmdcheck"))
gp(".")
```
Every check that depends on a skipped group is automatically excluded too.
For CI/CD pipelines, you can set this via the `GP_EXCLUDE_CHECK_GROUPS` environment variable instead of modifying R code:
```bash
GP_EXCLUDE_CHECK_GROUPS=covr,rcmdcheck Rscript -e 'goodpractice::gp(".")'
```
When both the R option and the environment variable are set, the R option wins.
Exclusion only applies when you use the default check set — if you explicitly pass check names to the `checks` argument, exclusion settings are ignored and those checks always run.
```{r options-dummy, eval = FALSE}
options(goodpractice.exclude_check_groups = "covr")
# covr checks are excluded here (using defaults)
gp(".")
# the "covr" check runs here because we asked for it by name
gp(".", checks = "covr")
```
## Excluding files
If your package contains generated code or vendored files that should not be checked, you can exclude specific files.
Set the `goodpractice.exclude_path` option to a character vector of paths relative to the package root:
```{r options-files-dummy, eval = FALSE}
options(goodpractice.exclude_path = c("R/RcppExports.R", "R/generated_bindings.R"))
gp(".")
```
Or via the `GP_EXCLUDE_PATH` environment variable:
```bash
GP_EXCLUDE_PATH=R/RcppExports.R,R/generated.R Rscript -e 'goodpractice::gp(".")'
```
Excluded files are skipped by lintr, treesitter, expression, and roxygen2 checks.
## Parallel preparation
Preparation steps run sequentially by default.
If you have the [future.apply](https://cran.r-project.org/package=future.apply) package installed, you can run them in parallel by setting a `future::plan()`:
```{r future-dummy, eval = FALSE}
future::plan("multisession")
gp(".")
```
This can significantly speed up runs on large packages where multiple slow preps (covr, rcmdcheck, lintr) would otherwise run one after another.
Preps run in parallel only when a non-sequential plan is active — the default behaviour is unchanged.
## Tidyverse style checks
The default checks deliberately stay away from style preferences — they focus on things that are good practice regardless of how you format your code.
If you or your team follows the [tidyverse style guide](https://style.tidyverse.org/), you can opt into an additional set of style checks:
```{r tidyverse-only-dummy, eval = FALSE}
# add tidyverse checks to the defaults
gp(".", checks = c(default_checks(), tidyverse_checks()))
# or run only the tidyverse checks
gp(".", checks = tidyverse_checks())
```
Most of these are powered by [lintr](https://cran.r-project.org/package=lintr) — brace placement, spacing, naming conventions, and so on.
They run `lintr::lint_package()` once and share the results across all lintr-based checks.
If your package has a `.lintr` configuration file, those settings are respected: disabled linters stay disabled, exclusions are honoured.
A few tidyverse checks go beyond linting and look at package structure directly — whether R file names use snake_case, whether test files mirror source files, whether functions use `missing()` where `NULL` defaults would be clearer, and whether exported functions appear before internal helpers.
All tidyverse checks belong to the `tidyverse` group:
```{r checks-by-group-tidyverse}
checks_by_group("tidyverse")
```
## Working with results
Beyond printing, the object returned by `gp()` gives you programmatic access to the results.
`checks()` returns the names of all checks that were run:
```{r check-names}
checks(g_url)
```
`failed_checks()` returns only the names of the checks that failed:
```{r failed-checks}
failed_checks(g)
```
`results()` gives you a data frame with one row per check and a `passed` column that is `TRUE`, `FALSE`, or `NA` (skipped):
```{r results}
results(g)[1L:5L, ]
```
A check shows `NA` when its preparation step failed or was excluded — it was not evaluated, so it neither passed nor failed.
To see all file positions for failed checks (not just the first five that `print()` shows), use `print()` with a higher limit:
```{r print-inf-dummy, eval = FALSE}
print(g, positions_limit = Inf)
```
You can also export the full results to JSON for use in other tools:
```{r export-json-dummy, eval = FALSE}
export_json(g, "gp_results.json")
```
## Custom checks
goodpractice is extensible — you can define your own checks and preparation steps to enforce team-specific conventions.
The `gp` string in each check supports [cli inline markup](https://cli.r-lib.org/reference/inline-markup.html) — use `{.fn func}` for function names, `{.code expression}` for code, `{.file path}` for file paths, `{.field name}` for field names, and `{.url url}` for URLs.
See `vignette("custom_checks")` for the full guide.