DEV Community

ReadyStack
ReadyStack

Posted on Originally published at getreadystack.com

CRAN Policy Submission Lint

CRAN Policy Submission Lint

Eight lines - a licence string, a version number, a Description opening, a library() call, an unrestored par(), a write to ~/, a ::: and a stray T - are all it takes for CRAN to send an R maintainer's first submission back into the queue.

None of the eight is a bug. The package installs, the tests pass, R CMD check --as-cran is quiet about every one of them. They are policy: prose in the CRAN Repository Policy and Writing R Extensions that a volunteer reviewer applies by reading. That is the layer no tool in the standard R toolchain reads for you, and it is the layer that decides whether your tarball is accepted this week or next month.

Here is the split. R CMD check computes: an argument you documented but did not declare, an example that errors, an import you forgot. A reviewer judges: a Description opening with "This package provides", which the policy asks you not to do; License: MIT, which CRAN refuses without "+ file LICENSE" and a two-line LICENSE file beside it; library(dplyr) in R/, which a package may not do because attaching changes the user's search path; par(mfrow = c(1, 2)) never put back with on.exit(). All documented rules, none computable the way check works.

CRAN Policy Submission Lint encodes 25 of those rules and runs them on the file you have open. Ten cover DESCRIPTION: Title in title case with no trailing period, a Description that is a real sentence and does not begin with the package name, a licence string CRAN accepts, exactly one person() carrying role = "cre", no four-component or .9000 development version, no example.com placeholder address, a Date field neither in the future nor over a month stale, and packages declared under Imports rather than Depends. One covers NAMESPACE: exportPattern(), which ships your internals as public API. Fourteen cover R sources: library() and require(), install.packages(), setwd(), options()/par()/Sys.setenv() with no on.exit(), writes to ~/ instead of tempdir(), T and F, print() and cat() where message() belongs, \dontrun{}, non-ASCII characters, <<- and assign() to .GlobalEnv, installed.packages(), set.seed() inside a function, ::: into another package, and source() at package level.

The sample package that ships with the extension is one DESCRIPTION, one NAMESPACE and one R file. Run it through and you get 28 findings - 18 errors and 10 warnings - which is every one of the 25 rules firing at least once. Run the corrected version of the same three files and you get zero. That is the whole demonstration: same engine, same three files, 28 to 0.

R that an assistant drafts looks like R from scripts, because scripts are what it learned from - and a script may call library(), may setwd(), may use T. A package may not. Those three are also the cheapest things in the world to fix, once someone points at the line.

The cost of not pointing at it is a round trip. CRAN's reviewers are volunteers and submissions queue; a published package that fails gets an email with a dated archival deadline - a named day, after which it leaves the repository and every reverse dependency breaks. A freelance R developer in North America bills $50 to $100 an hour (Upwork, 2026); the rework is not the expensive part, the wait is.

Checking the file you have open is free and uncapped, in VS Code and in the browser tool, which runs the identical engine with nothing uploaded. The licence covers a different axis: sweeping every file in the package at once and writing a dated report you keep, commit beside the tarball, or paste into your submission comments.


Free in your browser (the same rules): https://getreadystack.com/tools/cran-policy-submission-lint

Licence ($29, once, 7-day refund): https://buy.polar.sh/polar_cl_3p5QJlCGs0PUdsfxchdv4o2Jyr2C8Op4waTgt0xQNDs

Top comments (0)