Changelog
Source:NEWS.md
checktor 0.2.0
Every check now carries a severity tier, so a clean bill of health means something precise rather than merely quiet. Checks read your examples, vignettes and demos as well as R/, which is where several of the most common rejections actually land. New checks bring 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. Existing checks are sharper and quieter, every check is callable on its own, findings can go straight to your build system, checktor runs from anywhere inside a package tree, and a package can tune checktor through Config/checktor/* fields in its own DESCRIPTION.
Breaking changes
checktor needs R 4.5.0 or later, where 0.1.0 asked only for R 3.5.0. R 4.5.0 added
tools::check_package_urls(), which the newurl_livenesscheck uses. An older installation stays on 0.1.0.-
The individual checks are now
lab_*(), so the doctor orders a panel of labs.diagnose_tf_usage()islab_tf_usage(), and the name afterlab_is the check nametidy()reports andConfig/checktorrefers to, which several old names did not match. The five category functions such asdiagnose_code_issues()keep their names, since they run a panel rather than one test. The names released in 0.1.0 were renamed outright rather than deprecated, so replacediagnose_withlab_in any call todiagnose_tf_usage(),diagnose_seed_setting(),diagnose_print_cat_usage(),diagnose_roxygen_usage(),diagnose_value_tags(),diagnose_example_structure(),diagnose_missing_examples(),diagnose_suggested_in_examples(),diagnose_package_size(),diagnose_urls(),diagnose_news_file()ordiagnose_cran_comments_file().diagnose_readme_relative_links()is nowlab_readme_links(), after thereadme_linkscheck it runs.Check names themselves are unchanged, so code that picks rows of
tidy()orissues()by check name keeps working. -
Every check carries a severity tier, and
checktor()gained aseverityargument deciding 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 accept, 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 what 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. On achecktor()result,metadata$total_issues,metadata$failed_checks,n_issues(),n_failed_checks()andis_healthy()now count those tiers alone, whileissues()andfailed_checks()still list every finding. A check that does not run is reported as skipped rather than as passing, so a clean bill of health never includes a check that never happened.
tidy()gained askippedcolumn andsummary()askippedcount, the names are inmetadata$skipped_checks, and the printed result and everyhealth_report()format name the checks that sat out. Printing a check or a category shows a skipped check as skipped,summary()counts it only as skipped, and a skipped row intidy()is neverpassed, so!passed & !skippedpicks out the failures and thepassed,failedandskippedcounts ofsummary()add up tochecks.passed()asks only whether a check failed, so it isTRUEfor a skipped one (#15, thanks @TroyHernandez).Every check is exported, so any check
checktor()runs is one you can call yourself. The checks of theDESCRIPTIONfields take(path, verbose, desc = NULL), as the code checks take(path, verbose, parsed = NULL), so either can reuse a parse you already have.descmay be whatread.dcf()returns, andissues()andtidy()gained aseveritycolumn.description_bare_rwas removed. It asked you to quote every bareRin theDescription, which is not a rule anyone enforces. Writing R Extensions asks for single quotes around other packages and external software without namingReither way, and both forms clear CRAN, solanguage_namesleaves a bareRand a quoted'R'alone alike and takes no position on which you prefer.Three checks left the default run because no authority supports them, and each stays exported for anyone who wants it. The CRAN rule behind
title_starts_with_articleapplies to theDescriptionand requires the word “package” after the article, not to theTitle. Writing R Extensions treats single quotes as an inclusive list for non-English usage that a quoted function name fits, which is whatdescription_function_quotesruled out. And?personsays authors who are natural persons hold copyright by default and need nocphrole, which is whatcph_roleasked for (#17, thanks @eddelbuettel).
New checks
detect_cores_robustnesscatches adetectCores()result used without anNAguard. The help says 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().-
A family of checks mirrors CRAN’s incoming filter, so you see those findings offline against your own sources before you submit.
-
date_formatcatches aDatethat is not ISO 8601yyyy-mm-dd, is over a month old, or lies in the future. -
version_formatcatches aVersioncomponent with a leading zero or a suspiciously large one, while leaving a calendar-year version alone. -
encoding_utf8catches anEncodingother than exactlyUTF-8. CRAN’s incoming check callslatin1andlatin2deprecated, and a lower-caseutf-8draws the same NOTE. -
identifier_formatvalidates the ORCID and ROR identifiers inAuthors@R, and is reported as skipped whenAuthors@Rcannot be read.
-
description_filecatches aDESCRIPTIONthat R cannot read: a line that is neither a field nor an indented continuation, a blank line that splits the file in two, or a file R cannot open at all, such as a directory in its place or one without read permission.R CMD buildandR CMD INSTALLboth stop on such a file. For a malformed line checktor used to skip theDESCRIPTIONchecks with nothing counted, so the file could not count against a clean bill of health, and for a blank line it judged the package on the fields above it alone. It is now a policy finding, the checks that read the fields, registered ones included, are reported as skipped, andprescribe()shows how to fix it.description_fieldscatches aDESCRIPTIONfield R does not know, which CRAN’s incoming check NOTEs. The usual one isRemotes, which CRAN ignores since it installs dependencies from CRAN and Bioconductor alone. A typo such asBugreports,ImportorSuggestis silently ignored by R, so the finding names the field that was meant.Config/fields,RoxygenNoteand the other forms R allows pass.description_placeholderscatches aTitle,Description,AuthororMaintainerstill holding the text ausethisorpackage.skeleton()template wrote, such as “What the Package Does (One Line, Title Case)”, with the tests CRAN’s incoming check uses.title_package_namecatches aTitlethat is just the package name or opens with it and a colon, as intoypkg: Fit Simple Models, which Writing R Extensions asks you not to do and CRAN’s incoming check NOTEs. ATitlethat opens with a name that is an ordinary word, as in “Survival Analysis”, is left alone, since CRAN accepts it routinely.license_file_unneededcatches+ file LICENSEon a standard license such asGPL-3,LGPL-3or Apache, which the CRAN Cookbook asks you to drop along with the file, since “these are part of R”. MIT and BSD, whose templates need the file, are left alone. ALICENSEthat adds attribution requirements is the exception CRAN allows, andConfig/checktor/allowrecords it.hardcoded_credentialsscans string literals inR/for a leaked secret, knowing 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. A token published to CRAN is public and must be revoked, andR CMD checkdoes not look for these. Only string literals are examined, so the same text in a comment never matches. See?lab_hardcoded_credentialsfor the full list.spellingrunsutils::aspell()over theTitleandDescriptionto mirror CRAN’s incoming spelling pass, with the same ignores (single-quoted names,fn()calls,<doi:...>targets) and British anden_statswords. It reads any.aspell/dictionary,inst/WORDLIST, orConfig/checktorvocabulary you already keep, and is reported as skipped when no spell-check backend is installed or the one found fails to run. Turn it off withoptions(checktor.spelling = FALSE). When it reports a word,prescribe()prints a ready-to-paste.aspell/snippet, 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, which is whatR CMD check --as-crandoes throughtools::check_package_urls(). It runs when you are at the console and stays off in scripts, in continuous integration and underR CMD check, where a slow or unreachable network would make the result depend on the machine rather than the package. Setoptions(checktor.url_check = TRUE)orFALSEto decide for yourself. With no network reachable it comes back marked as a check that did not run, rather than calling every link broken or quietly reading as though every link resolved, while a single unreachable host among reachable ones still counts.urlsremains the offline half, catchinghttp://links and shorteners without leaving the room.-
A family of checks reads the code outside
R/, where several of the most common rejections land. Every code check used to readR/alone, so an install in an example, a write to the working directory in a vignette, or anoptions()call ininst/demothat was never put back all went unseen. Each rule below is one a maintainer has received verbatim from CRAN.-
example_interactiveasks forif (interactive())where an interactive function is hidden in\dontrun{}, so a reader sees it is not for a script. It reports one in\donttest{}too, sinceR CMD check --as-cranruns that code, where a prompt errors and an app waits for input. It reads the parsed example, so a function named in a comment or a string is not a call, an appshinyApp()builds without printing is not a launch, and only a guard that encloses the call excuses it,@examplesIf interactive()included (#19, thanks @TanguyBarthelemy). A guard still counts when it is combined with&&, or with||against another guard, wrapped insuppressWarnings(), split by a comment or kept in a variable assigned before it on every path, andinteractive() && f()guardsf()as anifwould. An assignment on the right of&&or||, which may never run, is not such a path. -
example_installscatches installing a package from an example, a vignette or a demo. -
example_writescatches a write to anywhere buttempdir(), judged with the same destination logic theR/check uses. The write checks now share one list of what counts as a write, so they agree with each other, and it covers the readr, data.table, arrow, spreadsheet and JSON writers alongside the base ones.write_csv(),write_rds(),fwrite(),write_xlsx(),write_parquet()andggsave()are all seen now, inR/and in examples alike. -
example_statecatchesoptions(),par()or the working directory changed and never put back. A change is put back when its old value is captured and later handed to the setter of the same kind in the same file, as inold <- setwd(tempdir())thensetwd(old).par(no.readonly = TRUE)is a read, not a change, and anon.exit()outside a function or a call that runs code in a frame of its own (local(),with(),within(),eval(),evalq()) is no restore:R CMD checknever runs it, and knitr runs it straight away. -
example_internal_nscatches:::in an example or vignette. -
example_tf_usagecatchesTorFforTRUEorFALSEin an example, a vignette or a demo, judged by the same ruletf_usageapplies toR/. Tests are read only withtests = TRUE, since CRAN rarely reads them. -
example_unparseablecatches an\examples{}section that is not valid R,\dontrun{}included, and names the line the parser stopped on.R CMD checkwrites\dontrun{}code out as comments and never parses it, while reviewers run it and send back “Unexecutable code in man/…”. It is a robustness finding rather than policy, since Writing R Extensions lets\dontrun{}hold text that is not R.
internal_nscovers the same rule inR/, where a:::call reaches an object another author is free to change in routine maintenance.unexported_example_nsused to suggest adding:::to an example, which is the change CRAN asks you to undo, so it now says to export the object or keep the topic internal instead.Every check in this family reads an example as R runs it. An Rd
%comment is dropped, code that shares a line with a\dontrun{}or\donttest{}block is still read, a line of a hidden block that is not R, such as a<your key>placeholder, is passed over without hiding the rest of the example, and code under#ifdefis read for every platform. Sweave (.Rnw) vignettes are read alongside R Markdown and Quarto, and the line a finding gives is its line in the.Rdfile or vignette. -
rd_bibliographyreads the\bibcitet{},\bibcitep{}and\bibshow{}macros R 4.6.0 added to.Rdfiles and reports whatR CMD checkwould: a key no bibliography holds, which R drops from the page, a key cited but never listed, and aREFERENCESfile left at the top level. Keys are looked up as R does, in the package’sREFERENCES.rds,.Ror.bib, in R’s own bibliography, and forpkg::keyin an installed dependency, without runningREFERENCES.R.rd_bibliography_files, at robustness tier, reports aREFERENCES.bibwithoutbibtexinSuggestsand a bibliography that is not installed because it is outsideinst/or excluded by.Rbuildignore.language_namescatches a bare programming-language or statistical-computing name in theTitleorDescriptionthat CRAN asks to see single-quoted, covering names likePython,Java,JavaScript,Rust,MATLAB,SASandStata. It is the language counterpart tosoftware_names, kept separate because a language name and a package name are different kinds of thing. Single-letter and common-word names are left out so ordinary prose stays quiet, and you can extend the list withConfig/checktor/language_names. Likesoftware_names, it judges each mention on its own and skips a quoted longer name such as'MATLAB Runtime', a link, a DOI, a function call and a double-quoted title.format_namesreports a format or markup name written without single quotes in theTitleorDescription:JSON,HTML,XML,CSS,YAML,TOML,Markdown,LaTeX,TeXandSQL, plusC++,FortranandTcl, which CRAN packages write either way. It runs only when you call it, at opinion tier. A census of CRAN in September 2026 found that packages accepted at new-package review in the previous 18 months wrote these names bare 62% of the time, against 28% for the languageslanguage_namescovers, so a bare one does not count against a clean bill of health, and one in double quotes is not adescription_quoted_quotesfinding either (#16, thanks @TroyHernandez).Config/checktor/format_namesextends its list.code_exercisedcatches a package that exports code but ships no examples, no tests and no vignettes, which CRAN’s incoming check WARNs about as “No examples, no tests, no vignettes”.devtools::check()leaves that check off, so the WARNING usually surfaces only at submission. It counts whatR CMD checkruns: atests/testthat/folder withouttests/testthat.Ris not a test, and files.Rbuildignoreexcludes do not count.citation_filereadsinst/CITATIONfor the calls CRAN’s incoming check NOTEs: the old-stylecitEntry(),personList()andas.personList(), andpackageDescription(),library()orrequire(), which assume the package is installed when R already hands the file its DESCRIPTION asmeta. R’s ownif (!exists("meta") || is.null(meta))fallback is exempt. The file is parsed, never run, and one that does not parse is reported with its line.
Configuration and extension
checktor runs from anywhere inside a package (#12, thanks @january3). It walks up from the path you give it to find the
DESCRIPTION, so a call with your working directory inR/ortests/testthat/examines the whole package instead of failing, and a file works as well as a directory. Every entry point resolves the root the same way, andfind_package_root()is exported for custom checks. A directory outside any package still says so.A package can configure checktor through
Config/checktor/*fields in its own DESCRIPTION.disableskips a check,allowmutes reviewed findings for a whole check or acheck:substring, andsoftware_names,language_names,format_namesandacronymsextend those checks’ vocabularies. A package with no such fields is unaffected.ci_report()writes findings in the shape your build system reads, so each one lands on the line that caused it rather than in a log somebody has to scroll. Called with no arguments it examines the package, works out where it is running, and emits the right thing. GitHub Actions gets workflow commands that annotate the pull request diff, and Gitea and Forgejo read the same ones. GitLab gets a Code Quality report for the merge request diff, Azure Pipelines gets logging commands, and Checkstyle XML covers Jenkins, reviewdog and the review bots. SARIF is there for GitHub code scanning. It reports every tier, since an annotation is information rather than a verdict, andcheckup()stays the gate. Checks that did not run are named once alongside the findings, so a quiet pipeline never implies a check that never happened, and the report formats write a document even when nothing was found, which is what lets a forge clear the findings an earlier run left behind.A few checks sit outside every run, because no authority backs them or they ask about a submission workflow rather than the package itself. The summary a verbose
checktor()run prints now names them so you can find out they are there, andmetadata$on_request_checkscarries the list. Calling one is the only way to run it, and since they sit in the opinion tier, running one never changes a verdict.register_check()adds a check of your own to everychecktor()run without editing checktor’s source. Give it a name, a function returning achecktor_check_result(), a category and a severity tier, and it runs alongside the built-ins, appears inissues()andtidy(), and counts toward the verdict at its tier.unregister_check()andregistered_checks()manage the registry. A check that cannot run returnschecktor_check_result()withskipped = TRUEand askip_reason, and is reported as skipped like a built-in one (#15).The AST toolkit the built-in checks use is exported, so a registered check has the same tools:
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 more accurate, and a few hand off to R’s own engines instead of reimplementing them.
option_changessuggests a fix.prescribe()shows the two ways out, namespacing a setting you keep for the session asoptions(<PackageName>.key = ...), or restoring a temporary change withon.exit().home_writingcatches a write whose destination resolves to the user’s home, such aswriteLines(x, "~/leaked.txt"), or is an argument that defaults there, as infunction(x, path = "~/x.txt") writeLines(x, path). A read likeSys.getenv("HOME"), or a home path that is only the text being written or the file being copied, is not a write to the user’s home.globalenv_modreports a<<-only when its target genuinely reaches.GlobalEnv, so a closure updating its parent frame and a package-level cache written as.cache <<- ...both come out clean.core_usageinspects 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)come out clean.roxygen_usagespots roxygen that never reachedNAMESPACE, such as a function tagged@exportthat is not actually exported, which is the real cost of a forgottendocument()run and somethingR CMD checkcannot see. It readsNAMESPACErather than file timestamps, so it behaves the same in CI.license_yearlooks for a genuinely unfilledLICENSEtemplate, a leftover<YEAR>or<COPYRIGHT HOLDER>, rather than a valid but non-current year.authorscatches an unfilledusethistemplate such asperson("First", "Last", , "you@example.com", ...), or theGivennameandyourfault@somewhere.netofpackage.skeleton(), whichR CMD checkpasses because the field is present but a reviewer sends back. It also validates the field’s structure, including a person with no name or no role, anAuthors@Rthat does not parse, and a missing maintainer. It accepts the calls R’s own reader accepts from R 4.6.0 on, a value wrapped in parentheses included, and reports any other call, a namespacedutils::person()among them, sinceR CMD buildrefuses it as a malformedAuthors@Rfield. A person combined with a list inc(), from which R cannot read the authors, is reported as well.referencesapplies the rules of CRAN’s incoming check to theDescription, whichdevtools::check()turns off. It catches a URL outside angle brackets, a DOI written as ahttps://doi.org/link or a baredoi:, a publisher link that embeds a DOI, and an arXiv id or link where CRAN now asks for the arXiv DOI<doi:10.48550/arXiv.ID>. It used to accept<arXiv:...>as correct. Each rule is one finding quoting the references that break it, and an arXiv finding gives the DOI to write. A space afterdoi:is no longer reported, since R does not NOTE it and CRAN’s page links it all the same.title_caseandlicensehand off to R’s owntools::toTitleCase()andtools::analyze_license(), so they match R’s behaviour.licensealso catches a bareMIT, which needsMIT + file LICENSEpointing at a file that exists, and aLICENSEholding the full MIT or BSD text rather than theYEAR/COPYRIGHT HOLDERstub, whichR CMD checkNOTEs as invalid DCF.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_usagereports unsuppressable console output only from a function that also returns a value, and treats a verbosity gate as the guard rather than any enclosingif,fororwhile(#10, thanks @january3).network_operationsreads an example as parsed R rather than searching its text, so a function named in a comment, a string or an Rd%comment is not a call. A request counts as guarded only when a guard encloses it, such asif (curl::has_internet()),if (interactive())or an@examplesIfasking one of them, combined with&&or with||against another guard. Only a call that makes a request is reported, and a request function handed to something that calls it, as inlapply(urls, download.file), still counts, as does one called through parentheses, as in(download.file)(u, f), or wrapped byVectorize()orpurrr::possibly(). Code in\dontshow{}and\dontdiff{}is now checked, sinceR CMD checkruns it, even when the block sits against another one, as in\dontrun{f()}\dontdiff{g()}, which used to stop the example parsing; the other Rd example checks read such an example too. See?lab_network_operationsfor every guard it accepts.suggested_in_examplesreads an example as parsed R and judges each use of a Suggested package by the guards that enclose it, so a package named in a comment or a string is not a use, and a guard for another package, or one elsewhere in the example, no longer excuses the use. A condition that is false underR CMD check, such asinteractive(), excuses it too. A use inside\donttest{}is now reported, becauseR CMD check --as-cranruns that code, while R’s base and recommended packages, such as parallel, MASS and survival, are never reported, since they ship with R. The advice now suggestsrequireNamespace()rather thanrlang::is_installed(). See?lab_suggested_in_examplesfor every guard it accepts.donttest_vs_dontrunno longer suggests moving a slow\dontrun{}block to\donttest{}when the block uses a Suggested package without a guard, sinceR CMD check --as-cranruns\donttest{}code and the move would trade its advice for asuggested_in_examplesfinding. Each block is judged on its own, so such a block does not hold back the advice for another that is only slow.A run parses each help page, R file and example once, rather than once for every check that reads it, so
checktor()finishes in well under half the time it took.
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. 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.An S4
setMethod("show", ...)is 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, and only the R chunks of a vignette are parsed, so its prose stays prose. A chunk set not to run is skipped, whether its header sayseval = FALSEor a Quarto#| eval: falseline does, and the header is read to its last brace, so a figure caption with braces of its own does not hide the option after it.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 withon.exit(restore_par(op))is recognised as the restore it is. A package’s own namespaced option such asoptions(datatable.verbose = ...)is its own state, and asetwd()oroptions()inside acallrsubprocess cannot reach the calling session. ASys.setenv()setter that captures the prior state and hands it back honours the same restore contract.file_operationsproves where a write lands, sowriteLines(x, "out.csv")is reported,writeLines(x, out_file)is trusted to the caller who passed the path, and a formal that defaults into~is still caught. The write checks,file_operations,home_writingandexample_writes, find the destination as R matches arguments: a named argument such assep =does not move it,to =is wherefile.copy()andfile.rename()write, and a call on the right of|>or%>%takes the piped value as its first argument, somtcars |> write.csv("out.csv")is seen. A method that shares a writer’s name, such as htmltools’tags$svg(), is not a write.A
system()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_examplesreports only an\examples{}block commented out entirely, so a prose comment beside working code is left alone (#9, thanks @TanguyBarthelemy).example_structureaccepts a database, a prompt or a Shiny reactive context as a reason for\dontrun{}, and apath/to/...placeholder the same way. An install or a launcher call is not among them, since CRAN asks forif (interactive())there rather than for\dontrun{}.library_in_pkgexempts code sent to a parallel worker, whose search path starts empty.software_namescatches the R-package and software-product names CRAN asks to see quoted, along withWebAssembly, and recognisesWASM,webRandShinylivewhen quoted. Programming-language names moved tolanguage_namesand format and markup names toformat_names, and a package can add its own withConfig/checktor/software_names. It judges each place a name appears, so one quoted mention no longer excuses a bare one elsewhere. A name is not bare inside a quoted longer name such as'shiny.semantic', or in a span CRAN’s own incoming spell check skips, a<https://...>or<doi:...>link or a function call such aspurrr::map(). checktor also skips a plain web address and a double-quoted title, and does not read a dotted name such asshiny.semanticasshiny.Smaller sharpenings round this out.
description_quoted_quoteslooks only for a recognised software name rather than scare-quoted jargon, in theTitleas well as theDescription. It knows every namesoftware_namesandlanguage_namesask to see quoted, including those a package adds, and reads a lower-case"rust"or"r"as a word rather thanRustorR, or a quote glued to a word, as in"R"estrictions, as a quotation. A Title Case"Bugs"in theTitleis the word, notBUGS.description_lengthcounts words,description_starts_withgained its initial-capital rule,acronymsno longer reportsCMD,YAMLorTOML, andurlsnames the offending URL while skipping fenced code and\verb{}spans.urlsandnetwork_operationsread every vignette source R builds, Sweave’s.Rnwincluded, and nothing in a subfolder R does not build, and a URL ends at the brace closing a LaTeX\url{}.
Bug fixes
health_report()reports the CRAN policy findings. It skipped that panel entirely, so the citable rejections were missing from every report, and the text and HTML formats carried no findings at all. Every format now lists each failing check, and the Markdown report says when the sections include advisory findings that the headline total leaves out.A treatment line renders its markup instead of printing braces. The report showed
{.code message()}on screen, because the treatment reachedclias a value rather than as part of the format string.package_sizemeasures what CRAN actually limits. It honours a bare directory entry in.Rbuildignore, such as the^docs$a pkgdown package uses, testing each file’s ancestor directories as R does, so a pkgdowndocs/or a build directory left beside your sources no longer counts. It also estimates the gzipped tarball rather than summing the files on disk, which over-reported any package whose bulk is compressible text.issues()keeps the file and line of a finding that carries a label. Only the plainfile.R:12form used to parse, so a finding such asa.R:3 (otherpkg:::helper)or one from an example lost its location and could not be pointed at.library_in_pkgno longer reports a method that happens to be namedlibraryorrequire. An object calling its ownapi$library()was read as a call to the base function, which made the check awkward for packages built on reference classes.title_lengthtreats the width a package listing may truncate to as a width rather than a limit, so aTitlethat exactly fills it is no longer reported. It shows in full, and only a longer one loses its tail. The message now says how much would be cut instead of only that the title is long.cph_roleaccepts aCopyrightfield as well as acphrole. It reads the roles from the parsedAuthors@Rrather than searching the field’s text, so an address such ascph@example.comno longer passes for the role. A package with noAuthors@Ris read from itsAuthorfield, where a role in square brackets such asACME Corporation [cph]counts, rather than failing for the missing field.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 skipping the argument value too.The code checks read your code when the
keep.parse.dataoption is off, as it is whilesys.source()runs a file. R then keeps no parse tree, so every check that reads one saw an empty file and passed, whatever the code contained.prescribe()surfaces every failed check. It previously walked only the curated treatment list, so a check could fail andprescribe()would say nothing (#4, thanks @january3). Its output no longer shows raw markup either.prescribe()andhealth_report()take their treatments from one table, so a check’s remedy reads the same wherever it is printed. Every check now has one, whereprescribe()covered seven andhealth_report()three, and the text and HTML reports carry it too.prescribe()lists what a check found above its treatment.print_cat_usageno longer reportscat()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 reads a line-wrapped gloss (#5, thanks @january3). A gloss whose expansion is a quoted software name counts too, so writing'WebAssembly' (WASM)assoftware_namesasks satisfies both checks at once.acronymsskips anything in quotes, single or double, straight or typographic, so a'MATLAB'or'SPSS'written the waylanguage_namesasks is no longer reported as an unexplained acronym, and neither is one inside a quoted article title, even beside an em dash, an ellipsis or a non-breaking space. It also ignores the letters inside a link, a<doi:...>or a function call.readme_linksno longer reads[[subsetting in an R code block as a link (#13, thanks @TanguyBarthelemy).Checks skip whatever
.Rbuildignoreexcludes, such as {devtag}@devhelp pages andvignettes/articles/(#14, thanks @TanguyBarthelemy).urlsskips tilde-fenced and nested code blocks, and drops the backtick from a quoted URL.unexported_example_nsreads a topic whose alias begins with an operator, such as[.myclass, instead of stopping with “invalid regular expression” (#18, thanks @RodrigoZepeda).A check named in
Config/checktor/disableno longer runs. It used to run and print its finding before being dropped from the results. To turn a check off in every package without touching eachDESCRIPTION, setoptions(checktor.disable = ...)(#17, thanks @eddelbuettel).A finding prints the text it quotes from your package as written. A
Title, file name or README link reachedclias part of a template, so a brace in it, such as a tidyverse-style{pkg}or the{id}of a URL template, was evaluated as R code, which either ran it or turned the finding into an error. Messages naming\dontrun{}and\donttest{}keep their braces.A README, NEWS file, vignette or
DESCRIPTIONthat R cannot open, such as a directory where the file is expected or a file without read permission, no longer prints R’s own warnings in the middle of checktor’s output.example_diagnose_scenario()no longer prints the temporary package path, keeping machine-specific paths out of help pages. It names that package withtempfile(), so it no longer creates or advances.Random.seed, and two scenarios built in the same second can no longer share a directory. It places a scenario by its extension rather than its folder, an.Rfile inR/, an.Rdfile inman/, a vignette invignettes/and a.txtfile as theDESCRIPTION, sonetwork_examples/bad_network_example.Rdis read by the Rd checks rather than landing inR/. Any other extension is an error.example_diagnose_scenario()andshow_example_files()report throughcli, like the rest of checktor, so the fileshow_contentprints arrives as a message rather than on standard output. It still prints exactly as written, braces and all.The
configure_doctor()example puts back the options it sets. It relied on anon.exit()outside any function, which an example never runs at the right time, so running the example left them changed.
Documentation and website
Two new vignettes explain where the rules come from (#8, thanks @TanguyBarthelemy). Where the Checks Come From maps every check to the CRAN Repository Policy or Writing R Extensions section it rests on, and to the CRAN Cookbook recipe where the authority is a convention rather than a rule. What R CMD check Checks walks through every step
R CMD checkperforms, so the line between the standard checks and checktor’s is clear.Every check’s help page gained a Source section naming the rule behind it, a CRAN policy clause, a Writing R Extensions section, a CRAN Cookbook recipe, or an honest note that no rule applies, with a link wherever one exists.
The original 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.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, andhealth_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