Changelog
Source:NEWS.md
checktor 0.2.0
Every check now carries a severity tier, so a clean bill of health tells you something precise. Your package is submission ready, and nothing here will crash a user. New checks widen that ground by bringing part of CRAN’s incoming filter offline, covering the Date, Encoding and Version fields, the structure of Authors@R, ORCID and ROR identifiers, a detectCores() that can return NA, and a scan for a leaked credential. Many existing checks are sharper and quieter, every diagnostic is now exported and callable on its own, and a package can tune checktor through Config/checktor/* fields in its own DESCRIPTION.
Breaking changes
-
Every check now carries a severity tier, and
checktor()gained aseverityargument that decides which tiers the verdict is about. It defaults to policy and robustness.- A policy finding is a citable violation of CRAN Repository Policy or Writing R Extensions.
- A robustness finding is a real defect that CRAN will still let you ship, such as a
detectCores()that may returnNA. - An opinion finding is a convention with no authority behind it.
Every check still runs and every finding is still reported with its tier. The tier only decides whether a finding counts against a clean bill of health, so zero issues now means your package is submission ready and nothing here will crash a user.
checkup()follows the same default, so a missingNEWS.mdno longer fails a build. A check’s tier reflects where its authority sits rather than how much we like it, which is whyvalue_tagsandmissing_examplessit at opinion, since noR CMD checkequivalent exists for either. Every
diagnose_*function is now exported. The old split was accidental, withdiagnose_tf_usage()public whilediagnose_option_changes()was not. The DESCRIPTION checks used to take a pre-parsedDESCRIPTIONas their first argument, which is not something a caller has, so they now take(path, verbose, desc = NULL)like every other check.description_bare_rwas removed. It flagged every bareRin theDescriptionand asked you to quote it, but Writing R Extensions reserves single quotes for other packages and external software, andRis the host language, so most packages write it bare and stay on CRAN. The newlanguage_namescheck settles the question the right way by leavingRalone.-
Two checks were dropped from the default run because no authority supports them, and each stays exported and callable for anyone who wants it.
-
title_starts_with_articlewas a mis-transplant of a real CRAN rule that applies to theDescriptionfield and requires the literal word “package” after the article. Plenty of packages sit on CRAN with titles that begin with an article. -
description_function_quotesasserted that single quotes are reserved for software names. Writing R Extensions actually treats them as an inclusive list for non-English usage that includes other packages, so a quoted function name breaks no rule.
-
New checks
detect_cores_robustnessflags adetectCores()result used without anNAguard. The help fordetectCores()says plainly that it returns an integer, orNAwhen the answer is unknown, andNA - 1isNA, so the next comparison dies withmissing value where TRUE/FALSE needed. The fix isparallelly::availableCores(), which never returnsNA.-
A family of new checks mirrors CRAN’s incoming filter, the run that happens under
R CMD check --as-cranagainst a built tarball, so you can see the same findings offline against your own sources before you submit.-
date_formatflags aDatefield that is not ISO 8601yyyy-mm-dd, is over a month old, or lies in the future. -
version_formatflags aVersioncomponent with a leading zero or a suspiciously large one, while leaving a calendar-year version alone. -
encoding_utf8flags a non-portableEncoding, meaning one outside theUTF-8,latin1andlatin2that Writing R Extensions names as portable. -
identifier_formatvalidates the ORCID and ROR identifiers inAuthors@R, checking ORCID against its checksum and ROR against its shape.
-
hardcoded_credentialsscans string literals inR/for a leaked secret. It knows the tokens and keys used by providers such as GitHub, AWS, Google, OpenAI, Anthropic and Stripe, along with PEM private keys and JSON Web Tokens, each recognised by its published prefix.R CMD checkdoes not look for these, and a token published to CRAN is public and must be revoked. Only string literals are examined, so the same text in a comment never matches. See?diagnose_hardcoded_credentialsfor the full list.spellingrunsutils::aspell()over theTitleandDescriptionto mirror CRAN’s incoming spelling pass and report words that look misspelled. It reads any.aspell/dictionary,inst/WORDLIST, orConfig/checktorvocabulary you already keep, so you can silence it however you prefer. It needs a spell-check backend to run and passes quietly without one, exactly as CRAN’s check skips spelling when none is present, which is why it sits at opinion tier and can be turned off withoptions(checktor.spelling = FALSE). When it fires,prescribe()prints a ready-to-paste.aspell/snippet with the flagged words filled in, sinceinst/WORDLISTalone does not clear CRAN’s aspell NOTE but a.aspell/dictionary does.url_livenessfetches every URL in the DESCRIPTION,.Rdfiles and vignettes and reports the ones that return an error, a 404, or a redirect. This is whatR CMD check --as-crandoes through the same base R machinery intools::check_package_urls(), with no dependency on theurlcheckerpackage. Because it needs a network and is slow, it is opt-in and does nothing until you setoptions(checktor.url_check = TRUE), passing quietly whenever it is off. It is the online half of the URL story, andurlsremains the fast offline half that flagshttp://and shorteners without leaving the room.language_namesflags a bare programming-language, markup, or statistical-computing name in theTitleorDescriptionthat CRAN asks to see single-quoted, covering names likePython,Java,C++,SQL,HTML,MATLABandSAS. It is the language counterpart tosoftware_names, and both are policy-tier quoting checks kept separate because a language name and a package name are different kinds of thing. Single-letter and common-word names are left out to avoid false positives, and you can extend the list withConfig/checktor/language_names.
Configuration and extension
A package can now configure checktor through
Config/checktor/*fields in its own DESCRIPTION. Thedisablefield skips a check,allowmutes reviewed findings for either a whole check or acheck:substringand gives a greencheckup()gate the escape hatch it needs, andsoftware_names,language_namesandacronymsextend those checks’ vocabularies. A package with no such fields is unaffected.register_check()adds a custom diagnostic to everychecktor()run without editing checktor’s source. You give it a name, a function that returns achecktor_check_result(), a category, and a severity tier, and the check then runs alongside the built-ins, appears inissues()andtidy(), and counts toward the verdict at its tier.unregister_check()andregistered_checks()manage the registry.The AST toolkit that the built-in checks use is now exported, so a registered check has the same tools available. These include
read_r_xml(),xpath_lints(),xpath_per_file(),undesirable_function_check(),not_under_fn_with_call_xpath(), and the.Rdwalkersextract_rd_section()andcollect_rd_text(). The Writing Your Own Checks vignette walks through building and registering one.
Checks improved
Several checks are now substantially more accurate, and a few hand off to R’s own engines instead of reimplementing them.
home_writingnow flags a write whose destination resolves to the user’s home, such aswriteLines(x, "~/leaked.txt")orsaveRDS(x, "~/.cache/x.rds"). It previously looked only at reads likeSys.getenv("HOME"), so the writes that actually matter slipped past.globalenv_modnow flags a<<-only when its target genuinely reaches.GlobalEnv, meaning it binds in neither an enclosing function nor the package. The two correct idioms, a closure updating its parent frame and the package-level cache written as.cache <<- ..., both come out clean.core_usagenow inspects the worker count itself and understands theparallel,snow,foreach,future,furrr,mirai,RcppParallel,data.tableandBiocParallelframeworks. It no longer keys off anmc.coresargument, which belongs tomclapply()alone, sodetectCores()and a compliantmakeCluster(2L)are no longer flagged.roxygen_usagenow detects roxygen that never reachedNAMESPACE, such as a function tagged@exportthat is not actually exported. That is the real cost of a forgottendevtools::document(), and it is somethingR CMD checkcannot see. It readsNAMESPACErather than file timestamps, so it behaves the same in CI, wheregitdoes not preserve modification times.license_yearnow flags a genuinely unfilledLICENSEtemplate, meaning a leftover<YEAR>or<COPYRIGHT HOLDER>that CRAN asks you to complete, rather than a valid but non-current year.authorsnow catches an unfilledusethistemplate such asperson("First", "Last", , "you@example.com", ...), whichR CMD checkpasses because the field is present but a CRAN reviewer sends back. It also validates the structure of the field and flags a person with no name, a person with no role, anAuthors@Rthat does not parse, or a missing maintainer.title_caseandlicensenow hand off to R’s own engines intools::toTitleCase()andtools::analyze_license(), so they match R’s behaviour, andtitle_casehandles quoted package names and punctuation the way R does.licensealso flags a bareMIT, which needsMIT + file LICENSEpointing at a file that exists.value_tagswalks each.Rdwithtools::parse_Rd()and exempts data, class, package and\keyword{internal}topics, so its verdict no longer depends on the R version.print_cat_usagenow flags unsuppressable console output only from a function that also returns a value, and it treats a verbosity gate as the guard rather than any enclosingif,fororwhile.
Understands more of R
checktor reads far more of the ways R is actually written, so a clean run reflects the code you wrote.
NAMESPACEis parsed with R’s ownbase::parseNamespaceFile(), so a multi-lineexport()block, anexportPattern(), and a method under a quoted non-syntactic generic such asS3method("[", foo)all read correctly. Every “is this exported?” check now sees a package’s full set of exports rather than just the first.An
=assignment is read as an assignment, and a classic"print.foo" <- function(x)definition, whose name parses as aSTR_CONST, is visible to every name-based exemption. A package that defines nearly all of its functions that way now reads correctly.An S4
setMethod("show", ...)is treated as an output method wherecat()is the required idiom,app$cat(...)is a method call rather thanbase::cat, and a verbosity flag namedmessagescounts as a gate.A
<<-insidelocal(),setRefClass()orR6Class()binds in that scope rather than.GlobalEnv, a call in a default argument is scoped to that argument rather than the function body, which preserves theon.exit()thatoption_changes,temp_cleanupandsys_setenvrely on, and only the R chunks of a vignette are parsed, so its prose stays prose.
checktor also tells a read apart from a write, and a package’s own state apart from the user’s.
options()andpar()both read and write, and only a named argument makes the call a write, sopar("usr")[3]and a package’s ownreset_options()stay clean. A restore factored into its own helper and registered by the caller withon.exit(restore_par(op))is recognised as the restore it is, not flagged as an unreset change.A package’s own namespaced option such as
options(datatable.verbose = ...)is its own state, and asetwd()oroptions()inside acallrsubprocess cannot reach the calling session.file_operationsproves where a write lands, sowriteLines(x, "out.csv")is flagged,writeLines(x, out_file)is trusted to the caller who passed the path, and a formal that defaults into~is still caught.package_sizemeasures the gzipped tarball that CRAN actually limits, which is often far smaller than the files on disk.A
Sys.setenv()setter that captures the prior state and hands it back, as inold <- get_path(); Sys.setenv(...); invisible(old), honours the same restore contractoption_changesalready follows. Many packages’ env-var and path setters are shaped that way.
checktor recognises the guards CRAN sanctions, too.
if (require("pkgB"))and roxygen’s@examplesIfboth count as the conditional-Suggests guard in an example, whileinteractive()does not, because it does not make the package available. Asystem()call inside an OS branch is the platform check the fix asks for, and aninstall.packages()behind a consent prompt is consent.set.seed(123)insideif (FALSE)cannot reach the RNG, andTorFinsidequote(),expression()orsubstitute()are language tokens rather than logicals.commented_examplesfires only when an entire\examples{}block is commented out.example_structureaccepts a database, a prompt, or a Shiny reactive context as a reason for\dontrun{}, and it now takes an install or launcher call or apath/to/...placeholder the same way. Andlibrary_in_pkgexempts code sent to a parallel worker, whose search path starts empty.software_namesflags the R-package and software-product names CRAN asks to see quoted, along withWebAssembly, a specific format the R WebAssembly ecosystem and CRAN quote consistently, and it recognisesWASM,webRandShinylivewhen quoted. Programming-language and markup names moved to their ownlanguage_namescheck. A package can add its own names withConfig/checktor/software_names.A few smaller sharpenings round this out.
description_quoted_quotesflags only a recognised software name rather than scare-quoted jargon,description_lengthcounts words,description_starts_withgained its initial-capital rule,acronymsno longer flagsCMD, andurlsnames the offending URL while skipping fenced code and\verb{}spans.
Bug fixes
package_sizenow honours a bare directory entry in.Rbuildignore, such as the^docs$a pkgdown package uses. R CMD build excludes a matched directory’s whole subtree, so an untrackeddocs/or.quartocache never reaches the tarball.package_sizematched only leaf paths, so it counted those trees and could report a package many times its real tarball size. It now tests each file’s ancestor directories too, case-insensitively, the way R does.mean(x, na.rm = T), the most common bareTin R, is now reported. An argument name parses asSYMBOL_SUBrather thanSYMBOL, so a guard meant to skipf(T = 1)was inadvertently skipping the argument value too.prescribe()now surfaces every failed check. It previously walked only the curated treatment list, so a check could fail andprescribe()would say nothing (#4, thanks @january3).prescribe()no longer prints rawclimarkup. Treatment strings carry inline markup such as{.code TRUE}, and it reachedclias an interpolated value rather than as part of the format string.diagnose_print_cat_usage()no longer flagscat()inside S3print.*andformat.*methods, where it is the required idiom, and base R’s ownprint.default()uses it (#6, thanks @jhelvy).The
acronymscheck treatsprincipal component analysis (PCA)andPCA (principal component analysis)alike as explained, and it detects a line-wrapped gloss (#5, thanks @january3).example_diagnose_scenario()no longer prints the temporary package path, keeping machine-specific paths out of help pages.
Documentation and website
The three vignettes gained figures. There is a coverage map of what
R CMD check,lintrandchecktoreach catch, a view of the three data frames the accessors return,checkup()running at three latencies in CI, and, for Writing Your Own Checks, the road from source to finding alongside the XPath axes around aSYMBOL_FUNCTION_CALLanchor.Two claims in Getting Started were corrected.
R CMD checkdoes flag aTitlethat is not in title case, andlintrdoes flag a bareT, so neither belongs in the list of things only checktor catches.The pkgdown site picked up a theme drawn from the package logo, with a light and dark toggle in the navbar.
checktor 0.1.0
CRAN release: 2026-07-02
- Initial release.
- Adds [
checktor()] as the top-level orchestrator, running five categories of diagnostics (code, DESCRIPTION, documentation, general, CRAN policy) against an R package directory. - Adds the [
checkup()] boolean wrapper for CI use, [prescribe()] for treatment recommendations, and [health_report()] for Markdown / HTML / text reports. - All code-side diagnostics run XPath queries against the parsed AST via
xmlparsedata+xml2. Documentation-side checks walk.Rdfiles viatools::parse_Rd(). DESCRIPTION is parsed withbase::read.dcf(). - Added result accessors so you no longer navigate nested lists:
issues()(per-issue table),tidy()(per-check table),summary()(per-category), pluspassed(),is_healthy(),n_issues(),n_failed_checks(), andfailed_checks().as.data.frame()on a result is equivalent totidy(). - Expanded the CRAN-submission diagnostics with additional heuristics:
- General: flags a missing
NEWSfile (diagnose_news_file()) andREADMErelative links whose target is missing or excluded by.Rbuildignoreand so absent from the built tarball (diagnose_readme_relative_links()).diagnose_cran_comments_file()is also provided but, since acran-comments.mdis a workflow convention rather than a CRAN requirement, it is opt-in and not part of the defaultchecktor()run. - DESCRIPTION: flags
Titlefields of 65 or more characters, single-quoted function names inTitle/Description(quotes are for software names), and over-capitalized small words in theTitle. - Documentation: flags exported functions whose
.Rdlacks an\examplessection (diagnose_missing_examples()) and examples that use a Suggested package without arequireNamespace()/@examplesIfguard (diagnose_suggested_in_examples()).
- General: flags a missing