mini-roxygen is a small, static implementation of a subset of
roxygen2.
The roxy CLI reads R source files and roxygen comments, then generates Rd
documentation and a NAMESPACE file.
It does not evaluate R code, load the package, or compile a native
extension.
roxygen2 is powerful and is the standard way to document an R package. That power comes from evaluation: to describe an object accurately, roxygen2 loads the package it is documenting.
For a package with compiled code, loading the package may require building its native code first. Regenerating the Rd files for a package written partly in C++ or Rust can therefore require a full compile, as well as a working toolchain, even for a documentation-only change.
The direct motivation was r-polars: roxygen2 cannot produce a single Rd file there until its Rust code has compiled, and without a warm cache, that compile alone easily takes twenty minutes.
mini-roxygen avoids that cost by trading away some coverage. It supports a minimal subset of roxygen comments, derives everything statically, and never loads or compiles the package. Some documentation that depends on evaluation is out of reach, but Rd generation itself takes only as long as the required static processing.
This is an early release: the command surface is small and the supported subset is still being refined. Generated files should be treated as build artifacts to review. Byte-for-byte compatibility with roxygen2 is not guaranteed.
| roxygen2 feature | mini-roxygen |
|---|---|
| DESCRIPTION defaults | Supported |
Package documentation (_PACKAGE) |
Supported |
| S3 methods and generic discovery | Supported (reads installed packages) |
| Inheritance within the package | Supported |
| Inheritance from installed packages | Supported (needs an R library path) |
| Documentation tags | Partial (a fixed set listed below) |
| NAMESPACE directives | Partial (no @evalNamespace) |
@inheritParams selectors |
Partial (name and -name only) |
| Markdown to Rd | Partial (no block quotes or raw HTML) |
| Raw Rd macros | Partial (\eqn, \deqn, some zero-argument macros) |
| S4 tags | Partial (static directives, no class loading) |
| S7 | Partial (literal new_class() definitions) |
| S3 registration helpers | Partial (signatures are configurable) |
Inline `r ` expressions |
Partial (substitutions are configurable) |
| Data objects | Partial (no generated @format) |
| Repeated scalar tags | Partial (one value per topic, no concatenation) |
| R6 | Partial (documented static value assignments) |
@eval, @template, @includeRmd, \Sexpr |
Not supported |
Compatibility with roxygen2 explains each limit. Two of them are lifted by configuration.
roxy doc [OPTIONS] [PACKAGE_PATH]
PACKAGE_PATH defaults to the current directory.
The package must contain a DESCRIPTION file. An R/ directory is optional.
When it is absent, the source set is empty. When present, the command reads the
.R files directly under R/, then writes man/*.Rd plus NAMESPACE. It
does not use .Rbuildignore to exclude source files.
Generated files carry the exact mini-roxygen (roxygen2 compatible) ownership
marker. Existing files with a recognized generated marker may be replaced or
left unchanged. A file without such a marker is treated as hand-written and is
not overwritten. A run with error diagnostics does not proceed to the
output-writing phase. Warnings and informational diagnostics are reported but
do not by themselves make the run fail.
R libraries serve two purposes: resolving S3 generics and inheriting
documentation from other packages with @inherit, @inheritParams, and
@inheritSection.
External inheritance is enabled only when at least one library path is
supplied. Without one, external inheritance remains disabled, and requests
that require it are diagnosed. Two options are available for supplying library
paths. Use either one.
--r-lib-path PATH is repeatable. Paths are searched in the order given.
Each value specifies exactly one path. Paths containing spaces are
supported.
roxy doc --r-lib-path /opt/R/library --r-lib-path /usr/local/R/library path/to/package--r-lib-paths PATH_LIST takes one OS-native path-list value, such as the
result of R's own library-path calculation:
roxy doc --r-lib-paths \
"$(Rscript -e 'cat(.libPaths(), sep=.Platform$path.sep)')" path/to/packageOnly the supplied paths are used. R/Rscript, R_LIBS, .libPaths(), and other
automatic environment or installation searches are not consulted. A missing
optional metadata file is treated as local-only information, while malformed
metadata is reported as a warning.
The supported catalog versions are R 4.5 and R 4.6. The catalog is selected
by decoding base/Meta/package.rds and reading the major.minor version from
its typed Built.R field, so patch releases use the catalog for their
corresponding major.minor version.
If no library path is supplied, base is missing, the metadata is
missing, unreadable, or undecodable, Built.R is missing, or its version is
older or newer than supported, the command warns and uses R 4.6 semantics as
a fallback. The warning includes the detected version when one is available,
so the fallback stays visible in automated builds.
The implementation is guided by roxygen2 8.1.0. Byte-for-byte equivalence with roxygen2 is not part of the compatibility contract.
The intended compatibility boundary is semantic: generated Rd should be
accepted by R's tools::parse_Rd, and generated NAMESPACE files should be
accepted by R's NAMESPACE parser with the intended directive meaning.
Headers, source-reference comments, directive ordering, whitespace, section
boundaries, and importFrom line wrapping may differ at the byte level.
Output whitespace is determined by the AST and renderer and is not part of the
semantic compatibility guarantee.
The documentation model supports the usual scalar and structured Rd fields:
@title, @description, @details, @return/@returns, @seealso,
@references, @note, @format, @source, @author, @param, @name,
@rdname, @docType, @aliases, @include, @keywords, @examples, @examplesIf, @usage,
@section, @order, @method, @noRd, @inherit, @inheritParams, and
@inheritSection. Multiple contributions are merged with source-aware
diagnostics for conflicts, missing parameters, cycles, and ambiguous
identities.
The NAMESPACE subset includes:
@export,@exportS3Method,@import, and@importFrom@rawNamespaceand@useDynLib@exportPattern,@exportClass,@exportMethod,@importClassesFrom, and@importMethodsFrom.
A documented top-level pkg::name expression is a static re-export. It is
merged into the shared reexports topic, emits the provider link list, and
adds the corresponding importFrom(pkg, name) and (for bare @export) the
member export. Private pkg:::name access, computed expressions, calls, and
re-exports with @name or @rdname are refused rather than guessed. The
generated provider description cannot be combined with an intro,
@description, or @details prose in the same block.
When a static re-export exists, reexports is reserved for its shared topic;
an ordinary topic using that name is rejected with a source-aware diagnostic.
Provider links use the static label convention name() for ordinary names;
infix names such as `%op%` keep their operator spelling without a
function suffix. No runtime inspection is used to distinguish other callable
and non-callable provider objects.
The S4-related tags produce static NAMESPACE directives. They do not load R classes or inspect S4 method tables.
A minimal S7 subset recognizes literal new_class() definitions with a direct
constructor = function(...) argument. These signatures are propagated
through simple aliases. S7 generics, unions, multi-dispatch, method metadata,
properties, and runtime introspection are not supported.
Markdown is enabled for every documentation block. The supported conversion
covers ordinary paragraphs, emphasis, strong text, links, inline code, lists,
tables, and fenced or indented code blocks within the implemented Rd subset.
Level-1 Markdown headings are flattened into prose with a warning. Use an
explicit @section Title: contribution when named section structure is needed.
Level-2 and deeper headings become Rd subsections. Markdown links to local or
external help topics are retained as Rd links when their target can be resolved
or checked.
Inheritance within the package is resolved statically, including recursive parameter inheritance.
@inherit, @inheritParams, and @inheritSection can also inherit from
topics in installed packages.
The donor's help database is read and converted into the internal Rd model.
Parameters, sections, and prose are projected into the inheriting topic.
Donor-relative links are qualified with the donor's package while that context
is still known. Every inheritable component is covered: parameters, return,
title, description, details, @seealso content, sections, references, examples,
and author.
External lookup remains disabled until a library path is supplied. While it is
disabled, a request that would need it produces an
external-inheritance-disabled warning
naming the topic, so a missing configuration does not silently drop
documentation. See External R packages for the
options that supply the paths.
Each scalar prose field can have only one value per topic, including
@seealso, @references, @note, and @author. This is a compatibility boundary:
implicit concatenation across repeated tags is not performed.
Put related entries in one Markdown body, usually a paragraph or a Markdown
list, instead of repeating the tag. The same rule applies when blocks are
merged with @rdname. A repeated valid value produces a source-aware
DuplicateTag error, and the first value is retained while diagnostics are collected.
Empty or invalid tags are reported as parse diagnostics and do not
consume the slot. @seealso NULL suppresses package fallback documentation.
It does not erase an explicit value from another block.
@examples and @examplesIf share one topic-wide slot and are likewise not
concatenated. When an examples section needs multiple parts or conditions, put
them in one body with blank lines, comments, or an explicit R if statement.
@docType is a typed topic directive. @docType NULL suppresses inferred
ordinary/data/package output; explicit values take precedence over suppression.
Equal explicit values merged by @rdname are deduplicated, while conflicting
values remain source-aware errors. A duplicate within one block is still an
error.
Some inputs are accepted only within a static subset.
Authors@R accepts statically parseable person() calls and vectors of
person() calls, with a restricted argument and string-escape grammar. The
generated author sections use recognized role codes. Unsupported forms are
diagnosed rather than evaluated.
Inline code is classified syntactically. A parseable single R expression is
emitted as \code. Code that cannot be classified safely is emitted as \verb.
The source spelling of generated usage, including defaults and multiline
expressions, is retained rather than evaluated.
Raw Rd support is intentionally limited. Equation macros that can be
isolated safely from the Markdown event stream are represented structurally
in the one- and two-argument forms of \eqn and \deqn. The zero-argument
prose macros \R, \dots, \ldots, \cr, and \sspace are also recognized.
Every other raw Rd macro, \tab included, and any malformed or overlapping
equation input produces a source-aware error and prevents that topic from
being generated. CLI processing continues to collect diagnostics from other
topics and exits nonzero.
@inheritParams supports the narrow name and -name selector forms. The
richer roxygen2 selection tail is not implemented: unsupported selector syntax
produces an unsupported-selection diagnostic and that inheritance request is
not applied.
Repeated inheritance requests. After targets are resolved to semantic donor
identities, each semantically identical @inherit, @inheritParams, or
@inheritSection request after the first produces one source-aware warning.
Section titles use the same formatting-insensitive semantic key as
section lookup, and only the first request is resolved. Field lists are compared
as sets. Parameter selector order remains significant where it can change the
selected result. Requests with different selections or donors retain their
original order so fallback and parameter union behavior are preserved. NULL
inheritance suppression keeps its topic-wide meaning.
Namespace names are decoded and validated before rendering. Non-syntactic
or reserved names are automatically quoted, directives are deduplicated and
sorted by rendered spelling, and importFrom names are merged per package.
The original spelling supplied by the author, such as the choice of single
quotes, is not preserved. Decoded names are rendered with the canonical
double-quote spelling when quoting is required. The output's physical wrapping
is a rendering choice.
DESCRIPTION supplies package documentation defaults such as title,
description, links, and authors when a package topic does not override or
suppress them. Encoding is accepted only when it is UTF-8. Roxygen and
markdown settings in DESCRIPTION do not switch the parser mode. Defaults are
applied after complete topic assembly, so explicit values win regardless of
block order. A NULL value contributed by a block suppresses the
corresponding fallback, but does not erase an explicit value from another block.
Multiple explicit single-value contributions are errors. Collate fields do not
reorder the source files. Their presence is retained only for the static
namespace and S3 ordering checks that need it.
Data-object topics receive static \docType{data} by default, usage, and
datasets keyword output. An explicit @docType replaces the inferred
document type while preserving the topic's data-object behavior. The automatic
format description that roxygen2 obtains by evaluating an object is not
generated. Without an explicit format, inherited format, or @format NULL,
mini-roxygen emits a missing-data-format warning.
R6 classes are supported when their documentation is written explicitly
and the class can be handled as a static value assignment. For example, a
top-level assignment such as Widget <- R6Class(...) can use ordinary tags
including @title, @description, @docType class, @usage NULL,
@format NULL, @section, @rdname, @aliases, and @export. mini-roxygen
does not specially parse or evaluate R6Class() or inspect its arguments and
body. It does not automatically generate documentation for public or private
methods, fields, active bindings, inheritance, or member sections. Packages
that disable roxygen2's automatic R6 documentation and maintain their class
documentation explicitly are a good fit for this subset. Runtime-generated
R6 classes and documentation that depends on R6 introspection are not
supported.
S3 generic discovery combines installed package metadata with a static base
catalog checked against R 4.5.3 and R 4.6.1. The catalog resolves base
primitive, group, and ordinary generics even when a standard installation has
no base/Meta/nsInfo.rds. The provider reads known S3 registrations
from base and recommended packages, and from packages named by the target
package's Depends and Imports fields.
For dotted method names, candidate generic prefixes are checked left to right
and the first, shortest proven generic is selected. A package-local binding
always shadows provider metadata for that candidate. If no prefix can be proven
to be a generic, the result remains unresolved rather than inferring intent
from dotted spelling. Use an explicit @method or
@exportS3Method when you need to state intent directly.
Mixed document types under one @rdname. When package documentation and a
data-object contribution share one @rdname, roxygen2 handles the mixed values
in source order. mini-roxygen reports an explicit error instead, because a
likely typo should not be hidden by source-order recovery.
These constructs require evaluation or file inclusion: @eval, @evalRd,
@evalNamespace, @template, @templateVar, @includeRmd, and \Sexpr.
General inline R evaluation is also unsupported. Inline `Rd ` expressions and
executable R code blocks are not run.
These roxygen2 tags are not implemented: @concept, @describeIn,
@example, @inheritDotParams, @rawRd, and @slot. Using one
produces an unknown-tag warning and the run continues, so
the omission is visible rather than silent. The tags listed under
Supported tags are the ones the model accepts.
@noMd is diagnosed because Markdown is always enabled. @md is accepted only
as a redundant declaration of that mode.
@include accepts one or more case-sensitive .R or .r filenames from the
package R/ directory, validates that each direct source exists, and does not
generate source ordering or DESCRIPTION Collate changes.
Block quotes, thematic breaks, raw HTML, and other unsupported Markdown constructs are diagnosed with source locations and recovered where possible. These are ordinary limits of the underlying Markdown conversion, not a claim that roxygen2 itself accepts every such construct without restriction.
Inline `r ` expressions are never evaluated. Instead, a substitution
table provides their results directly. S3 registrations made through a helper
function can't be proven statically on their own, so declaring the helper's
signature makes them visible to the static analysis.
Both live in mini-roxygen.toml, read independently of DESCRIPTION. The file
is searched for at the package root only, not in parent or nested
directories. The schema is strict: it may contain only the
[inline-r.substitutions] and [s3] tables described below.
[inline-r.substitutions]
'lifecycle::badge("stable")' = '\strong{[Stable]}'
'pkg::version()' = '0.1.0'Keys are source spellings and every value must be a quoted TOML string.
The Markdown code-span parser applies its normal outer-whitespace rule before
lookup. After that boundary handling, each key must match the inline `r `
expression exactly, including internal source spelling and arguments. Internal
whitespace is not normalized, because whitespace inside an R string, raw
string, or comment can change its meaning.
Each value is the final Rd fragment: not Markdown, and not an R expression. Use
Rd markup such as \emph{...} and \code{...} in replacement values. Values
are parsed and writer-validated before the table is used. An empty string is a
valid substitution and intentionally emits no fragment. Invalid entries are
diagnosed together and are not partially applied.
The nine badge spellings from the lifecycle
package are built in, so `r lifecycle::badge("stable")` resolves to its Rd
badge without configuration.
User entries override built-ins with the same key. A user entry that is never
encountered produces an unused-substitution warning. Built-in entries do not.
This mechanism is static lookup, not R evaluation: `r expression` is replaced
only when an exact configured key exists.
[[s3.registrars]]
function = "register_s3_method"
arguments = ["class", "generic", "method"]One registrar signature is built in and always enabled: vctrs'
s3_register(generic, class, method),
the helper most commonly vendored into packages for conditional registration.
Configured tables add exact bare or qualified callees. Each arguments array
must contain generic and class exactly once and may contain method once.
Argument names are matched exactly, without R's partial argument matching.
The generic and class must be statically known string literals to establish a
registration fact. If the method target is omitted or NULL, it defaults to
generic.class.
A bare symbol names a local method, and function or computed targets do not
create a named method block.
Registration facts provide Rd method metadata only: they never add NAMESPACE
directives. A documented target matched by a registration inherits its method
metadata even without @exportS3Method NULL, but it must still carry an export
tag or that NULL suppression, otherwise a warning is emitted. An
@exportS3Method NULL tag without resolvable registration metadata or an
explicit @method is an error.
Only registrar calls whose generic and class are statically proven string literals are extracted. Dynamic runtime registrations are not evaluated. They produce an informational diagnostic, but no registration fact, no Rd or NAMESPACE directive, and no fatal error. Decodable names and computed-expression arguments are treated as dynamic runtime values. Undecodable names, other statically non-string values, and undecodable string literals are invalid registrar calls. Ambiguous or malformed calls are diagnosed without guessing.
R source is parsed statically, and documentation blocks are associated with top-level source expressions without executing them.
Validation is strict. Tag names must begin with an ASCII letter. Required
values, singleton fields, malformed word lists, @section title/body
separators, and @order integer values are all checked. @section requires a
colon separating its title from its body. Ordered contributions sort ascending,
with missing orders last and source order breaking ties.
Diagnostics retain the originating source file and byte range whenever a source location exists. This makes malformed tags, unsupported evaluation, inheritance failures, namespace validation errors, and Markdown recovery diagnostics actionable without requiring the user to determine which source block produced the affected generated file.
The inline-R diagnostic codes are undefined-inline-r-substitution (error),
invalid-inline-r-substitution (error), unused-inline-r-substitution
(warning), and unsupported-inline-r (error).
MIT. See LICENSE. Third-party licensing and attribution details are in THIRD_PARTY_NOTICES.md.