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), source linting
(lintr),
cyclomatic complexity (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 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:
library(goodpractice)
# goodpractice ships with an example package that has some issues
pkg_path <- system.file("bad1", package = "goodpractice")
g <- gp(pkg_path)#> ── Preparing goodpractice for badpackage ───────────────────────────────────────
#> ℹ Preparing: description
#> ✔ Preparing: description [29ms]
#>
#> ℹ Preparing: code_structure
#> ✔ Preparing: code_structure [33ms]
#>
#> ℹ Preparing: namespace
#> ✔ Preparing: namespace [8ms]
#>
#> ℹ Preparing: covr
#> ✔ Preparing: covr [1.9s]
#>
#> ℹ Preparing: cyclocomp
#> ✔ Preparing: cyclocomp [6.2s]
#>
#> ℹ Preparing: package_structure
#> ✔ Preparing: package_structure [7ms]
#>
#> ℹ Preparing: lintr
#> ✔ Preparing: lintr [164ms]
#>
#> ℹ Preparing: rcmdcheck
#> ✔ Preparing: rcmdcheck [6.6s]
#>
#> ℹ Preparing: rd
#> ✔ Preparing: rd [8ms]
#>
#> ℹ Preparing: revdep
#> ✔ Preparing: revdep [260ms]
#>
#> ℹ Preparing: roxygen2
#> ✔ Preparing: roxygen2 [36ms]
#>
#> ℹ Preparing: spelling
#> ✔ Preparing: spelling [7ms]
#>
#> ℹ Preparing: urlchecker
#> ✔ Preparing: urlchecker [103ms]
#>
#> ℹ Preparing: vignette
#> ✔ Preparing: vignette [9ms]
#>
Printing the result shows only the checks that failed:
g
#> ── It is good practice to ──────────────────────────────────────────────────────
#>
#> ✖ remove or use internal functions that are defined but never called. Dead code
#> increases maintenance burden.
#>
#> R/semicolons.R:2
#> R/tf.R:2
#> R/tf.R:9
#> R/tf2.R:2
#>
#> ✖ not use Depends in DESCRIPTION, as it can cause name clashes, and poor
#> interaction with other packages. Use Imports instead.
#>
#> ✖ omit the Date field in DESCRIPTION. It is not required and it gets invalid
#> quite often. A build date will be added to the package when you perform `R
#> CMD build` on it.
#>
#> ✖ add a URL field to DESCRIPTION. It helps users find information about your
#> package online. If your package does not have a homepage, add an URL to
#> GitHub, or the CRAN package page.
#>
#> ✖ add a BugReports field to DESCRIPTION, and point it to a bug tracker. Many
#> online code hosting services provide bug trackers for free,
#> <https://github.com>, <https://gitlab.com>, etc.
#>
#> ✖ add a README.md (or README.Rmd) file to the top-level directory. A good
#> README describes what the package does, how to install it, and includes a
#> short example.
#>
#> ✖ add a NEWS.md file to track user-visible changes between releases. See
#> <https://style.tidyverse.org/news.html> for formatting guidance.
#>
#> ✖ use the .R file extension for R scripts, not .r or .q. CRAN requires the
#> uppercase .R extension.
#>
#> R/bad_extension.r
#>
#> ✖ omit trailing semicolons from code lines. They are not needed and most R
#> coding standards forbid them
#>
#> R/semicolons.R:4:30
#> R/semicolons.R:5:29
#> R/semicolons.R:9:38
#>
#> ✖ avoid `x == TRUE` or `x == FALSE`. Use `x` or `!x` directly. The comparison
#> is redundant and less readable.
#>
#> R/tf.R:3:7
#>
#> ✖ not import packages as a whole, as this can cause name clashes between the
#> imported packages, especially over time as packages change. Instead, import
#> only the specific functions you need.
#>
#> ✖ fix this R CMD check ERROR: VignetteBuilder package not declared: ‘knitr’ See
#> section ‘The DESCRIPTION file’ in the ‘Writing R Extensions’ manual.
#>
#> ────────────────────────────────────────────────────────────────────────────────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:
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.Run checks — each check reads from the stored results and returns pass, fail, or skip. A single preparation step can feed many checks — the
rcmdcheckstep alone powers over 200 individual checks, all drawn from a singleR CMD checkrun.
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:
length(default_checks())
#> [1] 308If 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():
# find checks related to URLs
grep("url", all_checks(), value = TRUE)
#> [1] "description_url" "description_urls_in_angle_brackets"
#> [3] "description_urls_not_http" "urlchecker_ok"
#> [5] "urlchecker_no_redirects"Then run just the ones you care about:
g_url <- gp(pkg_path, checks = "description_url")
#> ── Preparing goodpractice for badpackage ───────────────────────────────────────
#> ℹ Preparing: description
#> ✔ Preparing: description [10ms]
#>
g_url
#> ── It is good practice to ──────────────────────────────────────────────────────
#>
#> ✖ add a URL field to DESCRIPTION. It helps users find information about your
#> package online. If your package does not have a homepage, add an URL to
#> GitHub, or the CRAN package page.
#>
#> ────────────────────────────────────────────────────────────────────────────────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:
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 -
all_checks()— both combined
length(tidyverse_checks())
#> [1] 30
length(all_checks())
#> [1] 338Check groups
Every check belongs to at least one check group. Groups let you work with categories of checks instead of individual names:
all_check_groups()
#> [1] "covr" "cyclocomp" "description"
#> [4] "expressions" "lintr" "namespace"
#> [7] "rcmdcheck" "rd" "revdep"
#> [10] "roxygen2" "code_structure" "package_structure"
#> [13] "spelling" "tidyverse" "urlchecker"
#> [16] "vignette"To see which checks belong to a group, use
checks_by_group():
checks_by_group("description")
#> [1] "no_obsolete_deps" "no_description_depends"
#> [3] "no_description_date" "description_url"
#> [5] "description_not_start_with_package" "description_urls_in_angle_brackets"
#> [7] "description_doi_format" "description_urls_not_http"
#> [9] "no_description_duplicate_deps" "description_valid_roles"
#> [11] "description_pkgname_single_quoted" "description_bugreports"
#> [13] "reverse_dependencies"You can select multiple groups at once:
# run only DESCRIPTION and namespace checks
gp(".", checks = checks_by_group("description", "namespace"))The table below lists every group and what it covers:
| Group | What it checks |
|---|---|
rcmdcheck |
~200 checks from R CMD check: documentation, namespace,
compilation, tests, vignettes, CRAN compliance |
covr |
Whether all code is covered by tests |
cyclocomp |
Whether functions are too complex (default limit: 50) |
description |
Package metadata: URL, BugReports, dependency fields, author roles |
lintr |
~50 linters covering correctness (anyDuplicated,
class_equals), performance (matrix_apply,
fixed_regex), readability (redundant_ifelse,
nested_pipe), and testthat expectations |
namespace |
Whether importFrom() is used instead of whole-package
imports, no exportPattern()
|
rd |
Whether exported functions have \examples and
\value sections |
roxygen2 |
Roxygen2 tag validation: export/noRd tagging, unknown tags, inheritParams |
expressions |
Source expression parsing checks |
code_structure |
Code analysis: print() returns invisibly,
on.exit(add = TRUE), function length, duplicate bodies |
package_structure |
README exists, NEWS exists, .R file extension |
spelling |
Misspelled words in documentation |
urlchecker |
Broken or redirecting URLs in documentation |
revdep |
Whether CRAN reverse dependencies exist (informational) |
vignette |
Vignette-related checks |
tidyverse |
Tidyverse style checks (opt-in via
tidyverse_checks()) |
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:
# 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:
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.
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:
Or via the GP_EXCLUDE_PATH environment variable:
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
package installed, you can run them in parallel by setting a
future::plan():
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, you can opt into an additional set of style checks:
# 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 — 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:
checks_by_group("tidyverse")
#> [1] "tidyverse_brace_linter"
#> [2] "tidyverse_commas_linter"
#> [3] "tidyverse_commented_code_linter"
#> [4] "tidyverse_equals_na_linter"
#> [5] "tidyverse_function_left_parentheses_linter"
#> [6] "tidyverse_indentation_linter"
#> [7] "tidyverse_infix_spaces_linter"
#> [8] "tidyverse_object_length_linter"
#> [9] "tidyverse_object_name_linter"
#> [10] "tidyverse_object_usage_linter"
#> [11] "tidyverse_paren_body_linter"
#> [12] "tidyverse_pipe_consistency_linter"
#> [13] "tidyverse_pipe_continuation_linter"
#> [14] "tidyverse_quotes_linter"
#> [15] "tidyverse_return_linter"
#> [16] "tidyverse_spaces_inside_linter"
#> [17] "tidyverse_spaces_left_parentheses_linter"
#> [18] "tidyverse_trailing_blank_lines_linter"
#> [19] "tidyverse_trailing_whitespace_linter"
#> [20] "tidyverse_vector_logic_linter"
#> [21] "tidyverse_whitespace_linter"
#> [22] "tidyverse_assignment_linter"
#> [23] "tidyverse_line_length_linter"
#> [24] "tidyverse_semicolon_linter"
#> [25] "tidyverse_seq_linter"
#> [26] "tidyverse_T_and_F_symbol_linter"
#> [27] "tidyverse_r_file_names"
#> [28] "tidyverse_test_file_names"
#> [29] "tidyverse_no_missing"
#> [30] "tidyverse_export_order"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:
checks(g_url)
#> [1] "description_url"failed_checks() returns only the names of the checks
that failed:
failed_checks(g)
#> [1] "complexity_unused_internal"
#> [2] "no_description_depends"
#> [3] "no_description_date"
#> [4] "description_url"
#> [5] "description_bugreports"
#> [6] "has_readme"
#> [7] "has_news"
#> [8] "r_file_extension"
#> [9] "lintr_semicolon_linter"
#> [10] "lintr_redundant_equals_linter"
#> [11] "no_import_package_as_a_whole"
#> [12] "rcmdcheck_package_dependencies_present"results() gives you a data frame with one row per check
and a passed column that is TRUE,
FALSE, or NA (skipped):
results(g)[1:5, ]
#> check passed
#> 1 no_obsolete_deps TRUE
#> 2 print_return_invisible TRUE
#> 3 on_exit_has_add TRUE
#> 4 complexity_function_length TRUE
#> 5 complexity_unused_internal FALSEA 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:
print(g, positions_limit = Inf)You can also export the full results to JSON for use in other tools:
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 — 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.
