Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* `@section` titles can now contain code that includes a colon (#1878).
* The automatic usage for a data object that is conditional on the `LazyData` option in the `DESCRIPTION` (see below) now correctly detects all ways to specify a true value, e.g. also `yes`, `Yes` or `True` (@jranke, #1881).
* `@import` now inserts the directive as is into `NAMESPACE` when it contains a comma, making it possible to use other forms like `@import rlang, except = ":="`.
* `@importFrom`, `@importClassesFrom`, and `@importMethodsFrom` now accept multi-line input, restoring the ability to spread imports across multiple lines for readability; continuation lines must use a hanging indent, so the first flush or blank line ends the tag and content after it (e.g. from a forgotten `@examples`) is no longer silently absorbed into the namespace (#1890).

# roxygen2 8.0.0

Expand Down
6 changes: 3 additions & 3 deletions R/namespace.R
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ roxy_tag_ns.roxy_tag_import <- function(x, block, env) {

#' @export
roxy_tag_parse.roxy_tag_importClassesFrom <- function(x) {
tag_words(x, min = 2)
tag_words(x, min = 2, multiline = "indent")
}
#' @export
roxy_tag_ns.roxy_tag_importClassesFrom <- function(x, block, env) {
Expand All @@ -276,7 +276,7 @@ roxy_tag_ns.roxy_tag_importClassesFrom <- function(x, block, env) {

#' @export
roxy_tag_parse.roxy_tag_importFrom <- function(x) {
tag_words(x, min = 2)
tag_words(x, min = 2, multiline = "indent")
}
#' @export
roxy_tag_ns.roxy_tag_importFrom <- function(x, block, env) {
Expand All @@ -301,7 +301,7 @@ roxy_tag_ns.roxy_tag_importFrom <- function(x, block, env) {

#' @export
roxy_tag_parse.roxy_tag_importMethodsFrom <- function(x) {
tag_words(x, min = 2)
tag_words(x, min = 2, multiline = "indent")
}
#' @export
roxy_tag_ns.roxy_tag_importMethodsFrom <- function(x, block, env) {
Expand Down
2 changes: 1 addition & 1 deletion R/rd-describe-in.R
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ roxy_tag_parse.roxy_tag_describeIn <- function(x) {
)
NULL
} else {
tag_two_part(x, "a topic name", "a description", multiline = TRUE)
tag_two_part(x, "a topic name", "a description", multiline = "always")
}
}

Expand Down
2 changes: 1 addition & 1 deletion R/rd-examples.R
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ roxy_tag_parse.roxy_tag_examplesIf <- function(x) {
}
#' @export
roxy_tag_parse.roxy_tag_example <- function(x) {
x <- tag_value(x, multiline = TRUE)
x <- tag_value(x, multiline = "always")

nl <- re_count(x$val, "\n")
if (any(nl) > 0) {
Expand Down
2 changes: 1 addition & 1 deletion R/rd-params.R
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#' @export
roxy_tag_parse.roxy_tag_param <- function(x) {
tag_two_part(x, "an argument name", "a description", multiline = TRUE)
tag_two_part(x, "an argument name", "a description", multiline = "always")
}

#' @export
Expand Down
2 changes: 1 addition & 1 deletion R/rd-r6-external.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ roxy_tag_parse.roxy_tag_R6method <- function(x) {
warn_roxy_tag(x, "requires a value like {.code Class$method}")
return(NULL)
}
warn_if_multiline(x, raw, multiline = FALSE)
raw <- check_multiline(x, raw, multiline = "never")

pieces <- strsplit(raw, "\\$")[[1]]
if (length(pieces) != 2 || pieces[1] == "" || pieces[2] == "") {
Expand Down
2 changes: 1 addition & 1 deletion R/rd-raw.R
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ roxy_tag_rd.roxy_tag_evalRd <- function(x, base_path, env) {
}

#' @export
roxy_tag_parse.roxy_tag_rawRd <- function(x) tag_value(x, multiline = TRUE)
roxy_tag_parse.roxy_tag_rawRd <- function(x) tag_value(x, multiline = "always")
#' @export
roxy_tag_rd.roxy_tag_rawRd <- function(x, base_path, env) {
rd_section(x$tag, x$val)
Expand Down
4 changes: 2 additions & 2 deletions R/rd-s4.R
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#' @export
roxy_tag_parse.roxy_tag_field <- function(x) {
tag_two_part(x, "a field name", "a description", multiline = TRUE)
tag_two_part(x, "a field name", "a description", multiline = "always")
}
#' @export
roxy_tag_rd.roxy_tag_field <- function(x, base_path, env) {
Expand All @@ -14,7 +14,7 @@ format.rd_section_field <- function(x, ...) {

#' @export
roxy_tag_parse.roxy_tag_slot <- function(x) {
tag_two_part(x, "a slot name", "a description", multiline = TRUE)
tag_two_part(x, "a slot name", "a description", multiline = "always")
}
#' @export
roxy_tag_rd.roxy_tag_slot <- function(x, base_path, env) {
Expand Down
2 changes: 1 addition & 1 deletion R/rd-s7.R
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#' @export
roxy_tag_parse.roxy_tag_prop <- function(x) {
x <- tag_two_part(x, "a property name", "a description", multiline = TRUE)
x <- tag_two_part(x, "a property name", "a description", multiline = "always")
if (is.null(x)) {
return()
}
Expand Down
2 changes: 1 addition & 1 deletion R/rd-template.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ roxy_tag_parse.roxy_tag_template <- function(x) {

#' @export
roxy_tag_parse.roxy_tag_templateVar <- function(x) {
tag_two_part(x, "a variable name", "a value", multiline = TRUE)
tag_two_part(x, "a variable name", "a value", multiline = "always")
}

process_templates <- function(block, base_path) {
Expand Down
2 changes: 1 addition & 1 deletion R/rd-usage.R
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#' @export
roxy_tag_parse.roxy_tag_usage <- function(x) {
x <- tag_value(x, multiline = TRUE)
x <- tag_value(x, multiline = "always")
x$val <- rd(x$val)
x
}
Expand Down
95 changes: 79 additions & 16 deletions R/tag-parser.R
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,29 @@ NULL

#' @export
#' @rdname tag_parsers
#' @param multiline If `FALSE` (the default), tags that span multiple lines
#' will generate a warning. Set to `TRUE` for tags where multiline content
#' is expected (e.g., `@usage`, `@rawRd`).
tag_value <- function(x, multiline = FALSE) {
#' @param multiline Controls how the tag may span multiple lines:
#' * `"never"` (the default): the tag must be a single line, and spanning
#' multiple lines generates a warning.
#' * `"indent"`: the tag may span multiple lines, but continuation lines must

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mind applying https://design.tidyverse.org/boolean-strategies.html#how-do-you-remediate-past-mistakes ? (but without the deprecation; just silently handle TRUE/FALSE since this is mostly an internal API)

#' use a hanging indent (i.e. be indented more than the first line). The
#' first line that is not indented (including a blank line) ends the tag,
#' and anything after it is ignored, with a warning. Use this for tags where
#' multiline input is convenient but a flush line almost always signals a
#' missing tag (e.g., `@importFrom`).
#' * `"always"`: the tag may span any number of lines and paragraphs. Use this
#' for tags where multiline content is expected (e.g., `@usage`, `@rawRd`).
#'
#' For backward compatibility, `FALSE` and `TRUE` are accepted as synonyms for
#' `"never"` and `"always"` respectively.
tag_value <- function(x, multiline = "never") {
x$val <- trimws(x$raw)

if (x$val == "") {
warn_roxy_tag(x, "requires a value")
return(NULL)
}

warn_if_multiline(x, x$val, multiline)
x$val <- check_multiline(x, x$val, multiline)

if (!rdComplete(x$raw, is_code = FALSE)) {
warn_roxy_tag(x, "has mismatched braces or quotes")
Expand Down Expand Up @@ -123,7 +134,7 @@ tag_two_part <- function(
second,
required = TRUE,
markdown = TRUE,
multiline = FALSE
multiline = "never"
) {
if (trimws(x$raw) == "") {
if (!required) {
Expand All @@ -136,8 +147,8 @@ tag_two_part <- function(
warn_roxy_tag(x, "has mismatched braces or quotes")
NULL
} else {
warn_if_multiline(x, trimws(x$raw), multiline)
pieces <- split_two_part(trimws(x$raw))
val <- check_multiline(x, trimws(x$raw), multiline)
pieces <- split_two_part(val)

if (required && pieces[[2]] == "") {
warn_roxy_tag(x, "requires two parts: {first} and {second}")
Expand Down Expand Up @@ -186,12 +197,12 @@ tag_name_description <- function(x) {
#' @export
#' @rdname tag_parsers
#' @param min,max Minimum and maximum number of words
tag_words <- function(x, min = 0, max = Inf, multiline = FALSE) {
tag_words <- function(x, min = 0, max = Inf, multiline = "never") {
val <- trimws(x$raw)

warn_if_multiline(x, val, multiline)
val <- check_multiline(x, val, multiline)

if (!rdComplete(x$raw, is_code = FALSE)) {
if (!rdComplete(val, is_code = FALSE)) {
warn_roxy_tag(x, "has mismatched braces or quotes")
return(NULL)
}
Expand All @@ -216,11 +227,35 @@ tag_words_line <- function(x) {
tag_words(x)
}

# Warns if multiline is FALSE and val contains multiple lines.
warn_if_multiline <- function(x, val, multiline) {
if (multiline) {
return(invisible())
# Normalises the `multiline` argument to one of "never", "indent", or "always",
# silently translating the legacy `FALSE`/`TRUE` values for backward
# compatibility.
as_multiline <- function(multiline, error_call = caller_env()) {
if (isTRUE(multiline)) {
return("always")
}
if (isFALSE(multiline)) {
return("never")
}

arg_match0(multiline, c("never", "indent", "always"), error_call = error_call)
}

# Applies the multiline policy for a tag's value, warning when it is violated
# and returning the value to use (possibly truncated to its hanging-indented
# continuation). See the `multiline` parameter of `tag_value()` for the meaning
# of each mode.
check_multiline <- function(x, val, multiline) {
multiline <- as_multiline(multiline)

if (multiline == "always") {
return(val)
}

if (multiline == "indent") {
return(check_indent(x, val))
}

n_lines <- re_count(val, "\n")
if (n_lines >= 1) {
first_line <- re_split_half(val, "\n")[[1]]
Expand All @@ -232,7 +267,35 @@ warn_if_multiline <- function(x, val, multiline) {
)
)
}
invisible()

val
}

# Keeps the first line of `val` plus any immediately following lines that use a
# hanging indent (indented more than the first line). The first flush or blank
# line ends the tag; anything after it is dropped with a warning, since a flush
# line usually signals a forgotten tag (e.g. a missing `@examples`).
check_indent <- function(x, val) {
lines <- strsplit(val, "\n", fixed = TRUE)[[1]]
if (length(lines) <= 1) {
return(val)
}

indent <- leadingSpaces(lines)
continues <- nzchar(trimws(lines[-1])) & indent[-1] > indent[[1]]

ends_at <- if (all(continues)) length(lines) else which(!continues)[[1]]
if (ends_at < length(lines)) {
warn_roxy_tag(
x,
c(
"must use a hanging indent to span multiple lines",
i = "Continuation lines must be indented; did you forget a tag like {.code @examples}?"
)
)
}

paste(lines[seq_len(ends_at)], collapse = "\n")
}

#' @export
Expand Down
25 changes: 19 additions & 6 deletions man/tag_parsers.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 14 additions & 6 deletions tests/testthat/_snaps/namespace.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
Message
Writing 'NAMESPACE'
i Loading testNamespace
x multiline.R:1: @importFrom must be only 1 line long, not 2.
i The first line is "stats median"
x multiline.R:1: @import must be only 1 line long, not 2.
i The first line is "stats"

# @exportS3Method generates fully automatically

Expand Down Expand Up @@ -43,13 +43,21 @@
Message
x <text>:2: @importFrom must have at least 2 words, not 1.

# multiline importFrom generates warning
# blank line ends a multiline importFrom

Code
. <- roc_proc_text(namespace_roclet(), block)
out <- roc_proc_text(namespace_roclet(), block)
Message
x <text>:2: @importFrom must use a hanging indent to span multiple lines.
i Continuation lines must be indented; did you forget a tag like `@examples`?

# flush line ends a multiline importFrom

Code
out <- roc_proc_text(namespace_roclet(), block)
Message
x <text>:2: @importFrom must be only 1 line long, not 2.
i The first line is "test test1"
x <text>:2: @importFrom must use a hanging indent to span multiple lines.
i Continuation lines must be indented; did you forget a tag like `@examples`?

# can regenerate NAMESPACE even if its broken

Expand Down
Loading
Loading