Skip to content

Commit

Permalink
Massive refactoring of roxy_block() data structure
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
hadley committed Sep 20, 2019
1 parent c7754e7 commit 8c82a07
Show file tree
Hide file tree
Showing 38 changed files with 965 additions and 907 deletions.
7 changes: 7 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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).

Expand Down
197 changes: 124 additions & 73 deletions R/block.R
Original file line number Diff line number Diff line change
@@ -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("<roxy_block> @ ", 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("<roxy_block> [", 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()) {
Expand All @@ -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
)
}
Expand All @@ -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()
Expand All @@ -96,72 +122,97 @@ 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,
global_options = global_options
)

# 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) {
Expand Down Expand Up @@ -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])
}
Expand All @@ -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]
}

Expand All @@ -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
}
Expand Down
2 changes: 0 additions & 2 deletions R/field.R
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
8 changes: 0 additions & 8 deletions R/markdown.R
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
Loading

0 comments on commit 8c82a07

Please sign in to comment.