Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve S3 method handling #1534

Merged
merged 12 commits into from
Nov 21, 2023
7 changes: 7 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ S3method(object_name,s3generic)
S3method(object_name,s3method)
S3method(object_name,s4generic)
S3method(object_name,s4method)
S3method(object_usage,"function")
S3method(object_usage,data)
S3method(object_usage,default)
S3method(object_usage,s3method)
S3method(object_usage,s4generic)
S3method(object_usage,s4method)
S3method(print,object)
S3method(print,rd)
S3method(print,rd_section)
Expand Down Expand Up @@ -162,6 +168,7 @@ S3method(roxy_tag_parse,roxy_tag_templateVar)
S3method(roxy_tag_parse,roxy_tag_title)
S3method(roxy_tag_parse,roxy_tag_usage)
S3method(roxy_tag_parse,roxy_tag_useDynLib)
S3method(roxy_tag_rd,default)
S3method(roxy_tag_rd,roxy_tag_.formals)
S3method(roxy_tag_rd,roxy_tag_.methods)
S3method(roxy_tag_rd,roxy_tag_.reexport)
Expand Down
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# roxygen2 (development version)

* The NAMESPACE roclet now reports if you have S3 methods that are missing
an `@export` tag. All S3 methods need to be `@export`ed (which confusingly
really registers the method) even if the generic is not. This avoids rare,
but hard to debug, problems (#1175).

* `@include` now gives an informative warning if you use a path that doesn't
exist (#1497).

Expand Down
6 changes: 0 additions & 6 deletions R/block.R
Original file line number Diff line number Diff line change
Expand Up @@ -289,9 +289,3 @@ parse_description <- function(tags) {

c(compact(list(title, description, details)), tags)
}

warn_roxy_block <- function(block, message, ...) {
message[[1]] <- paste0(link_to(block$file, block$line), ": ", message[[1]], ".")
names(message)[[1]] <- "x"
cli::cli_inform(message, ..., .envir = parent.frame())
}
19 changes: 19 additions & 0 deletions R/namespace.R
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ namespace_roclet <- function() {

#' @export
roclet_process.roclet_namespace <- function(x, blocks, env, base_path) {
warn_missing_s3_exports(blocks, env)

blocks_to_ns(blocks, env)
}

Expand Down Expand Up @@ -376,3 +378,20 @@ repeat_first_ignore_current <- function(name, x) {
repeat_first(name, x)
}
}

# missing s3 exports ------------------------------------------------------

warn_missing_s3_exports <- function(blocks, env) {
objs <- as.list(env)
funs <- Filter(is.function, objs)
methods <- funs[map_lgl(names(funs), is_s3_method, env = env)]

s3blocks <- blocks[map_lgl(blocks, block_has_tags, c("export", "exportS3method"))]
s3objects <- map(blocks, function(block) block$object$value)

undocumented <- methods[!methods %in% s3objects]
srcrefs <- map(undocumented, attr, "srcref")
messages <- paste0("S3 method `", names(undocumented) , "` needs @export or @exportS3method tag")
map2(undocumented, messages, warn_roxy_function)
}

2 changes: 1 addition & 1 deletion R/parse.R
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ env_file <- function(file) {
env <- new.env(parent = parent.env(globalenv()))
methods::setPackageName("roxygen_devtest", env)

sys.source(file, envir = env)
sys.source(file, envir = env, keep.source = TRUE)
env
}

Expand Down
6 changes: 6 additions & 0 deletions R/rd-usage.R
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,24 @@ object_usage <- function(x) {
UseMethod("object_usage")
}

#' @export
object_usage.default <- function(x) {
NULL
}

#' @export
object_usage.data <- function(x) {
rd(x$alias)
}

#' @export
object_usage.function <- function(x) {
function_usage(x$alias, formals(x$value), identity)
}

object_usage.s3generic <- object_usage.function

#' @export
object_usage.s3method <- function(x) {
method <- attr(x$value, "s3method")
s3method <- function(name) {
Expand All @@ -48,10 +52,12 @@ object_usage.s3method <- function(x) {
function_usage(method[1], formals(x$value), s3method)
}

#' @export
object_usage.s4generic <- function(x) {
function_usage(x$value@generic, formals(x$value), identity)
}

#' @export
object_usage.s4method <- function(x) {
s4method <- function(name) {
classes <- auto_backtick(as.character(x$value@defined))
Expand Down
1 change: 1 addition & 0 deletions R/rd.R
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ roxy_tag_rd <- function(x, base_path, env) {
UseMethod("roxy_tag_rd")
}

#' @export
roxy_tag_rd.default <- function(x, base_path, env) {
}

Expand Down
20 changes: 0 additions & 20 deletions R/tag.R
Original file line number Diff line number Diff line change
Expand Up @@ -116,23 +116,3 @@ roxy_tag_warning <- function(x, ...) {
warning(message, call. = FALSE, immediate. = TRUE)
NULL
}

#' @export
#' @rdname roxy_tag
warn_roxy_tag <- function(tag, message, ...) {
message[[1]] <- paste0(
link_to(tag$file, tag$line), ": {.strong @", tag$tag, "} ",
if (is.null(tag$raw)) ("(automatically generated) "),
message[[1]], "."
)
names(message)[[1]] <- "x"
cli::cli_inform(message, ..., .envir = parent.frame())
}

link_to <- function(file, line) {
cli::style_hyperlink(
paste0(basename(file), ":", line),
paste0("file://", file),
params = c(line = line, col = 1)
)
}
34 changes: 34 additions & 0 deletions R/utils-warn.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

#' @export
#' @rdname roxy_tag
warn_roxy_tag <- function(tag, message, parent = NULL, envir = parent.frame()) {
tag_name <- cli::format_inline("{.strong @{tag$tag}} ")
if (is.null(tag$raw)) tag_name <- paste(tag_name, "(automatically generated) ")
message[[1]] <- paste0(tag_name, message[[1]])

warn_roxy(tag$file, tag$line, message, parent = parent, envir = envir)
}

warn_roxy_block <- function(block, message, parent = NULL, envir = parent.frame()) {
warn_roxy(block$file, block$line, message, parent = parent, envir = envir)
}

warn_roxy_function <- function(fun, message, parent = NULL, envir = parent.frame()) {
srcref <- attr(fun, "srcref")
file <- attr(srcref, "srcfile")$filename
line <- as.vector(srcref)[[1]]

warn_roxy(file, line, message, parent = parent, envir = envir)
}

warn_roxy <- function(file, line, message, parent = NULL, envir = parent.frame()) {
link <- cli::style_hyperlink(
paste0(basename(file), ":", line),
paste0("file://", file),
params = c(line = line, col = 1)
)

message[[1]] <- paste0(link, ": ", message[[1]], ".")
names(message)[[1]] <- "x"
cli::cli_inform(message, parent = parent, .envir = envir)
}
4 changes: 2 additions & 2 deletions man/roxy_tag.Rd

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

11 changes: 11 additions & 0 deletions tests/testthat/_snaps/namespace.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,14 @@
Code
out <- roc_proc_text(namespace_roclet(), block)

# warns if S3 method not documented

Code
roc_proc_text(namespace_roclet(),
"\n foo <- function(x) UseMethod('foo')\n foo.numeric <- function(x) 1\n\n mean.myclass <- function(x) 2\n ")
Message
x <text>:5: S3 method `mean.myclass` needs @export or @exportS3method tag.
x <text>:3: S3 method `foo.numeric` needs @export or @exportS3method tag.
Output
character(0)

26 changes: 26 additions & 0 deletions tests/testthat/test-namespace.R
Original file line number Diff line number Diff line change
Expand Up @@ -419,3 +419,29 @@ test_that("Invalid imports throw a helpful error", {
expect_equal(out, "importFrom(AnUnknownUnavailablePackage,Unchecked)")

})


# warn_missing_s3_exports -------------------------------------------------

test_that("warns if S3 method not documented", {
expect_snapshot(
roc_proc_text(namespace_roclet(), "
foo <- function(x) UseMethod('foo')
foo.numeric <- function(x) 1

mean.myclass <- function(x) 2
"),
# Need to manually transform since the srcref is coming from the function;
# roc_proc_text() uses fake srcrefs for the blocks themselves
transform = function(x) gsub("file[a-z0-9]+", "<text>", x)
)
})

test_that("doesn't warn for potential false postives", {
roc <- namespace_roclet()
expect_no_warning({
roc_proc_text(roc, "foo.numeric <- function(x) 1")
roc_proc_text(roc, "is.numeric <- function(x) 1")
roc_proc_text(roc, "as.numeric <- function(x) 1")
})
})
Loading
Loading