From 66cc7d6ea7c094776ca9819be3a7530e2c8f92e2 Mon Sep 17 00:00:00 2001 From: Dirk Eddelbuettel Date: Wed, 24 Jun 2026 07:31:23 -0500 Subject: [PATCH 1/2] Support ESS-style comment symbol ##' as well --- R/cran.R | 18 +++++++++--------- R/parse.R | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/R/cran.R b/R/cran.R index 7201fe6..bed04ce 100644 --- a/R/cran.R +++ b/R/cran.R @@ -134,7 +134,7 @@ find_exports_without_examples <- function(lines) { line <- lines[i] # Start of doc block - if (grepl("^#'", line)) { + if (grepl("^##?'", line)) { if (!in_doc_block) { in_doc_block <- TRUE has_export <- FALSE @@ -143,12 +143,12 @@ find_exports_without_examples <- function(lines) { } # Check for export tag - if (grepl("^#'\\s*@export", line)) { + if (grepl("^##?'\\s*@export", line)) { has_export <- TRUE } # Check for examples tag - if (grepl("^#'\\s*@examples", line)) { + if (grepl("^##?'\\s*@examples", line)) { has_examples <- TRUE } } else if (in_doc_block) { @@ -183,14 +183,14 @@ find_dontrun_examples <- function(lines) { for (i in seq_along(lines)) { line <- lines[i] - if (grepl("^#'", line)) { + if (grepl("^##?'", line)) { if (!in_doc_block) { in_doc_block <- TRUE has_export <- FALSE has_dontrun <- FALSE } - if (grepl("^#'\\s*@export", line)) { + if (grepl("^##?'\\s*@export", line)) { has_export <- TRUE } @@ -226,25 +226,25 @@ find_long_example_lines <- function(lines, filename) { for (i in seq_along(lines)) { line <- lines[i] - if (!grepl("^#'", line)) { + if (!grepl("^##?'", line)) { in_examples <- FALSE next } - if (grepl("^#'\\s*@examples", line)) { + if (grepl("^##?'\\s*@examples", line)) { in_examples <- TRUE next } # Other tags end the examples block - if (grepl("^#'\\s*@[a-zA-Z]", line)) { + if (grepl("^##?'\\s*@[a-zA-Z]", line)) { in_examples <- FALSE next } if (in_examples) { # Strip the #' prefix to get the actual example content - content <- sub("^#'\\s?", "", line) + content <- sub("^##?'\\s?", "", line) if (nchar(content) > 100) { found <- c(found, paste0(filename, ":", i, " (", nchar(content), " chars)")) diff --git a/R/parse.R b/R/parse.R index 45f9721..4913706 100644 --- a/R/parse.R +++ b/R/parse.R @@ -14,7 +14,7 @@ parse_file <- function(file) { lines <- readLines(file, encoding = "UTF-8", warn = FALSE) # Find documentation blocks (consecutive #' lines) - doc_lines <- grep("^#'", lines) + doc_lines <- grep("^##?'", lines) if (length(doc_lines) == 0) { return(list()) @@ -43,8 +43,8 @@ parse_file <- function(file) { result <- list() for (block_lines in blocks) { - # Extract the comment text (strip #' prefix) - comment_text <- sub("^#'\\s?", "", lines[block_lines]) + # Extract the comment text (strip #' or ##' prefix) + comment_text <- sub("^##?'\\s?", "", lines[block_lines]) # Find the object definition after the block. Blank lines and plain # comments between the block and its object are allowed; another #' From f796a1f9cd6c08aa1efdd5fe7718a1ec449f9bc9 Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Wed, 24 Jun 2026 08:22:13 -0500 Subject: [PATCH 2/2] Extend ##' support to the orphaned-block check; add tests PR #27 relaxed the doc-line regexes to ^##?' but left the two ^\\s*#' checks in the orphaned-block detector (parse.R:55,60) unchanged, so a stray ##' block bled into the next object instead of warning (the #18 regression, for double-hash comments). Extend those two checks and add test_parse.R coverage for ##' parsing and ##' orphan detection. --- R/parse.R | 4 ++-- inst/tinytest/test_parse.R | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/R/parse.R b/R/parse.R index 4913706..716d304 100644 --- a/R/parse.R +++ b/R/parse.R @@ -52,12 +52,12 @@ parse_file <- function(file) { next_line <- max(block_lines) + 1 while (next_line <= length(lines) && - !grepl("^\\s*#'", lines[next_line]) && + !grepl("^\\s*##?'", lines[next_line]) && grepl("^\\s*(#|$)", lines[next_line])) { next_line <- next_line + 1 } - if (next_line > length(lines) || grepl("^\\s*#'", lines[next_line])) { + if (next_line > length(lines) || grepl("^\\s*##?'", lines[next_line])) { warning("Documentation block at ", basename(file), ":", block_lines[1], " is not followed by an object ", "definition; its tags are ignored.", call. = FALSE) diff --git a/inst/tinytest/test_parse.R b/inst/tinytest/test_parse.R index 533ee16..d1b01c8 100644 --- a/inst/tinytest/test_parse.R +++ b/inst/tinytest/test_parse.R @@ -110,3 +110,33 @@ expect_equal(length(big$formals$names), 30L) expect_equal(big$formals$names[30], "a30") expect_true(all(grepl("= NULL$", big$formals$usage))) unlink(tmp) + +# ESS-style ##' comments parse like #' (PR #27). +tmp <- tempfile(fileext = ".R") +writeLines(c( + "##' Add one", + "##' @param x A number.", + "##' @export", + "add_one <- function(x) x + 1" +), tmp) +blocks <- tinyrox:::parse_file(tmp) +expect_equal(length(blocks), 1) +expect_equal(blocks[[1]]$object, "add_one") +unlink(tmp) + +# The orphaned-block detector must recognise ##' too, so a stray ##' block +# can't bleed into the next object (the #18 regression for double-hash). +tmp <- tempfile(fileext = ".R") +writeLines(c( + "##' Orphaned block (its object was moved away)", + "##' @export", + "", + "##' Real", + "##' @export", + "real_fn <- function() 1" +), tmp) +expect_warning(blocks <- tinyrox:::parse_file(tmp), + pattern = "not followed by an object definition") +expect_equal(length(blocks), 1) +expect_equal(blocks[[1]]$object, "real_fn") +unlink(tmp)