From 8c82a079e8d333ed62ecadabfc6b726b8947baf0 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Fri, 20 Sep 2019 08:36:51 -0500 Subject: [PATCH] Massive refactoring of roxy_block() data structure * roxy_block() is now a list, containing a list of roxy_tags() Exported interface for getting multiple tags, a single tag, and a single value. * roxy_tag() now distinguishes between raw and parsed values * Both get massively improved print methods * block_warning() has been removed in favour of using more specific roxy_tag_waning() everywhere * Multiple uses of `@usage` now get a warning (due to use of consistent wrapper) Fixes #664 --- NAMESPACE | 7 + NEWS.md | 15 ++ R/block.R | 197 +++++++++++------- R/field.R | 2 - R/markdown.R | 8 - R/namespace.R | 136 +++++++------ R/object-defaults.R | 37 ++-- R/object-from-call.R | 18 +- R/object-import.R | 11 - R/parse.R | 20 +- R/rd-describe-in.R | 27 ++- R/rd-examples.R | 17 +- R/rd-template.R | 23 +-- R/rd.R | 176 ++++++++-------- R/roclet.R | 29 ++- R/tag-parser.R | 233 +++++++++++++++++++++ R/tag.R | 263 +++++------------------- R/tokenize.R | 5 +- R/topic.R | 1 - R/utils.R | 21 +- man/parse_package.Rd | 3 +- man/roc_proc_text.Rd | 7 +- man/roclet.Rd | 21 +- man/roxy_block.Rd | 63 ++++++ man/roxy_tag.Rd | 67 +------ man/tag_parsers.Rd | 64 ++++++ src/parser2.cpp | 3 +- tests/testthat/test-block-print.txt | 12 ++ tests/testthat/test-block.R | 26 ++- tests/testthat/test-namespace.R | 9 +- tests/testthat/test-object-s3.R | 2 +- tests/testthat/test-rd-examples.R | 2 +- tests/testthat/test-rd-inherit.R | 4 +- tests/testthat/test-rd-param.R | 17 -- tests/testthat/test-rd-raw.R | 2 +- tests/testthat/test-tag.R | 10 +- tests/testthat/test-tokenize.R | 13 +- vignettes/extending.Rmd | 301 +++++++--------------------- 38 files changed, 965 insertions(+), 907 deletions(-) create mode 100644 R/tag-parser.R create mode 100644 man/roxy_block.Rd create mode 100644 man/tag_parsers.Rd create mode 100644 tests/testthat/test-block-print.txt diff --git a/NAMESPACE b/NAMESPACE index 467da9e81..8541b68ca 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -8,6 +8,7 @@ S3method(default_export,s3method) S3method(default_export,s4class) S3method(default_export,s4generic) S3method(default_export,s4method) +S3method(format,object) S3method(format,roxy_field) S3method(format,roxy_field_alias) S3method(format,roxy_field_author) @@ -41,6 +42,7 @@ S3method(format,roxy_field_source) S3method(format,roxy_field_title) S3method(format,roxy_field_usage) S3method(format,roxy_field_value) +S3method(format,roxy_tag) S3method(merge,roxy_field) S3method(merge,roxy_field_inherit) S3method(merge,roxy_field_inherit_dot_params) @@ -75,6 +77,10 @@ S3method(roclet_process,roclet_vignette) S3method(roclet_tags,roclet_namespace) S3method(roclet_tags,roclet_rd) S3method(roclet_tags,roclet_vignette) +export(block_get_tag) +export(block_get_tag_value) +export(block_get_tags) +export(block_has_tags) export(env_file) export(env_package) export(is_s3_generic) @@ -98,6 +104,7 @@ export(roclet_output) export(roclet_preprocess) export(roclet_process) export(roclet_tags) +export(roxy_block) export(roxy_tag) export(roxy_tag_warning) export(roxygenise) diff --git a/NEWS.md b/NEWS.md index 87c3f3d57..6f45711bc 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,20 @@ # roxygen2 (development version) +* The internal `roxy_tag()` gains a new field: `raw`. This now always contains + the raw string value parsed from the file. `val` is only set after the tag + has been parsed. + +* The internal data structure used to represent blocks has been overhauled. + It is now documented and stable - see `roxy_block()` for details. If you're + one of the few people who have written a roxygen2 extension, this will + break your code - but the documentation, object structure, and print methods + are now much better that I hope it's not too annoying! You can also + learn more in the new `vignette("extending")` - a bit thanks to @mikldk + for getting this started (#882). + +* You now get a warning if you use multiple `@usage` statements. Previously, + the first was used without a warning + * Functions documented in `reexports` are now sorted alphabetically by package (#765). diff --git a/R/block.R b/R/block.R index 7efe5b96d..d5480576c 100644 --- a/R/block.R +++ b/R/block.R @@ -1,48 +1,78 @@ +#' Blocks +#' +#' @description +#' A `roxy_block` represents a single roxygen2 block. +#' +#' The `block_*` functions provide a few helpers for common operations: +#' * `block_has_tag(blocks, tags)`: does `block` contain any of these `tags`? +#' * `block_get_tags(block, tags)`: get all instances of `tags` +#' * `block_get_tag(block, tag)`: get single tag. Returns `NULL` if 0, +#' throws warning if more than 1. +#' * `block_get_tag_value(block, tag)`: gets `val` field from single tag. +#' +#' @param tags A list of [roxy_tag]s. +#' @param file,line Location of the `call` (i.e. the line after the last +#' line of the block). +#' @param call Expression associated with block. +#' @param object Optionally, the object associated with the block, found +#' by inspecting/evaluating `call`. +#' @param block A `roxy_block` to manipulate. +#' @param tag,tags Either a single tag name, or a character vector of tag names. +#' @export +#' @keywords internal +#' @examples +#' # The easiest way to see the structure of a roxy_block is to create one +#' # using parse_text: +#' text <- " +#' #' This is a title +#' #' +#' #' @param x,y A number +#' #' @export +#' f <- function(x, y) x + y +#' " +#' +#' # parse_text() returns a list of blocks, so I extract the first +#' block <- parse_text(text)[[1]] +#' block roxy_block <- function(tags, - filename, - location, + file, + line, call, object = NULL) { stopifnot(is.list(tags)) - stopifnot(is.character(filename)) - stopifnot(is.vector(location)) + stopifnot(is.character(file), length(file) == 1) + stopifnot(is.integer(line), length(line) == 1) structure( - tags, - filename = filename, - location = location, - call = call, - object = object, + list( + tags = tags, + file = file, + line = line, + call = call, + object = object + ), class = "roxy_block" ) } -roxy_block_copy <- function(block, tags) { - roxy_block( - tags, - filename = attr(block, "filename"), - location = attr(block, "location"), - call = attr(block, "call"), - object = attr(block, "object") - ) -} - is_roxy_block <- function(x) inherits(x, "roxy_block") #' @export print.roxy_block <- function(x, ...) { - call <- deparse(attr(x, "call"), nlines = 2) + call <- deparse(x$call, nlines = 2) if (length(call) == 2) { call <- paste0(call[[1]], " ...") } - - cat_line(" @ ", block_location(x)) - cat_line(" Tags: ", paste0(names(x), collapse = ", ")) - cat_line(" Call: ", call) - cat_line(" Obj ? ", !is.null(attr(x, "object"))) + obj <- format(x$object) + + cat_line(" [", basename(x$file), ":", x$line, "]") + cat_line(" $tag") + cat_line(" ", map_chr(x$tags, format, file = x$file)) + cat_line(" $call ", call) + cat_line(" $object ", obj[[1]]) + cat_line(" ", obj[-1]) } -# Creates roxy_block from list of raw tags , block_create <- function(tokens, call, srcref, registry = list(), global_options = list()) { @@ -54,8 +84,8 @@ block_create <- function(tokens, call, srcref, if (length(tags) == 0) return() roxy_block(tags, - filename = attr(srcref, "srcfile")$filename, - location = as.vector(srcref), + file = attr(srcref, "srcfile")$filename, + line = as.vector(srcref)[[1]], call = call ) } @@ -75,17 +105,13 @@ block_evaluate <- function(block, env, global_options = list() ) { - is_eval <- names(block) == "eval" - eval <- block[is_eval] - if (length(eval) == 0) + tags <- block_get_tags(block, "eval") + if (length(tags) == 0) { return(block) + } # Evaluate - results <- lapply(eval, block_eval, - block = block, - env = env, - tag_name = "@eval" - ) + results <- lapply(tags, roxy_tag_eval, env = env) results <- lapply(results, function(x) { if (is.null(x)) { character() @@ -96,8 +122,8 @@ block_evaluate <- function(block, env, # Tokenise and parse tokens <- lapply(results, tokenise_block, - file = attr(block, "filename"), - offset = attr(block, "location")[[1]] + file = block$file, + offset = block$line ) tags <- lapply(tokens, parse_tags, registry = registry, @@ -105,63 +131,88 @@ block_evaluate <- function(block, env, ) # Interpolate results back into original locations - out <- lapply(block, list) - out[is_eval] <- tags - names(out)[is_eval] <- "" - - roxy_block_copy(block, compact(unlist(out, recursive = FALSE))) + block_replace_tags(block, "eval", tags) } block_find_object <- function(block, env) { stopifnot(is_roxy_block(block)) object <- object_from_call( - call = attr(block, "call"), + call = block$call, env = env, block = block, - file = attr(block, "filename") + file = block$file ) - attr(block, "object") <- object + block$object <- object # Add in defaults generated from the object defaults <- object_defaults(object) + defaults <- c(defaults, list(roxy_tag("backref", block$file, block$file))) - for (tag in names(defaults)) { - if (tag %in% names(block)) - next - - block[[tag]] <- defaults[[tag]] - } + default_tags <- map_chr(defaults, "tag") + defaults <- defaults[!default_tags %in% block_tags(block)] + block$tags <- c(block$tags, defaults) block } -block_location <- function(block) { - if (is.null(block)) { +# block accessors --------------------------------------------------------- + +block_tags <- function(block) { + map_chr(block$tags, "tag") +} + +#' @export +#' @rdname roxy_block +block_has_tags <- function(block, tags) { + any(block_tags(block) %in% tags) +} + +#' @export +#' @rdname roxy_block +block_get_tags <- function(block, tags) { + block$tags[block_tags(block) %in% tags] +} + +#' @export +#' @rdname roxy_block +block_get_tag <- function(block, tag) { + matches <- which(block_tags(block) %in% tag) + n <- length(matches) + if (n == 0) { NULL + } else if (n == 1) { + block$tags[[matches]] } else { - paste0(basename(attr(block, "filename")), ":", attr(block, "location")[[1]]) + roxy_tag_warning(block$tags[[matches[[2]]]], "May only use one @", tag, " per block") + block$tags[[matches[[1]]]] } } -block_warning <- function(block, ...) { - warning( - block_location(block), ": ", ..., - call. = FALSE, - immediate. = TRUE - ) - NULL +#' @export +#' @rdname roxy_block +block_get_tag_value <- function(block, tag) { + block_get_tag(block, tag)$val +} + +block_replace_tags <- function(block, tags, values) { + indx <- which(block_tags(block) %in% tags) + stopifnot(length(indx) == length(values)) + + tags <- lapply(block$tags, list) + tags[indx] <- values + + block$tags <- compact(unlist(tags, recursive = FALSE)) + block } +# parsing ----------------------------------------------------------------- + parse_tags <- function(tokens, registry = list(), global_options = list()) { markdown_activate(tokens, global_options = global_options) tokens <- parse_description(tokens) - tags <- compact(lapply(tokens, parse_tag, registry = registry)) - - # Convert to existing named list format - this isn't ideal, but - # it's what roxygen already uses - set_names(map(tags, "val"), map_chr(tags, "tag")) + compact(lapply(tokens, parse_tag, registry = registry)) } parse_tag <- function(x, registry) { @@ -191,7 +242,7 @@ parse_description <- function(tags) { } intro <- tags[[1]] - intro$val <- str_trim(intro$val) + intro$val <- str_trim(intro$raw) if (intro$val == "") { return(tags[-1]) } @@ -205,17 +256,17 @@ parse_description <- function(tags) { if ("title" %in% tag_names) { title <- NULL } else if (length(paragraphs) > 0) { - title <- roxy_tag("title", paragraphs[1], intro$file, intro$line) + title <- roxy_tag("title", paragraphs[1], NULL, intro$file, intro$line) paragraphs <- paragraphs[-1] } else { - title <- roxy_tag("title", "", intro$file, intro$line) + title <- roxy_tag("title", "", NULL, intro$file, intro$line) } # 2nd paragraph = description (unless has @description) if ("description" %in% tag_names || length(paragraphs) == 0) { description <- NULL } else if (length(paragraphs) > 0) { - description <- roxy_tag("description", paragraphs[1], intro$file, intro$line) + description <- roxy_tag("description", paragraphs[1], NULL, intro$file, intro$line) paragraphs <- paragraphs[-1] } @@ -226,12 +277,12 @@ parse_description <- function(tags) { # Find explicit @details tags didx <- which(tag_names == "details") if (length(didx) > 0) { - explicit_details <- map_chr(tags[didx], "val") + explicit_details <- map_chr(tags[didx], "raw") tags <- tags[-didx] details_para <- paste(c(details_para, explicit_details), collapse = "\n\n") } - details <- roxy_tag("details", details_para, intro$file, intro$line) + details <- roxy_tag("details", details_para, NULL, intro$file, intro$line) } else { details <- NULL } diff --git a/R/field.R b/R/field.R index 85de5db26..1b544a726 100644 --- a/R/field.R +++ b/R/field.R @@ -19,8 +19,6 @@ roxy_field <- function(field, ...) { ) } -is_roxy_field <- function(x) inherits(x, "roxy_field") - #' @export print.roxy_field <- function(x, ...) { cat(format(x, wrap = FALSE), "\n") diff --git a/R/markdown.R b/R/markdown.R index 9ac96063d..fd5d0fca3 100644 --- a/R/markdown.R +++ b/R/markdown.R @@ -1,11 +1,3 @@ -markdown_if_active <- function(text, tag, sections = FALSE) { - if (markdown_on()) { - markdown(text, tag, sections) - } else { - text - } -} - markdown <- function(text, tag = NULL, sections = FALSE) { esc_text <- escape_rd_for_md(text) esc_text_linkrefs <- add_linkrefs_to_md(esc_text) diff --git a/R/namespace.R b/R/namespace.R index b4efc3aaf..1b5258baf 100644 --- a/R/namespace.R +++ b/R/namespace.R @@ -56,11 +56,10 @@ roclet_preprocess.roclet_namespace <- function(x, base_path, global_options = list()) { - lines <- unlist(lapply(blocks, block_to_ns, tag_set = ns_tags_import)) %||% character() - lines <- sort_c(unique(lines)) - + lines <- blocks_to_ns(blocks, env, tag_set = ns_tags_import) NAMESPACE <- file.path(base_path, "NAMESPACE") - if (purrr::is_empty(lines) && !made_by_roxygen(NAMESPACE)) { + + if (length(lines) == 0 && !made_by_roxygen(NAMESPACE)) { return(x) } @@ -70,17 +69,13 @@ roclet_preprocess.roclet_namespace <- function(x, invisible(x) } - #' @export roclet_process.roclet_namespace <- function(x, blocks, env, base_path, global_options = list()) { - - ns <- unlist(lapply(blocks, block_to_ns, env = env)) %||% - character() - sort_c(unique(ns)) + blocks_to_ns(blocks, env) } #' @export @@ -101,22 +96,6 @@ roclet_tags.roclet_namespace <- function(x) { ) } -block_to_ns <- function(block, env, tag_set = ns_tags) { - tags <- intersect(names(block), tag_set) - lapply(tags, ns_process_tag, block = block, env = env) -} - -ns_process_tag <- function(tag_name, block, env) { - f <- if (tag_name == "evalNamespace") { - function(tag, block) ns_evalNamespace(tag, block, env) - } else { - get(paste0("ns_", tag_name), mode = "function") - } - tags <- block[names(block) == tag_name] - - lapply(tags, f, block = block) -} - #' @export roclet_output.roclet_namespace <- function(x, results, base_path, ...) { NAMESPACE <- file.path(base_path, "NAMESPACE") @@ -137,78 +116,78 @@ roclet_clean.roclet_namespace <- function(x, base_path) { } } -# Functions that take complete block and return NAMESPACE lines -ns_export <- function(tag, block) { - if (identical(tag, "")) { +# NAMESPACE generation ---------------------------------------------------- + +blocks_to_ns <- function(blocks, env, tag_set = ns_tags) { + lines <- map(blocks, block_to_ns, env = env, tag_set = tag_set) + lines <- unlist(lines) %||% character() + + sort_c(unique(lines)) +} + +block_to_ns <- function(block, env, tag_set = ns_tags) { + tags <- block_get_tags(block, tag_set) + + map(tags, function(tag) { + exec(paste0("ns_", tag$tag), tag, block, env) + }) +} + +ns_export <- function(tag, block, env) { + if (identical(tag$val, "")) { # FIXME: check for empty exports (i.e. no name) - default_export(attr(block, "object"), block) + default_export(block$object, block) } else { - export(tag) + export(tag$val) } } -default_export <- function(x, block) UseMethod("default_export") -#' @export -default_export.s4class <- function(x, block) export_class(x$value@className) -#' @export -default_export.s4generic <- function(x, block) export(x$value@generic) -#' @export -default_export.s4method <- function(x, block) export_s4_method(x$value@generic) -#' @export -default_export.s3method <- function(x, block) export_s3_method(attr(x$value, "s3method")) -#' @export -default_export.rcclass <- function(x, block) export_class(x$value@className) -#' @export -default_export.default <- function(x, block) export(x$alias) -#' @export -default_export.NULL <- function(x, block) export(block$name) -ns_exportClass <- function(tag, block) export_class(tag) -ns_exportMethod <- function(tag, block) export_s4_method(tag) -ns_exportPattern <- function(tag, block) one_per_line("exportPattern", tag) -ns_import <- function(tag, block) one_per_line("import", tag) -ns_importFrom <- function(tag, block) repeat_first("importFrom", tag) -ns_importClassesFrom <- function(tag, block) repeat_first("importClassesFrom", tag) -ns_importMethodsFrom <- function(tag, block) repeat_first("importMethodsFrom", tag) +ns_exportClass <- function(tag, block, env) export_class(tag$val) +ns_exportMethod <- function(tag, block, env) export_s4_method(tag$val) +ns_exportPattern <- function(tag, block, env) one_per_line("exportPattern", tag$val) +ns_import <- function(tag, block, env) one_per_line("import", tag$val) +ns_importFrom <- function(tag, block, env) repeat_first("importFrom", tag$val) +ns_importClassesFrom <- function(tag, block, env) repeat_first("importClassesFrom", tag$val) +ns_importMethodsFrom <- function(tag, block, env) repeat_first("importMethodsFrom", tag$val) -ns_exportS3Method <- function(tag, block) { - obj <- attr(block, "object") +ns_exportS3Method <- function(tag, block, env) { + obj <- block$object - if (length(tag) < 2 && !inherits(obj, "s3method")) { - block_warning(block, + if (length(tag$val) < 2 && !inherits(obj, "s3method")) { + roxy_tag_warning(tag, "`@exportS3Method` and `@exportS3Method generic` must be used with an S3 method" ) return() } - if (identical(tag, "")) { + if (identical(tag$val, "")) { method <- attr(obj$value, "s3method") - } else if (length(tag) == 1) { - method <- c(tag, attr(obj$value, "s3method")[[2]]) + } else if (length(tag$val) == 1) { + method <- c(tag$val, attr(obj$value, "s3method")[[2]]) } else { - method <- tag + method <- tag$val } export_s3_method(method) } - -ns_useDynLib <- function(tag, block) { - if (length(tag) == 1) { - return(paste0("useDynLib(", auto_quote(tag), ")")) +ns_useDynLib <- function(tag, block, env) { + if (length(tag$val) == 1) { + return(paste0("useDynLib(", auto_quote(tag$val), ")")) } - if (any(grepl(",", tag))) { + if (any(grepl(",", tag$val))) { # If there's a comma in list, don't quote output. This makes it possible # for roxygen2 to support other NAMESPACE forms not otherwise mapped - args <- paste0(tag, collapse = " ") + args <- paste0(tag$val, collapse = " ") paste0("useDynLib(", args, ")") } else { - repeat_first("useDynLib", tag) + repeat_first("useDynLib", tag$val) } } -ns_rawNamespace <- function(tag, block) tag +ns_rawNamespace <- function(tag, block, env) tag$val ns_evalNamespace <- function(tag, block, env) { - block_eval(tag, block, env, "@evalNamespace") + roxy_tag_eval(tag, env) } # Functions used by both default_export and ns_* functions @@ -220,6 +199,25 @@ export_s3_method <- function(x) { paste0("S3method(", args, ")") } +# Default export methods -------------------------------------------------- + +default_export <- function(x, block) UseMethod("default_export") +#' @export +default_export.s4class <- function(x, block) export_class(x$value@className) +#' @export +default_export.s4generic <- function(x, block) export(x$value@generic) +#' @export +default_export.s4method <- function(x, block) export_s4_method(x$value@generic) +#' @export +default_export.s3method <- function(x, block) export_s3_method(attr(x$value, "s3method")) +#' @export +default_export.rcclass <- function(x, block) export_class(x$value@className) +#' @export +default_export.default <- function(x, block) export(x$alias) +#' @export +default_export.NULL <- function(x, block) export(block_get_tag_value(block, "name")) + + # Helpers ----------------------------------------------------------------- one_per_line <- function(name, x) { diff --git a/R/object-defaults.R b/R/object-defaults.R index 856d88392..5df30bd6c 100644 --- a/R/object-defaults.R +++ b/R/object-defaults.R @@ -8,9 +8,9 @@ object_defaults.data <- function(x) { str_out <- rd(object_format(x$value)) list( - docType = "data", - format = str_out, - keywords = "datasets" + roxy_tag("docType", NULL, "data"), + roxy_tag("format", NULL, str_out), + roxy_tag("keywords", NULL, "datasets") ) } @@ -26,34 +26,45 @@ object_defaults.package <- function(x) { } list( - docType = "package", - name = package_suffix(desc$Package), + roxy_tag("docType", NULL, "package"), + roxy_tag("name", NULL, package_suffix(desc$Package)), # "NULL" prevents addition of default aliases, see also #202 - aliases = paste("NULL", desc$Package, package_suffix(desc$Package)), - title = paste0(desc$Package, ": ", desc$Title), - description = description, - seealso = package_seealso(desc), - author = package_authors(desc) + roxy_tag("aliases", NULL, paste("NULL", desc$Package, package_suffix(desc$Package))), + roxy_tag("title", NULL, paste0(desc$Package, ": ", desc$Title)), + roxy_tag("description", NULL, description), + roxy_tag("seealso", NULL, package_seealso(desc)), + roxy_tag("author", NULL, package_authors(desc)) + ) +} + +#' @export +object_defaults.import <- function(x) { + list( + roxy_tag("docType", NULL, "import"), + roxy_tag("name", NULL, "reexports"), + roxy_tag("keywords", NULL, "internal"), + roxy_tag("title", NULL, "Objects exported from other packages"), + roxy_tag(".reexport", NULL, roxy_field_reexport(x$value$pkg, x$value$fun)) ) } #' @export object_defaults.s4class <- function(x) { list( - docType = "class" + roxy_tag("docType", NULL, "class") ) } #' @export object_defaults.rcclass <- function(x) { list( - docType = "class" + roxy_tag("docType", NULL, "class") ) } #' @export object_defaults.s4method <- function(x) { list( - docType = "methods" + roxy_tag("docType", NULL, "class") ) } diff --git a/R/object-from-call.R b/R/object-from-call.R index 70f29d027..2bbcbeb60 100644 --- a/R/object-from-call.R +++ b/R/object-from-call.R @@ -55,7 +55,7 @@ object_from_name <- function(name, env, block) { type <- "s4generic" } else if (is.function(value)) { # Potential S3 methods/generics need metadata added - method <- unlist(block$method, use.names = FALSE) + method <- block_get_tag_value(block, "method") value <- add_s3_metadata(value, name, env, method) if (inherits(value, "s3generic")) { type <- "s3generic" @@ -166,7 +166,7 @@ parser_setMethodS3 <- function(call, env, block) { class <- as.character(call[[3]]) name <- paste(method, class, sep = ".") - method <- unlist(block$method, use.names = FALSE) + method <- block_get_tag_value(block, "method") value <- add_s3_metadata(get(name, env), name, env, method) object(value, name, "s3method") @@ -249,13 +249,19 @@ object <- function(value, alias, type) { } #' @export -print.object <- function(x, ...) { - cat("<", class(x)[1], "> ", x$name, - if (!is.null(x$alias)) paste0(" (", x$alias, ")"), "\n", - sep = "" +format.object <- function(x, ...) { + c( + paste0("<", class(x)[1], "> ", x$name), + paste0(" $topic ", x$topic), + if (!is.null(x$alias)) paste0(" $alias ", x$alias) ) } +#' @export +print.object <- function(x, ...) { + cat_line(format(x, ...)) +} + object_topic <- function(value, alias, type) { switch(type, s4method = paste0(value@generic, ",", paste0(value@defined, collapse = ","), "-method"), diff --git a/R/object-import.R b/R/object-import.R index 6030ece00..e3a857377 100644 --- a/R/object-import.R +++ b/R/object-import.R @@ -1,14 +1,3 @@ -#' @export -object_defaults.import <- function(x) { - list( - docType = "import", - name = "reexports", - keywords = "internal", - title = "Objects exported from other packages", - .reexport = roxy_field_reexport(x$value$pkg, x$value$fun) - ) -} - # Re-export ---------------------------------------------------------------- roxy_field_reexport <- function(pkg, fun) { diff --git a/R/parse.R b/R/parse.R index 5f3eeae8a..4d6babeb1 100644 --- a/R/parse.R +++ b/R/parse.R @@ -56,11 +56,13 @@ parse_package <- function(path = ".", parse_file <- function(file, env = env_file(file), registry = default_tags(), - global_options = list()) { + global_options = list(), + srcref_path = NULL) { blocks <- tokenize_file(file, registry = registry, - global_options = global_options + global_options = global_options, + srcref_path = srcref_path ) if (!is.null(env)) { @@ -90,7 +92,8 @@ parse_text <- function(text, file, env = env, registry = registry, - global_options = global_options + global_options = global_options, + srcref_path = "" ) blocks <- order_blocks(blocks) blocks @@ -116,14 +119,11 @@ env_package <- function(path) { order_blocks <- function(blocks) { block_order <- function(x) { - if (!"order" %in% names(x)) { - Inf - } else { - ord <- x[names(x) == "order"] - if (length(ord) > 1) { - ord <- ord[[length(ord)]] - } + if (block_has_tags(x, "order")) { + ord <- block_get_tag_value(x, "order") as.double(ord) + } else { + Inf } } diff --git a/R/rd-describe-in.R b/R/rd-describe-in.R index 771bceae5..d01b1bbcf 100644 --- a/R/rd-describe-in.R +++ b/R/rd-describe-in.R @@ -1,34 +1,31 @@ topic_add_describe_in <- function(topic, block, env) { - tags <- block_tags(block, "describeIn") - if (length(tags) == 0) - return(NULL) - - if (length(tags) > 1) { - block_warning(block, "May only use one @describeIn per block") + tag <- block_get_tag(block, "describeIn") + if (is.null(tag)) { return() } - if (is.null(attr(block, "object"))) { - block_warning(block, "@describeIn must be used with an object") + + if (is.null(block$object)) { + roxy_tag_warning(tag, "must be used with an object") return() } - if (any(names(block) == "name")) { - block_warning(block, "@describeIn can not be used with @name") + if (block_has_tags(block, "name")) { + roxy_tag_warning(tag, "can not be used with @name") return() } - if (any(names(block) == "rdname")) { - block_warning(block, "@describeIn can not be used with @rdname") + if (block_has_tags(block, "rdname")) { + roxy_tag_warning(tag, "can not be used with @rdname") return() } - dest <- find_object(tags$describeIn$name, env) - label <- build_label(attr(block, "object"), dest, block) + dest <- find_object(tag$val$name, env) + label <- build_label(block$object, dest, block) if (is.null(label)) return() topic$add(roxy_field_minidesc( label$type, label$label, - tags$describeIn$description + tag$val$description )) dest$topic } diff --git a/R/rd-examples.R b/R/rd-examples.R index 670e3999c..c25fe83b0 100644 --- a/R/rd-examples.R +++ b/R/rd-examples.R @@ -1,26 +1,27 @@ topic_add_examples <- function(topic, block, base_path) { - examples <- block_tags(block, c("examples", "example")) + tags <- block_get_tags(block, c("examples", "example")) - for (i in seq_along(examples)) { - if (names(examples)[[i]] == "examples") { - example <- examples[[i]] + for (tag in tags) { + if (tag$tag == "examples") { + example <- tag$val } else { - example <- read_example_from_path(str_trim(examples[[i]]), base_path, block = block) + example <- read_example_from_path(tag, base_path) } topic$add_simple_field("examples", example) } } -read_example_from_path <- function(path, base_path, block = NULL) { +read_example_from_path <- function(tag, base_path) { + path <- str_trim(tag$val) nl <- str_count(path, "\n") if (any(nl) > 0) { - block_warning(block, "@example spans multiple lines. Do you want @examples?") + roxy_tag_warning(tag, "spans multiple lines. Do you want @examples?") return() } path <- file.path(base_path, path) if (!file.exists(path)) { - block_warning(block, "@example ", path, " doesn't exist") + roxy_tag_warning(tag, "'", path, "' doesn't exist") return() } diff --git a/R/rd-template.R b/R/rd-template.R index 5902c5bac..cc77daf5b 100644 --- a/R/rd-template.R +++ b/R/rd-template.R @@ -18,28 +18,27 @@ template_eval <- function(template_path, vars) { } process_templates <- function(block, base_path, global_options = list()) { - template_locs <- names(block) == "template" - template_tags <- block[template_locs] - if (length(template_tags) == 0) + tags <- block_get_tags(block, "template") + if (length(tags) == 0) return(block) - templates <- unlist(template_tags, use.names = FALSE) + templates <- map_chr(tags, "val") paths <- map_chr(templates, template_find, base_path = base_path) - var_tags <- block[names(block) == "templateVar"] - vars <- set_names(map(var_tags, "description"), map_chr(var_tags, "name")) + var_tags <- block_get_tags(block, "templateVar") + vars <- set_names( + map(var_tags, c("val", "description")), + map_chr(var_tags, c("val", "name")) + ) vars <- lapply(vars, utils::type.convert, as.is = TRUE) results <- lapply(paths, template_eval, vars = list2env(vars)) tokens <- lapply(results, tokenise_block, file = "TEMPLATE", offset = 0L) - - # Insert templates back in the location where they came from - tags <- lapply(block, list) - tags[template_locs] <- lapply(tokens, parse_tags, + tags <- lapply(tokens, parse_tags, registry = roclet_tags.roclet_rd(list()), global_options = global_options ) - names(tags)[template_locs] <- "" - roxy_block_copy(block, unlist(tags, recursive = FALSE)) + # Insert templates back in the location where they came from + block_replace_tags(block, "template", tags) } diff --git a/R/rd.R b/R/rd.R index b8c0f311e..ad05888a9 100644 --- a/R/rd.R +++ b/R/rd.R @@ -126,9 +126,9 @@ block_to_rd <- function(block, base_path, env, global_options = list()) { return() } - name <- block$name %||% attr(block, "object")$topic + name <- block_get_tag(block, "name")$val %||% block$object$topic if (is.null(name)) { - block_warning(block, "Missing name") + roxy_tag_warning(block$tags[[1]], "Missing name") return() } @@ -138,8 +138,10 @@ block_to_rd <- function(block, base_path, env, global_options = list()) { topic_add_name_aliases(rd, block, name) # Some fields added directly by roxygen internals - fields <- Filter(is_roxy_field, block) - rd$add(fields) + tags <- Filter(roxy_tag_is_field, block$tags) + for (tag in tags) { + rd$add(tag$val) + } topic_add_backref(rd, block) topic_add_doc_type(rd, block) @@ -158,13 +160,12 @@ block_to_rd <- function(block, base_path, env, global_options = list()) { topic_add_value(rd, block) if (rd$has_field("description") && rd$has_field("reexport")) { - block_warning(block, "Can't use description when re-exporting") + roxy_tag_warning(block$tags[[1]], "Can't use description when re-exporting") return() } describe_rdname <- topic_add_describe_in(rd, block, env) - - filename <- describe_rdname %||% block$rdname %||% nice_name(name) + filename <- describe_rdname %||% block_get_tag(block, "rdname")$val %||% nice_name(name) rd$filename <- paste0(filename, ".Rd") rd @@ -204,58 +205,51 @@ roclet_clean.roclet_rd <- function(x, base_path) { unlink(purrr::keep(rd, made_by_roxygen)) } -block_tags <- function(x, tag) { - x[names(x) %in% tag] -} - +# Does this block get an Rd file? needs_doc <- function(block) { - # Does this block get an Rd file? - if (any(names(block) == "noRd")) { + if (block_has_tags(block, "noRd")) { return(FALSE) } - key_tags <- c("description", "param", "return", "title", "example", + block_has_tags(block, c( + "description", "param", "return", "title", "example", "examples", "name", "rdname", "usage", "details", "introduction", "inherit", "describeIn") - - any(names(block) %in% key_tags) + ) } # Tag processing functions ------------------------------------------------ topic_add_backref <- function(topic, block) { - backrefs <- block_tags(block, "backref") %||% attr(block, "filename") - - for (backref in backrefs) { - topic$add_simple_field("backref", backref) + tags <- block_get_tags(block, "backref") + for (tag in tags) { + topic$add_simple_field("backref", tag$val) } } # Simple tags can be converted directly to fields topic_add_simple_tags <- function(topic, block) { - simple_tags <- c( - "author", "concept", "description", "details", "encoding", "family", - "format", "note", "rawRd", "references", - "seealso", "source", "title" + simple_tags <- block_get_tags(block, + c( + "author", "concept", "description", "details", "encoding", "family", + "format", "note", "rawRd", "references", + "seealso", "source", "title" + ) ) - is_simple <- names(block) %in% simple_tags - tag_values <- block[is_simple] - tag_names <- names(block)[is_simple] - - for (i in seq_along(tag_values)) { - if (length(tag_values[[i]]) && nchar(tag_values[[i]][[1]])) { - topic$add_simple_field(tag_names[[i]], tag_values[[i]][[1]]) + for (tag in simple_tags) { + if (length(tag$val) && nchar(tag$val[[1]])) { + topic$add_simple_field(tag$tag, tag$val[[1]]) } - for (sec in tag_values[[i]][-1]) { - topic$add_simple_field("rawRd", sec) + for (extra in tag$val[-1]) { + topic$add_simple_field("rawRd", extra) } } } topic_add_params <- function(topic, block) { # Used in process_inherit_params() - value <- attr(block, "object")$value + value <- block$object$value if (is.function(value)) { formals <- formals(value) topic$add_simple_field("formals", names(formals)) @@ -265,19 +259,20 @@ topic_add_params <- function(topic, block) { } topic_add_name_aliases <- function(topic, block, name) { - tags <- block_tags(block, "aliases") + tags <- block_get_tags(block, "aliases") if (length(tags) == 0) { aliases <- character() } else { - aliases <- str_split(str_trim(unlist(tags, use.names = FALSE)), "\\s+")[[1]] + vals <- map_chr(tags, "val") + aliases <- unlist(str_split(vals, "\\s+")) } if (any(aliases == "NULL")) { # Don't add default aliases - aliases <- aliases[aliases != "NULL"] + aliases <- setdiff(aliases, "NULL") } else { - aliases <- c(name, attr(block, "object")$alias, aliases) + aliases <- c(name, block$object$alias, aliases) } aliases <- unique(aliases) @@ -287,7 +282,7 @@ topic_add_name_aliases <- function(topic, block, name) { topic_add_methods <- function(topic, block) { - obj <- attr(block, "object") + obj <- block$object if (!inherits(obj, "rcclass")) return() methods <- obj$methods @@ -306,57 +301,61 @@ topic_add_methods <- function(topic, block) { } topic_add_inherit <- function(topic, block) { - tags <- block_tags(block, "inherit") + tags <- block_get_tags(block, "inherit") for (tag in tags) { - field <- roxy_field_inherit(tag$source, list(tag$fields)) + field <- roxy_field_inherit(tag$val$source, list(tag$val$fields)) topic$add_field(field) } - tags <- block_tags(block, "inheritParams") + tags <- block_get_tags(block, "inheritParams") for (tag in tags) { - field <- roxy_field_inherit(tag, list("params")) + field <- roxy_field_inherit(tag$val, list("params")) topic$add_field(field) } - tags <- block_tags(block, "inheritSection") + tags <- block_get_tags(block, "inheritSection") for (tag in tags) { - field <- roxy_field_inherit_section(tag$name, tag$description) + field <- roxy_field_inherit_section(tag$val$name, tag$val$description) topic$add_field(field) } - tags <- block_tags(block, "inheritDotParams") + tags <- block_get_tags(block, "inheritDotParams") for (tag in tags) { - field <- roxy_field_inherit_dot_params(tag$source, tag$args) + field <- roxy_field_inherit_dot_params(tag$val$source, tag$val$args) topic$add_field(field) } } topic_add_value <- function(topic, block) { - tags <- block_tags(block, "return") + tags <- block_get_tags(block, "return") for (tag in tags) { - topic$add_simple_field("value", tag) + topic$add_simple_field("value", tag$val) } } topic_add_keyword <- function(topic, block) { - tags <- block_tags(block, "keywords") - keywords <- unlist(str_split(str_trim(tags), "\\s+")) + tags <- block_get_tags(block, "keywords") + + vals <- map_chr(tags, "val") + keywords <- unlist(str_split(vals, "\\s+")) topic$add_simple_field("keyword", keywords) } # Prefer explicit \code{@@usage} to a \code{@@formals} list. topic_add_usage <- function(topic, block, old_usage = FALSE) { - if (is.null(block$usage)) { - usage <- object_usage(attr(block, "object"), old_usage = old_usage) - } else if (block$usage == "NULL") { + tag <- block_get_tag(block, "usage") + + if (is.null(tag)) { + usage <- object_usage(block$object, old_usage = old_usage) + } else if (tag$val == "NULL") { usage <- NULL } else { # Treat user input as already escaped, otherwise they have no way # to enter \S4method etc. - usage <- rd(block$usage) + usage <- rd(tag$val) } topic$add_simple_field("usage", usage) } @@ -370,26 +369,18 @@ topic_add_fields <- function(topic, block) { } topic_add_eval_rd <- function(topic, block, env) { - tags <- block_tags(block, "evalRd") + tags <- block_get_tags(block, "evalRd") for (tag in tags) { - out <- block_eval(tag, block, env, "@evalRd") - if (!is.null(out)) { - topic$add_simple_field("rawRd", out) - } + out <- roxy_tag_eval(tag, env) + topic$add_simple_field("rawRd", out) } } topic_add_include_rmd <- function(topic, block, base_path) { - rmds <- block_tags(block, "includeRmd") - - for (rmd in rmds) { - tag <- roxy_tag( - "includeRmd", - rmd, - attr(block, "filename"), - attr(block, "location")[[1]] - ) + tags <- block_get_tags(block, "includeRmd") + + for (tag in tags) { if (!is_installed("rmarkdown")) { roxy_tag_warning(tag, "Needs the rmarkdown package") } @@ -404,17 +395,17 @@ topic_add_include_rmd <- function(topic, block, base_path) { } topic_add_sections <- function(topic, block) { - sections <- block_tags(block, "section") + tags <- block_get_tags(block, "section") - for (section in sections) { - pieces <- str_split(section, ":", n = 2)[[1]] + for (tag in tags) { + pieces <- str_split(tag$val, ":", n = 2)[[1]] title <- str_split(pieces[1], "\n")[[1]] if (length(title) > 1) { - return(block_warning( - block, + roxy_tag_warning(tag, "Section title spans multiple lines: \n", "@section ", title[1] - )) + ) + return() } topic$add_field(roxy_field_section(pieces[1], pieces[2])) @@ -422,40 +413,35 @@ topic_add_sections <- function(topic, block) { } topic_add_doc_type <- function(topic, block) { - doctype <- block$docType - if (is.null(doctype)) return() + tag <- block_get_tag(block, "docType") + if (is.null(tag)) { + return() + } - topic$add_simple_field("docType", doctype) + topic$add_simple_field("docType", tag$val) - if (doctype == "package") { - name <- block$name - if (!str_detect(name, "-package")) { - topic$add_simple_field("alias", package_suffix(name)) + if (tag$val == "package") { + name <- block_get_tag(block, "name") + if (!str_detect(name$val, "-package")) { + topic$add_simple_field("alias", package_suffix(name$val)) } } - } package_suffix <- function(name) { paste0(name, "-package") } -process_tag <- function(block, tag, f = roxy_field, ...) { - matches <- block[names(block) == tag] - if (length(matches) == 0) return() - - lapply(matches, function(p) f(tag, p, ...)) -} - # Name + description tags ------------------------------------------------------ - process_def_tag <- function(topic, block, tag) { - tags <- block[names(block) == tag] - if (length(tags) == 0) return() + tags <- block_get_tags(block, tag) + if (length(tags) == 0) { + return() + } - desc <- str_trim(sapply(tags, "[[", "description")) - names(desc) <- sapply(tags, "[[", "name") + desc <- str_trim(map_chr(tags, c("val", "description"))) + names(desc) <- map_chr(tags, c("val", "name")) topic$add_simple_field(tag, desc) } diff --git a/R/roclet.R b/R/roclet.R index ec84065a4..68e7586e3 100644 --- a/R/roclet.R +++ b/R/roclet.R @@ -6,7 +6,7 @@ #' @section Methods: #' #' * `roclet_tags()`: return named list, where names give recognised tags and -#' values give tag parsing function. See [roxy_tag] for built-in options. +#' values give tag parsing function. See [roxy_tag] for built-in parsers. #' #' * `roclet_preprocess()` is called after blocks have been parsed but before #' code has been evaluated. This should only be needed if your roclet affects @@ -22,6 +22,12 @@ #' * `roclet_clean()` called when `roxygenise(clean = TRUE)`. Should remove #' any files created by the roclet. #' +#' @param x A `roclet` object. +#' @param blocks A list of [roxy_block] objects. +#' @param results Value returned from your `roclet_process()` method. +#' @param base_path Path to root of source package. +#' @param global_options List of roxygen2 options. +#' @param env Package environment. #' @keywords internal #' @name roclet NULL @@ -32,19 +38,12 @@ roclet <- function(subclass, ...) { structure(list(...), class = c(paste0("roclet_", subclass), "roclet")) } -#' @export -#' @rdname roclet -roclet_output <- function(x, results, base_path, ...) { - UseMethod("roclet_output", x) -} - #' @export #' @rdname roclet roclet_tags <- function(x) { UseMethod("roclet_tags") } - #' @export #' @rdname roclet roclet_preprocess <- function(x, blocks, base_path, global_options = list()) { @@ -62,6 +61,12 @@ roclet_process <- function(x, blocks, env, base_path, global_options = list()) { UseMethod("roclet_process") } +#' @export +#' @rdname roclet +roclet_output <- function(x, results, base_path, ...) { + UseMethod("roclet_output", x) +} + #' @export #' @rdname roclet roclet_clean <- function(x, base_path) { @@ -120,10 +125,16 @@ is.roclet <- function(x) inherits(x, "roclet") #' @param global_options List of global options #' @export #' @keywords internal -roc_proc_text <- function(roclet, input, registry = default_tags(), +roc_proc_text <- function(roclet, + input, + registry = NULL, global_options = list()) { stopifnot(is.roclet(roclet)) + if (is.null(registry)) { + registry <- c(default_tags(), roclet_tags(roclet)) + } + file <- tempfile() write_lines(input, file) on.exit(unlink(file)) diff --git a/R/tag-parser.R b/R/tag-parser.R new file mode 100644 index 000000000..34c4c0784 --- /dev/null +++ b/R/tag-parser.R @@ -0,0 +1,233 @@ +#' Parse tags +#' +#' These functions parse the `raw` tag value, convert a string into a richer R +#' object and storing it in `val`, or provide an informative warning and +#' returning `NULL`. Two exceptions to the rule are `tag_words()` and +#' `tag_two_part()`, which are tag parsing generator functions. +#' +#' @param x A [roxy_tag] object to parss +#' @return A [roxy_tag] object with the `val` field set to the parsed value. +#' @name tag_parsers +#' @keywords internal +NULL + +#' @export +#' @rdname tag_parsers +tag_value <- function(x) { + if (x$raw == "") { + roxy_tag_warning(x, "requires a value") + } else if (!rdComplete(x$raw)) { + roxy_tag_warning(x, "mismatched braces or quotes") + } else { + x$val <- str_trim(x$raw) + x + } +} + +#' @export +#' @rdname tag_parsers +tag_inherit <- function(x) { + if (x$raw == "") { + roxy_tag_warning(x, "requires a value") + } else if (!rdComplete(x$raw)) { + roxy_tag_warning(x, "mismatched braces or quotes") + } else { + pieces <- str_split(str_trim(x$raw), "\\s+")[[1]] + fields <- pieces[-1] + + # Also recorded in `rd.Rmd` + all <- c("params", "return", "title", "description", "details", "seealso", + "sections", "references", "examples", "author", "source") + if (length(fields) == 0) { + fields <- all + } else { + unknown <- setdiff(fields, all) + if (length(unknown) > 0) { + types <- paste0(unknown, collapse = ", ") + roxy_tag_warning(x, "Unknown inherit type: ", types) + fields <- intersect(fields, all) + } + } + + x$val <- list( + source = pieces[1], + fields = fields + ) + + x + } +} + +#' @export +#' @rdname tag_parsers +tag_name <- function(x) { + if (x$raw == "") { + roxy_tag_warning("requires a name") + } else if (!rdComplete(x$raw)) { + roxy_tag_warning("mismatched braces or quotes") + } else if (str_count(x$raw, "\\s+") > 1) { + roxy_tag_warning("should have only a single argument") + } else { + x$val <- str_trim(x$raw) + x + } +} + +#' @export +#' @rdname tag_parsers +#' @param first,second Name of first and second parts of two part tags +#' @param required Is the second part required (TRUE) or can it be blank +#' (FALSE)? +#' @param markdown Should the second part be parsed as markdown? +tag_two_part <- function(first, second, required = TRUE, markdown = TRUE) { + force(required) + force(markdown) + + function(x) { + if (str_trim(x$raw) == "") { + roxy_tag_warning(x, "requires a value") + } else if (required && !str_detect(x$raw, "[[:space:]]+")) { + roxy_tag_warning(x, "requires ", first, " and ", second) + } else if (!rdComplete(x$raw)) { + roxy_tag_warning(x, "mismatched braces or quotes") + } else { + pieces <- str_split_fixed(str_trim(x$raw), "[[:space:]]+", 2) + + if (markdown) { + pieces[,2] <- markdown_if_active(pieces[,2], x) + } + + x$val <- list( + pieces[, 1], + trim_docstring(pieces[,2]) + ) + names(x$val) <- c(first, second) + x + } + } +} + +#' @export +#' @rdname tag_parsers +tag_name_description <- tag_two_part("name", "description") + +#' @export +#' @rdname tag_parsers +#' @param min,max Minimum and maximum number of words +tag_words <- function(min = 0, max = Inf) { + function(x) { + if (!rdComplete(x$raw)) { + return(roxy_tag_warning(x, "mismatched braces or quotes")) + } + + words <- str_split(str_trim(x$raw), "\\s+")[[1]] + if (length(words) < min) { + roxy_tag_warning(x, " needs at least ", min, " words") + } else if (length(words) > max) { + roxy_tag_warning(x, " can have at most ", max, " words") + } + + x$val <- words + x + } +} + +#' @export +#' @rdname tag_parsers +tag_words_line <- function(x) { + x$val <- str_trim(x$raw) + + if (str_detect(x$val, "\n")) { + roxy_tag_warning(x, "may only span a single line") + } else if (!rdComplete(x$val)) { + roxy_tag_warning(x, "mismatched braces or quotes") + } else { + x$val <- str_split(x$val, "\\s+")[[1]] + x + } +} + +#' @export +#' @rdname tag_parsers +tag_toggle <- function(x) { + x$val <- str_trim(x$raw) + + if (x$val != "") { + roxy_tag_warning(x, "has no parameters") + } else { + x + } +} + +#' @export +#' @rdname tag_parsers +tag_code <- function(x) { + if (x$raw == "") { + roxy_tag_warning(x, "requires a value") + } else { + tryCatch({ + parse(text = x$raw) + }, error = function(e) { + roxy_tag_warning(x, "code failed to parse.\n", e$message) + }) + + x$val <- x$raw + x + } +} + +# Examples need special parsing because escaping rules are different +#' @export +#' @rdname tag_parsers +tag_examples <- function(x) { + if (x$raw == "") { + return(roxy_tag_warning(x, "requires a value")) + } + + x$val <- escape_examples(gsub("^\n", "", x$raw)) + if (!rdComplete(x$val, TRUE)) { + roxy_tag_warning(x, "mismatched braces or quotes") + } else { + x + } +} + +#' @export +#' @rdname tag_parsers +tag_markdown <- function(x) { + x$val <- markdown_if_active(x$raw, x) + x +} + +#' @export +#' @rdname tag_parsers +tag_markdown_with_sections <- function(x) { + if (x$raw == "") { + return(roxy_tag_warning(x, "requires a value")) + } + + x$val <- markdown_if_active(x$raw, x, sections = TRUE) + for (i in seq_along(x$val)) { + if (!rdComplete(x$val[i])) { + roxy_tag_warning(x, "mismatched braces or quotes") + x$val[i] <- "" + } else { + x$val[i] <- str_trim(x$val[i]) + } + } + + x +} + +markdown_if_active <- function(text, tag, sections = FALSE) { + if (markdown_on()) { + markdown(text, tag, sections) + } else { + if (!rdComplete(text)) { + roxy_tag_warning(tag, "mismatched braces or quotes") + "" + } else { + str_trim(text) + } + } +} diff --git a/R/tag.R b/R/tag.R index 72622df2d..a3f814e15 100644 --- a/R/tag.R +++ b/R/tag.R @@ -1,26 +1,21 @@ -#' Parsing tags. +#' `roxy_tag` S3 constructor #' -#' `roxy_tag` constructs a tag object, and `roxy_tag_warning` makes -#' an informative warning using the location information stored in the tag. -#' The remainder of the tag functions parse the tag value, convert a string -#' into a richer R object, or providing informative warnings and returning -#' valid if the value is invalid. -#' -#' Two exceptions to the rule are `tag_words` and `tag_two_part`, which are -#' tag parsing generator functions. +#' `roxy_tag()` is the constructor for tag objects. +#' `roxy_tag_warning()` generates a warning that gives the location of the tag. #' #' @keywords internal #' @export #' @param tag Tag name -#' @param val Tag value. When read from the file, this will be a string, -#' but after parsing can be a more complicated structure (typically -#' a character vector, but sometimes a list). +#' @param raw Raw tag value, a string. +#' @param val Parsed tag value, typically a character vector, but sometimes +#' a list. Usually filled in by `tag_parsers` #' @param file,line Location of the tag -roxy_tag <- function(tag, val, file = "", line = 0) { +roxy_tag <- function(tag, raw, val = NULL, file = NA_character_, line = NA_integer_) { structure( list( file = file, line = line, + raw = raw, tag = tag, val = val ), @@ -31,231 +26,69 @@ roxy_tag <- function(tag, val, file = "", line = 0) { is.roxy_tag <- function(x) inherits(x, "roxy_tag") #' @export -print.roxy_tag <- function(x, ...) { - cat("[", x$file, ":", x$line, "] @", x$tag, " ", encodeString(x$val), "\n", - sep = "") -} - -make_tag_message <- function(x, message) { - paste0( - "@", - x$tag, - if (x$file != "") paste0(" [", x$file, "#", x$line, "]"), - ": ", - message - ) -} - -#' @export -#' @rdname roxy_tag -roxy_tag_warning <- function(x, ...) { - warning(make_tag_message(x, paste0(...)), call. = FALSE, immediate. = TRUE) - NULL -} - - -#' @export -#' @rdname roxy_tag -tag_value <- function(x) { - if (x$val == "") { - roxy_tag_warning(x, "requires a value") - } else if (!rdComplete(x$val)) { - roxy_tag_warning(x, "mismatched braces or quotes") +format.roxy_tag <- function(x, ..., file = NULL) { + if (identical(x$file, file)) { + file <- "line" + } else if (is.na(x$file)) { + file <- "????" } else { - x$val <- str_trim(x$val) - x + file <- basename(x$file) } -} + line <- if (is.na(x$line)) "???" else format(x$line, width = 3) -#' @export -#' @rdname roxy_tag -tag_inherit <- function(x) { - if (x$val == "") { - roxy_tag_warning(x, "requires a value") - } else if (!rdComplete(x$val)) { - roxy_tag_warning(x, "mismatched braces or quotes") - } else { - pieces <- str_split(str_trim(x$val), "\\s+")[[1]] - fields <- pieces[-1] + loc <- paste0("[", file, ":", line, "]") - # Also recorded in `rd.Rmd` - all <- c("params", "return", "title", "description", "details", "seealso", - "sections", "references", "examples", "author", "source") - if (length(fields) == 0) { - fields <- all + if (!is.null(x$raw)) { + if (nchar(x$raw) > 50) { + raw <- paste0(substr(x$raw, 1, 47), "...") } else { - unknown <- setdiff(fields, all) - if (length(unknown) > 0) { - types <- paste0(unknown, collapse = ", ") - roxy_tag_warning(x, "Unknown inherit type: ", types) - fields <- intersect(fields, all) - } - } - - x$val <- list( - source = pieces[1], - fields = fields - ) - - x - } -} - -#' @export -#' @rdname roxy_tag -tag_name <- function(x) { - if (x$val == "") { - roxy_tag_warning("requires a name") - } else if (!rdComplete(x$val)) { - roxy_tag_warning("mismatched braces or quotes") - } else if (str_count(x$val, "\\s+") > 1) { - roxy_tag_warning("should have only a single argument") - } else { - x$val <- str_trim(x$val) - x - } -} - -#' @export -#' @rdname roxy_tag -#' @param first,second Name of first and second parts of two part tags -#' @param required Is the second part required (TRUE) or can it be blank -#' (FALSE)? -#' @param markdown Should the second part be parsed as markdown? -tag_two_part <- function(first, second, required = TRUE, markdown = TRUE) { - force(required) - force(markdown) - - function(x) { - if (str_trim(x$val) == "") { - roxy_tag_warning(x, "requires a value") - } else if (required && !str_detect(x$val, "[[:space:]]+")) { - roxy_tag_warning(x, "requires ", first, " and ", second) - } else if (!rdComplete(x$val)) { - roxy_tag_warning(x, "mismatched braces or quotes") - } else { - pieces <- str_split_fixed(str_trim(x$val), "[[:space:]]+", 2) - - if (markdown) { - pieces[,2] <- markdown_if_active(pieces[,2], x) - } - - x$val <- list( - pieces[, 1], - trim_docstring(pieces[,2]) - ) - names(x$val) <- c(first, second) - x - } - } -} - -#' @export -#' @rdname roxy_tag -tag_name_description <- tag_two_part("name", "description") - -#' @export -#' @rdname roxy_tag -#' @param min,max Minimum and maximum number of words -tag_words <- function(min = 0, max = Inf) { - function(x) { - if (!rdComplete(x$val)) { - return(roxy_tag_warning(x, "mismatched braces or quotes")) - } - - words <- str_split(str_trim(x$val), "\\s+")[[1]] - if (length(words) < min) { - roxy_tag_warning(x, " needs at least ", min, " words") - } else if (length(words) > max) { - roxy_tag_warning(x, " can have at most ", max, " words") + raw <- x$raw } - - x$val <- words - x - } -} - -#' @export -#' @rdname roxy_tag -tag_words_line <- function(x) { - x$val <- str_trim(x$val) - - if (str_detect(x$val, "\n")) { - roxy_tag_warning(x, "may only span a single line") - } else if (!rdComplete(x$val)) { - roxy_tag_warning(x, "mismatched braces or quotes") } else { - x$val <- str_split(x$val, "\\s+")[[1]] - x + raw <- "" } -} -#' @export -#' @rdname roxy_tag -tag_toggle <- function(x) { - x$val <- str_trim(x$val) + parsed <- if (is.null(x$val)) "{unparsed}" else "{parsed}" - if (x$val != "") { - roxy_tag_warning(x, "has no parameters") - } else { - x - } + paste0(loc, " @", x$tag, " '", raw, "' ", parsed) } #' @export -#' @rdname roxy_tag -tag_code <- function(x) { - if (x$val == "") { - roxy_tag_warning(x, "requires a value") - } else { - tryCatch({ - parse(text = x$val) - x - }, error = function(e) { - roxy_tag_warning(x, "code failed to parse.\n", e$message) - }) - } +print.roxy_tag <- function(x, ...) { + cat_line(format(x, ...)) } -# Examples need special parsing because escaping rules are different #' @export #' @rdname roxy_tag -tag_examples <- function(x) { - if (x$val == "") { - return(roxy_tag_warning(x, "requires a value")) - } +roxy_tag_warning <- function(x, ...) { + message <- paste0( + if (!is.na(x$file)) paste0("[", x$file, ":", x$line, "] "), + "@", x$tag, " ", + ..., + collapse = " " + ) - x$val <- escape_examples(gsub("^\n", "", x$val)) - if (!rdComplete(x$val, TRUE)) { - roxy_tag_warning(x, "mismatched braces or quotes") - } else { - x - } + warning(message, call. = FALSE, immediate. = TRUE) + NULL } -#' @export -#' @rdname roxy_tag -tag_markdown <- function(x) { - x$val <- markdown_if_active(x$val, x) - tag_value(x) +roxy_tag_is_field <- function(tag) { + inherits(tag$val, "roxy_field") } -#' @export -#' @rdname roxy_tag -tag_markdown_with_sections <- function(x) { - if (x$val == "") { - return(roxy_tag_warning(x, "requires a value")) - } +roxy_tag_eval <- function(tag, env) { + tryCatch({ + expr <- parse(text = tag$val) + out <- eval(expr, envir = env) - x$val <- markdown_if_active(x$val, x, sections = TRUE) - for (i in seq_along(x$val)) { - if (!rdComplete(x$val[i])) { - roxy_tag_warning(x, "mismatched braces or quotes") - x$val[i] <- "" + if (!is.character(out)) { + roxy_tag_warning(tag, "did not evaluate to a string") + } else if (anyNA(out)) { + roxy_tag_warning(tag, "result contained NA") } else { - x$val[i] <- str_trim(x$val[i]) + out } - } - - x + }, error = function(e) { + roxy_tag_warning(tag, "failed with error:\n", e$message) + }) } diff --git a/R/tokenize.R b/R/tokenize.R index 278c72ae6..90074c5a1 100644 --- a/R/tokenize.R +++ b/R/tokenize.R @@ -1,14 +1,15 @@ # Returns list of roxy_blocks tokenize_file <- function(file, registry = list(), - global_options = list() + global_options = list(), + srcref_path = NULL ) { lines <- read_lines(file) parsed <- parse( text = lines, keep.source = TRUE, - srcfile = srcfilecopy(file, lines, isFile = TRUE) + srcfile = srcfilecopy(srcref_path %||% file, lines, isFile = TRUE) ) if (length(parsed) == 0) return(list()) diff --git a/R/topic.R b/R/topic.R index 095044c4b..f83430c76 100644 --- a/R/topic.R +++ b/R/topic.R @@ -66,7 +66,6 @@ RoxyTopic <- R6::R6Class("RoxyTopic", public = list( if (is.null(field)) return() - stopifnot(is_roxy_field(field)) field_name <- field$field if (self$has_field(field_name) && !overwrite) { field <- merge(self$get_field(field_name), field) diff --git a/R/utils.R b/R/utils.R index 2736b3dd9..70a5c4e7d 100644 --- a/R/utils.R +++ b/R/utils.R @@ -101,24 +101,6 @@ compact <- function(x) { x[!map_lgl(x, is.null)] } -block_eval <- function(tag, block, env, tag_name) { - tryCatch({ - expr <- parse(text = tag) - out <- eval(expr, envir = env) - - if (!is.character(out)) { - block_warning(block, tag_name, " did not evaluate to a string") - } else if (anyNA(out)) { - block_warning(block, tag_name, " result contained NA") - } else { - out - } - }, error = function(e) { - block_warning(block, tag_name, " failed with error:\n", e$message) - }) -} - - # Parse DESCRIPTION into convenient format read.description <- function(file) { dcf <- desc::desc(file = file) @@ -127,7 +109,6 @@ read.description <- function(file) { purrr::map(purrr::set_names(fields), ~ dcf$get_field(.x)) } - invert <- function(x) { if (length(x) == 0) return() stacked <- utils::stack(x) @@ -154,7 +135,7 @@ collapse <- function(key, value, fun, ...) { } cat_line <- function(...) { - cat(..., "\n", sep = "") + cat(paste0(..., "\n", collapse = "")) } tag_aliases <- function(f) { diff --git a/man/parse_package.Rd b/man/parse_package.Rd index 3992b70f1..ec13834db 100644 --- a/man/parse_package.Rd +++ b/man/parse_package.Rd @@ -19,7 +19,8 @@ parse_file( file, env = env_file(file), registry = default_tags(), - global_options = list() + global_options = list(), + srcref_path = NULL ) parse_text( diff --git a/man/roc_proc_text.Rd b/man/roc_proc_text.Rd index bca63d8f9..9dde64deb 100644 --- a/man/roc_proc_text.Rd +++ b/man/roc_proc_text.Rd @@ -4,12 +4,7 @@ \alias{roc_proc_text} \title{Process roclet on string and capture results.} \usage{ -roc_proc_text( - roclet, - input, - registry = default_tags(), - global_options = list() -) +roc_proc_text(roclet, input, registry = NULL, global_options = list()) } \arguments{ \item{roclet}{Name of roclet to use for processing.} diff --git a/man/roclet.Rd b/man/roclet.Rd index 5106da2e7..1c70a0364 100644 --- a/man/roclet.Rd +++ b/man/roclet.Rd @@ -2,25 +2,38 @@ % Please edit documentation in R/roclet.R \name{roclet} \alias{roclet} -\alias{roclet_output} \alias{roclet_tags} \alias{roclet_preprocess} \alias{roclet_process} +\alias{roclet_output} \alias{roclet_clean} \title{Build a new roclet.} \usage{ roclet(subclass, ...) -roclet_output(x, results, base_path, ...) - roclet_tags(x) roclet_preprocess(x, blocks, base_path, global_options = list()) roclet_process(x, blocks, env, base_path, global_options = list()) +roclet_output(x, results, base_path, ...) + roclet_clean(x, base_path) } +\arguments{ +\item{x}{A \code{roclet} object.} + +\item{blocks}{A list of \link{roxy_block} objects.} + +\item{base_path}{Path to root of source package.} + +\item{global_options}{List of roxygen2 options.} + +\item{env}{Package environment.} + +\item{results}{Value returned from your \code{roclet_process()} method.} +} \description{ To create a new roclet, you will need to create a constructor function that wraps \code{roclet}, and then implement the methods described below. @@ -29,7 +42,7 @@ that wraps \code{roclet}, and then implement the methods described below. \itemize{ \item \code{roclet_tags()}: return named list, where names give recognised tags and -values give tag parsing function. See \link{roxy_tag} for built-in options. +values give tag parsing function. See \link{roxy_tag} for built-in parsers. \item \code{roclet_preprocess()} is called after blocks have been parsed but before code has been evaluated. This should only be needed if your roclet affects how code will evaluated. Should return a roclet. diff --git a/man/roxy_block.Rd b/man/roxy_block.Rd new file mode 100644 index 000000000..96ded7572 --- /dev/null +++ b/man/roxy_block.Rd @@ -0,0 +1,63 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/block.R +\name{roxy_block} +\alias{roxy_block} +\alias{block_has_tags} +\alias{block_get_tags} +\alias{block_get_tag} +\alias{block_get_tag_value} +\title{Blocks} +\usage{ +roxy_block(tags, file, line, call, object = NULL) + +block_has_tags(block, tags) + +block_get_tags(block, tags) + +block_get_tag(block, tag) + +block_get_tag_value(block, tag) +} +\arguments{ +\item{tags}{A list of \link{roxy_tag}s.} + +\item{file, line}{Location of the \code{call} (i.e. the line after the last +line of the block).} + +\item{call}{Expression associated with block.} + +\item{object}{Optionally, the object associated with the block, found +by inspecting/evaluating \code{call}.} + +\item{block}{A \code{roxy_block} to manipulate.} + +\item{tag, tags}{Either a single tag name, or a character vector of tag names.} +} +\description{ +A \code{roxy_block} represents a single roxygen2 block. + +The \verb{block_*} functions provide a few helpers for common operations: +\itemize{ +\item \code{block_has_tag(blocks, tags)}: does \code{block} contain any of these \code{tags}? +\item \code{block_get_tags(block, tags)}: get all instances of \code{tags} +\item \code{block_get_tag(block, tag)}: get single tag. Returns \code{NULL} if 0, +throws warning if more than 1. +\item \code{block_get_tag_value(block, tag)}: gets \code{val} field from single tag. +} +} +\examples{ +# The easiest way to see the structure of a roxy_block is to create one +# using parse_text: +text <- " + #' This is a title + #' + #' @param x,y A number + #' @export + f <- function(x, y) x + y +" + +# parse_text() returns a list of blocks, so I extract the first +block <- parse_text(text)[[1]] +block +} +\keyword{internal} diff --git a/man/roxy_tag.Rd b/man/roxy_tag.Rd index 1b8c9ba0e..b42ced5e7 100644 --- a/man/roxy_tag.Rd +++ b/man/roxy_tag.Rd @@ -3,75 +3,24 @@ \name{roxy_tag} \alias{roxy_tag} \alias{roxy_tag_warning} -\alias{tag_value} -\alias{tag_inherit} -\alias{tag_name} -\alias{tag_two_part} -\alias{tag_name_description} -\alias{tag_words} -\alias{tag_words_line} -\alias{tag_toggle} -\alias{tag_code} -\alias{tag_examples} -\alias{tag_markdown} -\alias{tag_markdown_with_sections} -\title{Parsing tags.} +\title{\code{roxy_tag} S3 constructor} \usage{ -roxy_tag(tag, val, file = "", line = 0) +roxy_tag(tag, raw, val = NULL, file = NA_character_, line = NA_integer_) roxy_tag_warning(x, ...) - -tag_value(x) - -tag_inherit(x) - -tag_name(x) - -tag_two_part(first, second, required = TRUE, markdown = TRUE) - -tag_name_description(x) - -tag_words(min = 0, max = Inf) - -tag_words_line(x) - -tag_toggle(x) - -tag_code(x) - -tag_examples(x) - -tag_markdown(x) - -tag_markdown_with_sections(x) } \arguments{ \item{tag}{Tag name} -\item{val}{Tag value. When read from the file, this will be a string, -but after parsing can be a more complicated structure (typically -a character vector, but sometimes a list).} - -\item{file, line}{Location of the tag} +\item{raw}{Raw tag value, a string.} -\item{first, second}{Name of first and second parts of two part tags} +\item{val}{Parsed tag value, typically a character vector, but sometimes +a list. Usually filled in by \code{tag_parsers}} -\item{required}{Is the second part required (TRUE) or can it be blank -(FALSE)?} - -\item{markdown}{Should the second part be parsed as markdown?} - -\item{min, max}{Minimum and maximum number of words} +\item{file, line}{Location of the tag} } \description{ -\code{roxy_tag} constructs a tag object, and \code{roxy_tag_warning} makes -an informative warning using the location information stored in the tag. -The remainder of the tag functions parse the tag value, convert a string -into a richer R object, or providing informative warnings and returning -valid if the value is invalid. -} -\details{ -Two exceptions to the rule are \code{tag_words} and \code{tag_two_part}, which are -tag parsing generator functions. +\code{roxy_tag()} is the constructor for tag objects. +\code{roxy_tag_warning()} generates a warning that gives the location of the tag. } \keyword{internal} diff --git a/man/tag_parsers.Rd b/man/tag_parsers.Rd new file mode 100644 index 000000000..416a4102c --- /dev/null +++ b/man/tag_parsers.Rd @@ -0,0 +1,64 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tag-parser.R +\name{tag_parsers} +\alias{tag_parsers} +\alias{tag_value} +\alias{tag_inherit} +\alias{tag_name} +\alias{tag_two_part} +\alias{tag_name_description} +\alias{tag_words} +\alias{tag_words_line} +\alias{tag_toggle} +\alias{tag_code} +\alias{tag_examples} +\alias{tag_markdown} +\alias{tag_markdown_with_sections} +\title{Parse tags} +\usage{ +tag_value(x) + +tag_inherit(x) + +tag_name(x) + +tag_two_part(first, second, required = TRUE, markdown = TRUE) + +tag_name_description(x) + +tag_words(min = 0, max = Inf) + +tag_words_line(x) + +tag_toggle(x) + +tag_code(x) + +tag_examples(x) + +tag_markdown(x) + +tag_markdown_with_sections(x) +} +\arguments{ +\item{x}{A \link{roxy_tag} object to parss} + +\item{first, second}{Name of first and second parts of two part tags} + +\item{required}{Is the second part required (TRUE) or can it be blank +(FALSE)?} + +\item{markdown}{Should the second part be parsed as markdown?} + +\item{min, max}{Minimum and maximum number of words} +} +\value{ +A \link{roxy_tag} object with the \code{val} field set to the parsed value. +} +\description{ +These functions parse the \code{raw} tag value, convert a string into a richer R +object and storing it in \code{val}, or provide an informative warning and +returning \code{NULL}. Two exceptions to the rule are \code{tag_words()} and +\code{tag_two_part()}, which are tag parsing generator functions. +} +\keyword{internal} diff --git a/src/parser2.cpp b/src/parser2.cpp index 80d124115..49b80deb1 100644 --- a/src/parser2.cpp +++ b/src/parser2.cpp @@ -150,7 +150,8 @@ List tokenise_block(CharacterVector lines, std::string file = "", _["line"] = rows[i] + 1, _["tag"] = tags[i], // Rcpp::String() necessary to tag string as UTF-8 - _["val"] = Rcpp::String(stripTrailingNewline(vals[i])) + _["raw"] = Rcpp::String(stripTrailingNewline(vals[i])), + _["val"] = R_NilValue ); out[i].attr("class") = "roxy_tag"; } diff --git a/tests/testthat/test-block-print.txt b/tests/testthat/test-block-print.txt new file mode 100644 index 000000000..1dcf3c3d1 --- /dev/null +++ b/tests/testthat/test-block-print.txt @@ -0,0 +1,12 @@ +> block + [:6] + $tag + [line: 1] @title 'This is a title' {parsed} + [line: 5] @param 'x,y A number' {parsed} + [line: 6] @export '' {parsed} + [????:???] @backref '' {parsed} + $call f <- function(x, y) x + y + $object + $topic f + $alias f + diff --git a/tests/testthat/test-block.R b/tests/testthat/test-block.R index 6651c2112..7b03449eb 100644 --- a/tests/testthat/test-block.R +++ b/tests/testthat/test-block.R @@ -1,3 +1,18 @@ + +# object ------------------------------------------------------------------ + +test_that("has thoughtful print method", { + text <- " + #' This is a title + #' + #' @param x,y A number + #' @export + f <- function(x, y) x + y + " + block <- parse_text(text)[[1]] + verify_output(test_path("test-block-print.txt"), block) +}) + # description block ------------------------------------------------------- test_that("title and description taken from first line if only one", { @@ -166,8 +181,8 @@ test_that("description block preserves whitespace", { " )[[1]] - expect_equal(out$description, "Line 1\n Line 2") - expect_equal(out$details, "Line 1\n Line 2") + expect_equal(block_get_tag_value(out, "description"), "Line 1\n Line 2") + expect_equal(block_get_tag_value(out, "details"), "Line 1\n Line 2") }) @@ -190,7 +205,7 @@ test_that("errors are propagated", { #' @eval foo() NULL" ), - "@eval failed with error" + "failed with error" ) }) @@ -201,7 +216,7 @@ test_that("must return non-NA string", { #' @eval foo() NULL" ), - "@eval did not evaluate to a string" + "did not evaluate to a string" ) expect_warning( @@ -210,11 +225,10 @@ test_that("must return non-NA string", { #' @eval foo() NULL" ), - "@eval result contained NA" + "result contained NA" ) }) - test_that("also works with namespace roclet", { out <- roc_proc_text(namespace_roclet(), " foo <- function() '@export a' diff --git a/tests/testthat/test-namespace.R b/tests/testthat/test-namespace.R index 0a418416d..2151ca736 100644 --- a/tests/testthat/test-namespace.R +++ b/tests/testthat/test-namespace.R @@ -112,9 +112,6 @@ test_that("export uses name if no object present", { expect_equal(out, 'export(x)') }) - - - test_that("default export uses exportClass for RC objects", { out <- roc_proc_text(namespace_roclet(), " #' Title @@ -245,7 +242,7 @@ test_that("evalNamespace generates warning when code raises error", { #' @name a #' @title a NULL"), - "@evalNamespace failed with error" # From block_eval + "failed with error" # From block_eval ) }) @@ -258,7 +255,7 @@ test_that("evalNamespace generates warning when code doesn't eval to string", { #' @name a #' @title a NULL"), - "@evalNamespace did not evaluate to a string" # From block_eval + "did not evaluate to a string" # From block_eval ) # NA_character_ not allowed @@ -269,7 +266,7 @@ test_that("evalNamespace generates warning when code doesn't eval to string", { #' @name a #' @title a NULL"), - "@evalNamespace result contained NA" # From block_eval + "result contained NA" # From block_eval ) }) diff --git a/tests/testthat/test-object-s3.R b/tests/testthat/test-object-s3.R index 5a5a385e0..c1f104763 100644 --- a/tests/testthat/test-object-s3.R +++ b/tests/testthat/test-object-s3.R @@ -58,5 +58,5 @@ test_that("@method overrides auto-detection", { all.equal.data.frame <- function(...) 1 ")[[1]] - expect_equal(s3_method_info(attr(out, "object")$value), c("all.equal", "data.frame")) + expect_equal(s3_method_info(out$object$value), c("all.equal", "data.frame")) }) diff --git a/tests/testthat/test-rd-examples.R b/tests/testthat/test-rd-examples.R index 564cf47a9..0bed5c801 100644 --- a/tests/testthat/test-rd-examples.R +++ b/tests/testthat/test-rd-examples.R @@ -62,7 +62,7 @@ test_that("@example gives warning if used instead of @examples", { #' a <- 1 #' a + b NULL")[[1]], - "@example spans multiple lines" + "spans multiple lines" ) expect_null(get_tag(out, "examples")$values, NULL) diff --git a/tests/testthat/test-rd-inherit.R b/tests/testthat/test-rd-inherit.R index c75d4314d..2600770fe 100644 --- a/tests/testthat/test-rd-inherit.R +++ b/tests/testthat/test-rd-inherit.R @@ -53,7 +53,7 @@ test_that("no options gives default values", { ")[[1]] expect_equal( - block$inherit$fields, + block_get_tag_value(block, "inherit")$fields, c( "params", "return", "title", "description", "details", "seealso", "sections", "references", "examples", "author", "source" @@ -67,7 +67,7 @@ test_that("some options overrides defaults", { NULL ")[[1]] - expect_equal(block$inherit$fields, "return") + expect_equal(block_get_tag_value(block, "inherit")$fields, "return") }) diff --git a/tests/testthat/test-rd-param.R b/tests/testthat/test-rd-param.R index 96bc0d4bc..08a1120f5 100644 --- a/tests/testthat/test-rd-param.R +++ b/tests/testthat/test-rd-param.R @@ -69,23 +69,6 @@ test_that("argument order for multi-parameter documentation", { expect_equal(get_tag(out[["b.Rd"]], "param")$values, c(`x,z`="X,Z", y="Y", w="W")) }) -test_that("argument order for multiple usage statements", { - out <- roc_proc_text(rd_roclet(), " - #' A. - #' - #' @usage a(x, w) - #' @usage a(x, y) - #' @usage a(x, z) - #' @param x X - #' @param w W - #' @param y Y - #' @param z Z - a <- function(x, y, z, w) {} - ") - - expect_equal(get_tag(out[["a.Rd"]], "param")$values, c(x="X", y="Y", z="Z", w="W")) -}) - test_that("argument order for @rdfile", { out <- roc_proc_text(rd_roclet(), " #' A diff --git a/tests/testthat/test-rd-raw.R b/tests/testthat/test-rd-raw.R index 1a73af7ac..72cf5a215 100644 --- a/tests/testthat/test-rd-raw.R +++ b/tests/testthat/test-rd-raw.R @@ -29,7 +29,7 @@ test_that("error-ful evalRd generates warning", { #' @name a #' @title a NULL"), - "@evalRd failed with error" + "failed with error" ) }) diff --git a/tests/testthat/test-tag.R b/tests/testthat/test-tag.R index c71f001ed..6fa125d51 100644 --- a/tests/testthat/test-tag.R +++ b/tests/testthat/test-tag.R @@ -68,8 +68,12 @@ test_that("incomplete rd in tag raises error", { }) test_that("incomplete rd in prequel raises error", { - expect_warning(roc_proc_text(rd_roclet(), " - #' Title { - x <- 1"), "mismatched braces") + expect_warning( + roc_proc_text(rd_roclet(), " + #' Title { + x <- 1" + ), + "mismatched braces" + ) }) diff --git a/tests/testthat/test-tokenize.R b/tests/testthat/test-tokenize.R index 364059a9f..afb02d5ab 100644 --- a/tests/testthat/test-tokenize.R +++ b/tests/testthat/test-tokenize.R @@ -5,7 +5,7 @@ test_that("parses into tag and value", { expect_equal(length(x), 1) expect_equal(x[[1]]$tag, "xyz") - expect_equal(x[[1]]$val, "abc") + expect_equal(x[[1]]$raw, "abc") }) test_that("description block gets empty tag", { @@ -13,7 +13,7 @@ test_that("description block gets empty tag", { expect_equal(length(x), 1) expect_equal(x[[1]]$tag, "") - expect_equal(x[[1]]$val, "abc") + expect_equal(x[[1]]$raw, "abc") }) test_that("multi line tags collapsed into one", { @@ -22,7 +22,7 @@ test_that("multi line tags collapsed into one", { "#' def" )) expect_equal(length(x), 1) - expect_equal(x[[1]]$val, "abc\n def") + expect_equal(x[[1]]$raw, "abc\n def") }) test_that("description block gets empty tag when followed by tag", { @@ -33,10 +33,10 @@ test_that("description block gets empty tag when followed by tag", { expect_equal(length(x), 2) expect_equal(x[[1]]$tag, "") - expect_equal(x[[1]]$val, "abc") + expect_equal(x[[1]]$raw, "abc") expect_equal(x[[2]]$tag, "xyz") - expect_equal(x[[2]]$val, "abc") + expect_equal(x[[2]]$raw, "abc") }) test_that("leading whitespace is ignored", { @@ -53,7 +53,7 @@ test_that("need one or more #", { }) test_that("@@ becomes @", { - expect_equal(tokenise_block("#' @tag @@")[[1]]$val, "@") + expect_equal(tokenise_block("#' @tag @@")[[1]]$raw, "@") }) # Inline comments --------------------------------------------------------- @@ -89,3 +89,4 @@ test_that("Inline comments do not extend past the closing brace", { }; #' @seealso somewhere")[[1]] expect_null(get_tag(out, "seealso")) }) + diff --git a/vignettes/extending.Rmd b/vignettes/extending.Rmd index f8f64b74c..85e3188a4 100644 --- a/vignettes/extending.Rmd +++ b/vignettes/extending.Rmd @@ -9,212 +9,92 @@ vignette: > ```{r, include = FALSE} knitr::opts_chunk$set(comment = "#>", collapse = TRUE) -library(roxygen2) ``` ## Basics -Roxygen is extensible with user-defined roclets. -It means that you can take advantage of Roxygen's parser in various ways. -It is not a general tool for static analysis as only so-called 'blocks' are parsed, i.e. -comment blocks that has the `#'` line prefix. -On the other hand, the parser carries along information on the corresponding code -which then can be used. -Thus, as we will demonstrate first, you can exploit the parser and use the information in blocks -and corresponding code. +Roxygen is extensible with user-defined __roclets__. +It means that you can take advantage of Roxygen's parser and extend it with your own `@tags`. -Another option is to introduce custom tags similar to e.g. `@title`, `@param` etc. tags. -We will cover this in the end of this vignette. +Note that's not currently possible to extend an existing roclet, so you need to create your own if you want to add new features. This restriction may be relaxed in the future. -It is worth noting that it is not possible for a user to extend -included roclets (e.g. the `rd` roclet) to allow for new tags. -It is necessary to write a new roclet that can be used together with or instead of e.g. -the included `rd` roclet. +```{r setup} +library(roxygen2) +``` -## Roclets +## Key data structures -Running a roclet consists of multiple parts as documented in `?roclet`. -Very briefly, a roclet can implement a number of functions (please refer to `?roclet` for details): +Before we talk about creating your own roclets, we need to first discuss two important data structures that power roxygen: blocks and tags. -* `roclet_tags()`: named list of tags whose values are tag parsing functions (optional to implement) -* `roclet_preprocess()`: called after blocks have been parsed but before code has been evaluated (optional to implement) -* `roclet_process()`: called after `@eval` tag has been processed (main functionality) -* `roclet_output()`: produces files on disk based on the result from `roclet_process()` (output functionality) +### Tags -Depending on the roclet, there may be some work in managing files, ensuring that no existing files are over-written, deleted etc. That must be handled, too. -Be sure not to change or delete something that you did not create with the explicit -disclaimer that the content can be modified or deleted. -Roxy does this by the header (first line of the file) containing "`Generated by roxygen2: do not edit by hand`". - -A roclet will use a list of tag parsers to process the written documentation. -Internally, roxygen creates a `roxy_tag()` which is then passed on to the -tag parser specified. +A tag (a list with S3 class `roxy_tag`) represents a single tag. +It contains: -### Custom roclets +* `tag`: the name of the tag. -This example will list the functions with inconsistent parameter documentation, e.g. missing `@param`s or too many `@param`s. -This is caught when `R` is checking a package, but sometimes it is nice to catch is before that. -The check will be a lot more simplistic than the one from `R` check, but the idea is to illustrate Roxygen's functionality. +* `raw`: the raw contents of the tag (i.e. everything from the end of + this tag to the begining of the net). + +* `val`: the parsed value, which we'll come back to shortly. -Other examples of such functionality is to display -functions with most lines (to avoid too long functions), -functions with most parameters, -inconsistent parameter naming and so on. +* `file` and `line`: the location of the tag in the package. Used + with `roxy_tag_warning()` to produce informative error messages. -Let us focus on a block like this: +You _can_ construct tag objects by hand with `roxy_tag()`: ```{r} -txt1 <- " -#' Summing two numbers -#' -#' @param x A number -#' @param y Another number -f <- function(x, y) { - x + y -}" +roxy_tag("name", "Hadley") +str(roxy_tag("name", "Hadley")) ``` -The output from the `rd` roclet will be: +However, you should rarely need to do so, because you'll typically have them given to you in a block object -```{r} -roc_proc_text(rd_roclet(), txt1) -``` +## Blocks -Say that we forgot to document the `y` parameter: +A block (a list with S3 class `roxy_block`) represents a single roxygen block. It contains: -```{r} -txt2 <- " -#' Summing two numbers -#' -#' @param x A number -f <- function(x, y) { - x + y -}" -``` +* `tags`: a list of `roxy_tags`. +* `call`: the R code associated with the block (usually a function call). +* `file` and `line`: the location of the R code. +* `object`: the evaluated R object associated with the code. -The output from the `rd` roclet will be: +The easiest way to see the basic structure of a `roxy_block()` is to generate one by parsing a roxygen block: ```{r} -roc_proc_text(rd_roclet(), txt2) -``` - -That is in principle fine. But we do not discover that `y` is missing a `@param` (until checking the package). -Thus, we make a roclet to help us catch this earlier. -The input argument `blocks` to `roclet_process()` is a list of blocks. -Below, we show the block from `txt2` to give you an idea of its structure: - -``` -List of 2 - $ title: chr "Summing two numbers" - $ param:List of 2 - ..$ name : chr "x" - ..$ description: chr "A number" - - attr(*, "filename")= chr "/tmp/RtmpZx52lH/file2498288d1e88" - - attr(*, "location")= int [1:8] 5 1 7 1 1 1 5 7 - - attr(*, "call")= language f <- function(x, y) { x + y ... - - attr(*, "class")= chr "roxy_block" - - attr(*, "object")=List of 3 - ..$ alias : chr "f" - ..$ value :function (x, y) - ..$ methods: NULL - ..- attr(*, "class")= chr [1:2] "function" "object" +text <- " + #' This is a title + #' + #' @param x,y A number + #' @export + f <- function(x, y) x + y +" + +# parse_text() returns a list of blocks, so I extract the first +block <- parse_text(text)[[1]] +block ``` -Now, we can implement the roclet: - -```{r} -param_roclet <- function() { - roxygen2::roclet("param") -} - -roclet_process.roclet_param <- function(x, blocks, env, base_path, global_options = list()) { - warns <- list() - - for (block in blocks) { - block_obj <- attr(block, "object") - - if (!is(block_obj, "function")) { - next - } - - fun_args <- formalArgs(block_obj$value) - block_params <- block[names(block) == "param"] - block_params_names <- sort(unname(sapply(block_params, function(x) x$name), force = TRUE)) - - if (!isTRUE(all.equal(fun_args, block_params_names))) { - func_name <- block_obj$alias - - missing_params <- setdiff(fun_args, block_params_names) - toomany_params <- setdiff(block_params_names, fun_args) - msg <- "" - if (length(missing_params) > 0) { - msg <- paste0(msg, "\n - Missing @param's: ", paste0(missing_params, collapse = ", ")) - } - if (length(toomany_params) > 0) { - msg <- paste0(msg, "\n + Too many @param's: ", paste0(toomany_params, collapse = ", ")) - } - - warn <- paste0("Function '", func_name, "' with title '", block$title, "': ", msg) - warns <- c(warns, warn) - } - } - - return(warns) -} - -roc_proc_text(param_roclet(), txt2) -``` +## Roclets -Notice here that the roclet is only processed. -The output is not made. -To specify the output functionality you must implement a `roclet_output.roclet_param()` method. -It would probably go something like: +To create your own -```{r} -roclet_output.roclet_param <- function(x, results, base_path, ...) { - if (length(results) == 0L) { - return(invisible(NULL)) - } - - cat("Functions with @param inconsistency:\n") - cat(paste0(" * ", results, collapse = "\n"), sep = "") - - return(invisible(NULL)) -} -``` +Running a roclet consists of multiple parts as documented in `?roclet`. +Very briefly, a roclet can implement a number of functions (please refer to `?roclet` for details): -We can now make complete examples of three different cases: +* `roclet_tags()`: named list of tags whose values are tag parsing functions (optional to implement) +* `roclet_preprocess()`: called after blocks have been parsed but before code has been evaluated (optional to implement) +* `roclet_process()`: called after `@eval` tag has been processed (main functionality) +* `roclet_output()`: produces files on disk based on the result from `roclet_process()` (output functionality) -```{r} -# All good: -roclet_output(param_roclet(), roc_proc_text(param_roclet(), " -#' Summing two numbers -#' -#' @param x A number -#' @param y Another number -f <- function(x, y) { - x + y -}")) -# Missing documentation of 'y' parameter -roclet_output(param_roclet(), roc_proc_text(param_roclet(), " -#' Summing two numbers -#' -#' @param x A number -f <- function(x, y) { - x + y -}")) -# Additional documentation for 'z' parameter -roclet_output(param_roclet(), roc_proc_text(param_roclet(), " -#' Summing two numbers -#' -#' @param x A number -#' @param y Another number -#' @param z A third parameter -f <- function(x, y) { - x + y -}")) -``` +Depending on the roclet, there may be some work in managing files, ensuring that no existing files are over-written, deleted etc. That must be handled, too. +Be sure not to change or delete something that you did not create with the explicit +disclaimer that the content can be modified or deleted. +Roxy does this by the header (first line of the file) containing "`Generated by roxygen2: do not edit by hand`". +A roclet will use a list of tag parsers to process the written documentation. +Internally, roxygen creates a `roxy_tag()` which is then passed on to the +tag parser specified. ### Custom tags @@ -235,27 +115,22 @@ As an example: The idea is that all memos with the same headline are grouped together. -```{r} +```{r, eval = FALSE} memo_roclet <- function() { roxygen2::roclet("memo") } # Based on roxygen2:::tag_name_description tag_memo <- function(x) { - #print(x) - #print(str(x)) - - # Check syntax stopifnot(grepl("^\\[.*\\].*$", x$val)) - memo_parsed <- stringi::stri_match(str = x$val, regex = "\\[(.*)\\](.*)") - - header <- memo_parsed[1L, 2L] - msg <- memo_parsed[1L, 3L] - - x$val <- list(header = header, msg = msg) - - return(x) + parsed <- stringi::stri_match(str = x$val, regex = "\\[(.*)\\](.*)")[1, ] + + x$val <- list( + header = parsed[[2]], + message = parsed[[3]] + ) + x } roclet_tags.roclet_memo <- function(x) { @@ -263,50 +138,28 @@ roclet_tags.roclet_memo <- function(x) { } roclet_process.roclet_memo <- function(x, blocks, env, base_path, global_options = list()) { - memos <- list() - - print(str(blocks)) + results <- list() for (block in blocks) { - block_memos <- block[names(block) == "memo"] - print(str(block_memos)) - - if (length(block_memos) == 0L) { - next - } - - for (m in block_memos) { - extra <- "" - - if (!is.null(attr(block, "object")) && - !is.null(attr(block, "object")$alias) && - !is.null(attr(block, "filename"))) { - extra <- paste0(" @ '", attr(block, "object")$alias, "' [", attr(block, "filename"), "]") - } - - msg <- paste0(m$msg, extra) - memos[[m$header]] <- c(memos[[m$header]], msg) + tags <- block_get_tags(block, "memo") + + for (tag in tags) { + msg <- paste0("[", tag$file, ":", tag$line, "] ", tag$val$message) + results[[tag$val$header]] <- c(results[[tag$val$header]], msg) } - } - return(memos) + results } roclet_output.roclet_memo <- function(x, results, base_path, ...) { - if (length(results) == 0L) { - return(invisible(NULL)) + for (header in names(results)) { + messages <- results[[header]] + cat(paste0(header, ": ", "\n")) + cat(paste0(" * ", messages, "\n", collapse = "")) } - - cat("Memos:\n") - - for (i in seq_along(results)) { - cat(" ", names(results)[i], ":\n", sep = "") - cat(paste0(" - ", results[[i]], collapse = "\n"), sep = "") - cat("\n") - } - - return(invisible(NULL)) + + invisible(NULL) } results <- roc_proc_text(memo_roclet(), " @@ -320,7 +173,7 @@ f <- function(x, y) { g <- function(x, y) { # ... } -", registry = roclet_tags.roclet_memo()) +") roclet_output(memo_roclet(), results) ```