#' Explicitly declare module exports
#'
#' \code{box::export} explicitly marks a source file as a \pkg{box} module. If
#' can be used as an alternative to the \code{@export} tag comment to declare a
#' module’s exports.
#'
#' @usage \special{box::export(\dots)}
#' @param \dots zero or more unquoted names that should be exported from the
#' module.
#' @return \code{box::export} has no return value. It is called for its
#' side effect.
#'
#' @details
#' \code{box::export} can be called inside a module to specify the module’s
#' exports. If a module contains a call to \code{box::export}, this call
#' overrides any declarations made via the \code{@export} tag comment. When a
#' module contains multiple calls to \code{box::export}, the union of all thus
#' defined names is exported.
#'
#' A module can also contain an argument-less call to \code{box::export}. This
#' ensures that the module does not export any names. Otherwise, a module that
#' defines names but does not mark them as exported would be treated as a
#' \emph{legacy module}, and all default-visible names would be exported from
#' it. Default-visible names are names not starting with a dot (\code{.}).
#' Another use of \code{box::export()} is to enable a module without exports to
#' use \link[=mod-hooks]{module event hooks}.
#'
#' @note The preferred way of declaring exports is via the \code{@export} tag
#' comment. The main purpose of \code{box::export} is to explicitly prevent
#' exports, by being called without arguments.
#'
#' @seealso
#' \code{\link[=use]{box::use}} for information on declaring exports via
#' \code{@export}.
#' @export
export = function (...) {
mod_ns = mod_topenv(parent.frame())
if (! is_namespace(mod_ns)) {
throw('{"export";"} can only be called from inside a module')
}
check_dots_unnamed()
names = as.list(match.call())[-1L]
for (name in names) {
if (! is.name(name)) {
throw(
'{"export()";"} requires a list of unquoted names; ',
'{name;"} is not a name'
)
}
}
namespace_info(mod_ns, 'exports') = union(
namespace_info(mod_ns, 'exports'),
as.character(unlist(names))
)
namespace_info(mod_ns, 'legacy') = FALSE
invisible()
}
#' Find exported names in parsed module source
#'
#' @param info The module info.
#' @param exprs The list of expressions of the parsed module.
#' @param mod_ns The module namespace after evaluating the expressions.
#' @return \code{parse_export_specs} returns a potentially empty character
#' vector of exported names.
#'
#' @note
#' There are two situations in which the \code{@export} tag can be applied:
#' \enumerate{
#' \item When applied to assignments, the assigned name is exported.
#' \item When applied to a \code{box::use} call, the imported names are
#' exported. This can be the module name itself, any attached names, or
#' both. All names introduced by the \code{box::use} call are exported. See
#' \code{\link[box]{use}} for the rules governing what names are introduced
#' into the scope, and thus exported.
#' }
#' In any other situation, applying the \code{@export} tag is an error.
#' @keywords internal
parse_export_specs = function (info, exprs, mod_ns) {
parse_export = function (export) {
if (block_is_use_call(export)) {
imports = attr(export, 'call')[-1L]
aliases = names(imports) %||% character(length(imports))
flatmap_chr(reexport_names, imports, aliases, list(export))
} else {
# Attempt to extract a name from an arbitrary expressions.
block_name(export) %||% block_error(export)
}
}
reexport_names = function (declaration, alias, export) {
# Permit empty expression resulting from trailing comma.
if (declaration %==% quote(expr =) && alias %==% '') return()
spec = parse_spec(declaration, alias)
import = find_matching_import(namespace_info(mod_ns, 'imports'), spec)
if (is_mod_still_loading(import$info)) {
if (! is.null(spec$attach)) {
throw(
'invalid attempt to export names from an incompletely ',
'loaded, cyclic import (module {name;"}) in line {line}',
name = spec$name,
line = attr(export, 'location')[1L]
)
}
return(spec$alias)
}
export_names = mod_export_names(import$info, import$ns)
real_alias = if (is.null(spec$attach) || spec$explicit) spec$alias
c(names(attach_list(spec, export_names)), real_alias)
}
find_matching_import = function (imports, reexport) {
for (import in imports) {
if (import$spec %==% reexport) return(import)
}
}
exports = parse_export_tags(info, exprs, mod_ns)
unique(flatmap_chr(parse_export, exports))
}
block_error = function (export) {
throw(
'the {"@export";"} tag may only be applied to assignments or ',
'{"use";"} declarations;\n',
'used incorrectly in line {location}:\n',
' {paste(code, collapse = "\n ")}',
code = deparse(attr(export, 'call'), backtick = TRUE),
location = attr(export, 'location')[1L]
)
}
#' @keywords internal
#' @rdname parse_export_specs
use_call = quote(box::use)
#' @keywords internal
#' @rdname parse_export_specs
static_assign_calls = c(quote(`<-`), quote(`=`), quote(`:=`), quote(`<<-`))
#' @keywords internal
#' @rdname parse_export_specs
assign_calls = c(static_assign_calls, quote(assign), quote(makeActiveBinding))
#' @param call A call to test.
#' @keywords internal
#' @rdname parse_export_specs
is_static_assign_call = function (call) {
any(map_lgl(identical, static_assign_calls, call))
}
#' @keywords internal
#' @rdname parse_export_specs
is_assign_call = function (call) {
any(map_lgl(identical, assign_calls, call))
}
#' @param block A roxygen2 block to inspect.
#' @keywords internal
#' @rdname parse_export_specs
block_is_assign = function (block) {
is_assign_call(attr(block, 'call')[[1L]])
}
#' @keywords internal
#' @rdname parse_export_specs
block_is_use_call = function (block) {
attr(block, 'call')[[1L]] %==% use_call
}
#' @keywords internal
#' @rdname parse_export_specs
block_is_exported = function (block) {
'export' %in% names(block)
}
#' @keywords internal
#' @rdname parse_export_specs
block_name = function (block) {
attr(block, 'object')$alias
}
#' Extract comment tags from Roxygen block comments
#'
#' @param exprs The unevaluated expressions to parse.
#' @return \code{parse_export_tags} returns a list of \code{roxy_block}s for all
#' exported declarations.
#' @note The following code performs the same function as roxygen2 with a custom
#' \code{@} tag roclet. Unfortunately roxygen2 itself pulls in many
#' dependencies, making it less suitable for an infrastructure package such as
#' this one. Furthermore, the code license of roxygen2 is incompatible with
#' ours, so we cannot simply copy and paste the relevant code out. Luckily the
#' logic is straightforward to reimplement.
#' @keywords internal
parse_export_tags = function (info, exprs, mod_ns) {
refs = utils::getSrcref(exprs)
comment_refs = add_comments(refs)
is_exported = map_lgl(has_export_tag, comment_refs)
map(create_export_block, exprs[is_exported], refs[is_exported], list(info), list(mod_ns))
}
#' Collect export tag information
#'
#' @param expr The unevaluated expression represented by the tag.
#' @param ref The code reference \code{srcref} represented by the tag.
#' @return \code{create_export_block} returns an object of type
#' \code{roxy_block} represents an exported declaration expression, along with
#' its source code location.
#' @note This could be represented much simpler but we keep compatibility with
#' roxygen2 — at least for the time being — to make integration with the
#' roxygen2 API easier, should it become necessary.
#' @keywords internal
create_export_block = function (expr, ref, info, mod_ns) {
structure(
list(export = ''),
filename = attr(ref, 'srcfile')$filename,
location = as.vector(ref),
call = expr,
object = parse_object(info, expr, mod_ns),
class = 'roxy_block'
)
}
#' @keywords internal
#' @rdname create_export_block
parse_object = function (info, expr, mod_ns) {
if (is.character(expr)) {
if (expr %==% '_PACKAGE') {
value = list(path = info$source_path)
roxygen2_object(expr, value, 'package')
} else {
roxygen2_object(expr, expr, 'data')
}
} else if (is.call(expr)) {
if (expr[[1L]] %==% use_call) {
roxygen2_object('use', list(pkg = 'box', fun = 'use'), 'import')
} else {
# Attempt to extract a name from the LHS: recurse until the
# left-most token is a name or character literal (this is necessary
# because `"a" = 1` is valid R; but also to allow `assign` and
# `makeActiveBinding` calls).
while (is.call(expr) && length(expr) > 1L) {
if (is.name(expr[[2L]]) || is.character(expr[[2L]])) {
name = if (is_static_assign_call(expr[[1L]])) {
as.character(expr[[2L]])
} else {
# `assign(…)`, `makeActiveBinding(…)` and similar.
eval(expr[[2L]], mod_ns)
}
obj = if (bindingIsActive(name, mod_ns)) {
roxygen2_object(name, activeBindingFunction(name, mod_ns), 'active')
} else {
value = get(name, mod_ns)
if (is.function(value)) {
# FIXME: Add handling for S3
roxygen2_object(name, value, 'function')
} else {
roxygen2_object(name, value, NULL)
}
}
return(obj)
}
expr = expr[[2L]]
}
# We do not care about the rest.
NULL
}
} else {
NULL
}
}
#' @param alias The object name.
#' @param value The object value.
#' @param type The object type.
#' @keywords internal
#' @rdname create_export_block
roxygen2_object = function (alias, value, type) {
structure(
list(alias = alias, value = value, methods = NULL, topic = alias),
class = paste0('roxygen2_', c(type, 'object'))
)
}
#' Extend code regions to include leading comments and whitespace
#'
#' @param refs a list of the code region \code{srcref}s to extend.
#' @return \code{add_comment} returns a list of \code{srcref}s corresponding to
#' \code{srcref}, but extended to include the preceding comment block.
#' @keywords internal
add_comments = function (refs) {
if (length(refs) == 0L) return(list())
block_end_lines = map_int(`[[`, refs, 3L)
block_end_bytes = map_int(`[[`, refs, 4L)
block_start_lines = c(1L, block_end_lines[-length(block_end_lines)] + 1L)
srcfiles = map(attr, refs, 'srcfile')
llocs = map(c, block_start_lines, 1L, block_end_lines, block_end_bytes)
map(srcref, srcfiles, llocs)
}
#' Find \code{@export} tags in code regions
#'
#' @param ref The code region \code{srcref} to search.
#' @return \code{TRUE} if the given region is annotated with a \code{@export}
#' tag, \code{FALSE} otherwise.
#' @keywords internal
has_export_tag = function (ref) {
self = environment()
next_char = function () {
self$pos = pos + 1L
substr(line, pos, pos)
}
consume_char = function (chars) {
matched = next_char() %in% chars
if (! matched) self$pos = pos - 1L
matched
}
consume_chars = function (chars) {
while (pos != nchar(line) && consume_char(chars)) {}
}
consume_whitespace = function () {
consume_chars(c(' ', '\t'))
}
is_roxygen_comment = function () {
prev = pos
consume_whitespace()
if (! consume_char('#')) return(FALSE)
consume_chars('#')
if (! consume_char("'")) {
self$pos = prev
return(FALSE)
}
consume_whitespace()
TRUE
}
is_empty_or_comment = function () {
consume_whitespace()
pos == nchar(line) || consume_char('#')
}
is_export = function () {
self$pos = pos + 1L
len = nchar('@export')
substr(line, pos, pos + len) == '@export' &&
(pos + len > nchar(line) || substr(line, pos + len, pos + len) %in% c(' ', '\t'))
}
block = as.character(ref)
for (line in block) {
pos = 0L
if (! is_roxygen_comment()) {
if (is_empty_or_comment()) next else return(FALSE)
}
if (is_export()) return(TRUE)
}
FALSE
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.