Skip to content

Commit c293ca0

Browse files
jayqiCopilot
andauthored
Update function parsing to handle externalptr and add unlistable fallback (#344)
Co-authored-by: Jay Qi <jayqi@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 3fb585d commit c293ca0

File tree

3 files changed

+103
-16
lines changed

3 files changed

+103
-16
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
## CHANGES
55

66
## BUGFIXES
7+
* Fixed runtime error when `FunctionReporter` extract edges from a function containing expressions of `externalptr` type. `FunctionReporter` will generally now ignore unknown expression types and instead log a warning. (#344)
78

89
# pkgnet 0.6.0
910
## NEW FEATURES

R/FunctionReporter.R

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
#' Function Interdependency Reporter
2-
#'
2+
#'
33
#' @description
44
#' This reporter looks at the network of interdependencies of its
55
#' defined functions. Measures of centrality from graph theory can indicate
66
#' which function is most important to a package. Combined with unit test
77
#' coverage information---also provided by this reporter--- it can be used
88
#' as a powerful tool to prioritize test writing.
9-
#'
9+
#'
1010
#' @details
1111
#' \subsection{R6 Method Support:}{
1212
#' R6 classes are supported, with their methods treated as functions by the
@@ -396,35 +396,37 @@ FunctionReporter <- R6::R6Class(
396396
.parse_function <- function (x) {
397397
# If expression x is not an atomic value or symbol (i.e., name of object) or
398398
# an environment pointer then we can break x up into list of components
399-
listable <- (!is.atomic(x) && !is.symbol(x) && !is.environment(x))
399+
listable <- .is_listable_expr(x)
400400
if (!is.list(x) && listable) {
401-
x <- as.list(x)
401+
result <- .try_as_list(x)
402+
x <- result$value
403+
listable <- result$listable
402404

403-
if (length(x) > 0){
405+
if (listable && length(x) > 0){
404406
# Check for expression of the form foo$bar
405407
# We still want to split it up because foo might be a function
406408
# but we want to get rid of bar, because it's a symbol in foo's namespace
407409
# and not a symbol that could be reliably matched to the package namespace
408410
if (identical(x[[1]], quote(`$`))) {
409411
x <- x[1:2]
410412
}
411-
} else {
413+
} else if (listable) {
412414
# make empty lists "not listable" so recursion stops
413-
listable <- FALSE
415+
listable <- FALSE
414416
}
415417
}
416418

417419

418420

419421
if (listable){
420-
422+
421423
# If do.call and first argument is string (atomic), covert to call
422424
if (length(x) >= 2){
423425
if (deparse(x[[1]])[1] == "do.call" & is.character(x[[2]])){
424426
x[[2]] <- parse(text=x[[2]])
425427
}
426428
}
427-
429+
428430
# Filter out atomic values because we don't care about them
429431
x <- Filter(f = Negate(is.atomic), x = x)
430432

@@ -439,6 +441,50 @@ FunctionReporter <- R6::R6Class(
439441
return(out)
440442
}
441443

444+
# [description] check if expression can be expanded into a list of components
445+
.is_listable_expr <- function(x) {
446+
# Atomic value
447+
if (is.atomic(x)){return(FALSE)}
448+
# Symbol (i.e., name of object)
449+
if (is.symbol(x)){return(FALSE)}
450+
# Environment
451+
if (is.environment(x)){return(FALSE)}
452+
# Raw external pointer to non-R memory/state (e.g., for C/C++ code)
453+
if (typeof(x) == "externalptr"){return(FALSE)}
454+
455+
return(TRUE)
456+
}
457+
458+
# [description] attempt to coerce an expression to a list, returning a list
459+
# containing the result as `value` and a `listable` flag; on
460+
# error, log a warning and return the original object as
461+
# unlistable.
462+
.try_as_list <- function(x) {
463+
tryCatch(
464+
list(
465+
value = as.list(x),
466+
listable = TRUE
467+
),
468+
error = function(e) {
469+
log_warn(sprintf(
470+
paste0(
471+
"Expression parsing: as.list() failed for ",
472+
"typeof=%s class=%s; treating as unlistable. ",
473+
"Please report to pkgnet maintainers in an issue. ",
474+
"Error: %s"
475+
),
476+
typeof(x),
477+
paste(class(x), collapse = ","),
478+
conditionMessage(e)
479+
))
480+
list(
481+
value = x,
482+
listable = FALSE
483+
)
484+
}
485+
)
486+
}
487+
442488
# [description] given an R6 class, returns a data.table
443489
# enumerating all of its public, active binding, and private methods
444490
#' @importFrom assertthat assert_that
@@ -648,13 +694,15 @@ FunctionReporter <- R6::R6Class(
648694

649695
# If expression x is not an atomic value or symbol (i.e., name of object) or
650696
# an environment pointer then we can break x up into list of components
651-
listable <- (!is.atomic(x) && !is.symbol(x) && !is.environment(x))
697+
listable <- .is_listable_expr(x)
652698

653699
# If it is not a list but listable...
654700
if (!is.list(x) && listable) {
655701
# Convert to list
656-
xList <- as.list(x)
657-
if (length(xList) > 0){
702+
result <- .try_as_list(x)
703+
xList <- result$value
704+
listable <- result$listable
705+
if (listable && length(xList) > 0){
658706
# Check if expression x is from _$_
659707
if (identical(xList[[1]], quote(`$`))) {
660708
# Check if expression x is of form self$foo, private$foo, or super$foo
@@ -673,14 +721,14 @@ FunctionReporter <- R6::R6Class(
673721
# Left Hand is not a _$_. Proceed as normal list.
674722
x <- xList
675723
}
676-
} else {
724+
} else if (listable) {
677725
# List is zero length. This might occur when encountering a "break" command.
678726
# Make empty list "non-listable" so recursion stops in following step.
679727
listable <- FALSE
680-
}
728+
}
681729
}
682730

683-
731+
684732

685733
if (listable){
686734
# Filter out atomic values because we don't care about them
@@ -695,4 +743,3 @@ FunctionReporter <- R6::R6Class(
695743
}
696744
return(out)
697745
}
698-

tests/testthat/test-FunctionReporter-class.R

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,45 @@ test_that(".parse_R6_expression correctly parses expressions containing a next s
340340
})
341341
})
342342

343+
test_that(".is_listable_expr treats external pointers as unlistable", {
344+
ptr <- new("externalptr")
345+
expect_false(pkgnet:::.is_listable_expr(ptr))
346+
})
347+
348+
test_that(".parse_function falls back when as.list fails on listable objects", {
349+
if (!methods::isClass("PkgnetNoListable")) {
350+
methods::setClass("PkgnetNoListable", slots = c(x = "numeric"))
351+
}
352+
obj <- methods::new("PkgnetNoListable", x = 1)
353+
354+
expect_true(pkgnet:::.is_listable_expr(obj))
355+
expect_error(as.list(obj))
356+
357+
result <- expect_warning(
358+
pkgnet:::.parse_function(obj),
359+
regexp = "Expression parsing: as\\.list\\(\\) failed"
360+
)
361+
expect_true(is.character(result))
362+
expect_length(result, 1)
363+
})
364+
365+
test_that(".parse_R6_expression falls back when as.list fails on listable objects", {
366+
if (!methods::isClass("PkgnetNoListable")) {
367+
methods::setClass("PkgnetNoListable", slots = c(x = "numeric"))
368+
}
369+
obj <- methods::new("PkgnetNoListable", x = 1)
370+
371+
expect_true(pkgnet:::.is_listable_expr(obj))
372+
expect_error(as.list(obj))
373+
374+
result <- expect_warning(
375+
pkgnet:::.parse_R6_expression(obj),
376+
regexp = "Expression parsing: as\\.list\\(\\) failed"
377+
)
378+
expect_true(is.character(result))
379+
expect_length(result, 1)
380+
})
381+
343382

344383
test_that("FunctionReporter R6 edge extraction handles case where all methods have the same number of dependencies", {
345384

0 commit comments

Comments
 (0)