From 952a588c10558328c2acacda630eab6ad8465ca8 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Wed, 22 Apr 2026 15:00:05 -0500 Subject: [PATCH] Also warn about multiline inputs to `tag_two_part()` Fixes #1866 --- NAMESPACE | 1 + NEWS.md | 2 +- R/rd-describe-in.R | 2 +- R/rd-params.R | 2 +- R/rd-s4.R | 4 ++-- R/rd-s7.R | 2 +- R/rd-template.R | 2 +- R/tag-parser.R | 11 ++++++++++- man/tag_parsers.Rd | 9 ++++++++- tests/testthat/_snaps/tag-parser.md | 11 +++++++++++ tests/testthat/test-tag-parser.R | 11 +++++++++++ 11 files changed, 48 insertions(+), 9 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 561a63bcb..90b86d4e2 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -69,6 +69,7 @@ S3method(merge,rd_section_inherit_section) S3method(merge,rd_section_minidesc) S3method(merge,rd_section_param) S3method(merge,rd_section_prop) +S3method(merge,rd_section_r6_class) S3method(merge,rd_section_reexport) S3method(merge,rd_section_section) S3method(merge,rd_section_seealso) diff --git a/NEWS.md b/NEWS.md index 01257b6a7..70a4d99bb 100644 --- a/NEWS.md +++ b/NEWS.md @@ -74,7 +74,7 @@ See `vignette("rd-S7")` for best practices. ## Individual tags -* Tags that expect single-line input now warn when they span multiple lines, catching a common class of mistake. Affected tags: `@aliases`, `@concept`, `@encoding`, `@exportClass`, `@exportMethod`, `@exportPattern`, `@exportS3Method`, `@importFrom`, `@importClassesFrom`, `@importMethodsFrom`, `@include`, `@keywords`, `@method`, `@name`, `@order`, `@rdname`, `@S3method`, `@template`, and `@useDynLib` (#1642, #1688). This may break some existing usage, but it prevents a wide range of otherwise silent errors. +* Tags that expect single-line input now warn when they span multiple lines, catching a common class of mistake. Affected tags: `@aliases`, `@concept`, `@encoding`, `@exportClass`, `@exportMethod`, `@exportPattern`, `@exportS3Method`, `@importFrom`, `@importClassesFrom`, `@importMethodsFrom`, `@include`, `@includeRmd`, `@inheritDotParams`, `@inheritParams`, `@inheritSection`, `@keywords`, `@method`, `@name`, `@order`, `@rdname`, `@S3method`, `@template`, and `@useDynLib` (#1642, #1688). This may break some existing usage, but it prevents a wide range of otherwise silent errors. * Reexported functions now display with `()` appended (e.g. `fun()` instead of `fun`) on the reexports page, except for infix operators like `%>%` (#1222). They also use the modern (>= 4.1.0) linking style. * `@description` no longer errors when the markdown text starts with a heading (#1705). * `@examples` no longer warns about unmatched braces inside raw strings, or inside strings within R comments, e.g. `# '{greeting}'` (#1492). diff --git a/R/rd-describe-in.R b/R/rd-describe-in.R index 3d6b480ea..72c5a6645 100644 --- a/R/rd-describe-in.R +++ b/R/rd-describe-in.R @@ -10,7 +10,7 @@ roxy_tag_parse.roxy_tag_describeIn <- function(x) { ) NULL } else { - tag_two_part(x, "a topic name", "a description") + tag_two_part(x, "a topic name", "a description", multiline = TRUE) } } diff --git a/R/rd-params.R b/R/rd-params.R index b06277f18..4edb67779 100644 --- a/R/rd-params.R +++ b/R/rd-params.R @@ -1,6 +1,6 @@ #' @export roxy_tag_parse.roxy_tag_param <- function(x) { - tag_two_part(x, "an argument name", "a description") + tag_two_part(x, "an argument name", "a description", multiline = TRUE) } #' @export diff --git a/R/rd-s4.R b/R/rd-s4.R index faeb3333b..e22c3d2fd 100644 --- a/R/rd-s4.R +++ b/R/rd-s4.R @@ -1,6 +1,6 @@ #' @export roxy_tag_parse.roxy_tag_field <- function(x) { - tag_two_part(x, "a field name", "a description") + tag_two_part(x, "a field name", "a description", multiline = TRUE) } #' @export roxy_tag_rd.roxy_tag_field <- function(x, base_path, env) { @@ -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") + tag_two_part(x, "a slot name", "a description", multiline = TRUE) } #' @export roxy_tag_rd.roxy_tag_slot <- function(x, base_path, env) { diff --git a/R/rd-s7.R b/R/rd-s7.R index a092c3972..d9d8864e9 100644 --- a/R/rd-s7.R +++ b/R/rd-s7.R @@ -1,6 +1,6 @@ #' @export roxy_tag_parse.roxy_tag_prop <- function(x) { - x <- tag_two_part(x, "a property name", "a description") + x <- tag_two_part(x, "a property name", "a description", multiline = TRUE) if (is.null(x)) { return() } diff --git a/R/rd-template.R b/R/rd-template.R index 1fbf1fc32..9bca3cfe3 100644 --- a/R/rd-template.R +++ b/R/rd-template.R @@ -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") + tag_two_part(x, "a variable name", "a value", multiline = TRUE) } process_templates <- function(block, base_path) { diff --git a/R/tag-parser.R b/R/tag-parser.R index 60bf3fbc2..0ee91f71c 100644 --- a/R/tag-parser.R +++ b/R/tag-parser.R @@ -119,7 +119,14 @@ tag_name <- function(x) { #' @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(x, first, second, required = TRUE, markdown = TRUE) { +tag_two_part <- function( + x, + first, + second, + required = TRUE, + markdown = TRUE, + multiline = FALSE +) { if (trimws(x$raw) == "") { if (!required) { warn_roxy_tag(x, "requires {first}") @@ -127,6 +134,8 @@ tag_two_part <- function(x, first, second, required = TRUE, markdown = TRUE) { warn_roxy_tag(x, "requires two parts: {first} and {second}") } NULL + } else if (!multiline && warn_if_multiline(x, trimws(x$raw))) { + NULL } else if (!rdComplete(x$raw, is_code = FALSE)) { warn_roxy_tag(x, "has mismatched braces or quotes") NULL diff --git a/man/tag_parsers.Rd b/man/tag_parsers.Rd index cf534ede1..820a25be0 100644 --- a/man/tag_parsers.Rd +++ b/man/tag_parsers.Rd @@ -22,7 +22,14 @@ tag_inherit(x) tag_name(x) -tag_two_part(x, first, second, required = TRUE, markdown = TRUE) +tag_two_part( + x, + first, + second, + required = TRUE, + markdown = TRUE, + multiline = FALSE +) tag_name_description(x) diff --git a/tests/testthat/_snaps/tag-parser.md b/tests/testthat/_snaps/tag-parser.md index 4f89e2980..b5d52f687 100644 --- a/tests/testthat/_snaps/tag-parser.md +++ b/tests/testthat/_snaps/tag-parser.md @@ -182,6 +182,17 @@ x test.R:1: @test must be only 1 line long, not 3. i The first line is "a" +# tag_two_part() warns on multi-line content by default + + Code + tag <- roxy_test_tag("foo bar\nbaz") + expect_parse_failure(tag_two_part(tag, "a name", "a value")) + Output + + Message: + x test.R:1: @test must be only 1 line long, not 2. + i The first line is "foo bar" + # tag_value() warns on multi-line content Code diff --git a/tests/testthat/test-tag-parser.R b/tests/testthat/test-tag-parser.R index fcb4d5adf..4c06ffcc0 100644 --- a/tests/testthat/test-tag-parser.R +++ b/tests/testthat/test-tag-parser.R @@ -87,6 +87,17 @@ test_that("tag_words() warns on multi-line content", { }) }) +test_that("tag_two_part() warns on multi-line content by default", { + expect_snapshot({ + tag <- roxy_test_tag("foo bar\nbaz") + expect_parse_failure(tag_two_part(tag, "a name", "a value")) + }) + + tag <- roxy_test_tag("foo bar\nbaz") + out <- tag_two_part(tag, "a name", "a value", multiline = TRUE) + expect_equal(out$val, list(name = "foo", description = "bar\nbaz")) +}) + test_that("tag_value() warns on multi-line content", { expect_snapshot({ tag <- roxy_test_tag("a\nb")