使用静态代码分析检测“package_name::function_name()”

Detecting `package_name::function_name()` with static code analysis

我正在尝试深入研究 codetoolsCodeDepends 等静态代码分析包的内部结构,我的近期目标是了解如何检测写成 package_name::function_name() 的函数调用或 package_name:::function_name()。我本来想只使用 codetools 中的 findGlobals(),但这并不是那么简单。

要分析的示例函数:

f <- function(n){
  tmp <- digest::digest(n)
  stats::rnorm(n)
}

所需功能:

analyze_function(f)
## [1] "digest::digest" "stats::rnorm" 

尝试 codetools:

library(codetools)
f = function(n) stats::rnorm(n)
findGlobals(f, merge = FALSE)
## $functions
## [1] "::"
## 
## $variables
## character(0)

CodeDepends 更接近了,但我不确定我是否总是可以使用输出来将函数与包匹配。我正在寻找将 rnorm() 连接到 stats 并将 digest() 连接到 digest 的自动规则。

library(CodeDepends)
getInputs(body(f)
## An object of class "ScriptNodeInfo"
## Slot "files":
## character(0)
## 
## Slot "strings":
## character(0)
## 
## Slot "libraries":
## [1] "digest" "stats" 
## 
## Slot "inputs":
## [1] "n"
## 
## Slot "outputs":
## [1] "tmp"
## 
## Slot "updates":
## character(0)
## 
## Slot "functions":
##      {     :: digest  rnorm 
##     NA     NA     NA     NA 
## 
## Slot "removes":
## character(0)
## 
## Slot "nsevalVars":
## character(0)
## 
## Slot "sideEffects":
## character(0)
## 
## Slot "code":
## {
##     tmp <- digest::digest(n)
##     stats::rnorm(n)
## }

编辑 公平地说 CodeDepends,对于那些了解内部结构的人来说,它具有如此多的可定制性和强大功能。目前,我只是想集中精力了解收集器、处理程序、步行者等。显然,可以修改标准 :: 收集器以特别注意每个命名空间调用。现在,这是对类似事情的幼稚尝试。

col <- inputCollector(`::` = function(e, collector, ...){
  collector$call(paste0(e[[2]], "::", e[[3]]))
})
getInputs(quote(stats::rnorm(x)), collector = col)@functions
Browse[1]> getInputs(quote(stats::rnorm(x)), collector = col)@functions
stats::rnorm        rnorm 
          NA           NA 

如果你想从函数中提取命名空间函数,试试这样的方法

find_ns_functions <- function(f, found=c()) {
    if( is.function(f) ) {
        # function, begin search on body
        return(find_ns_functions(body(f), found))
    } else if (is.call(f) && deparse(f[[1]]) %in% c("::", ":::")) {
        found <- c(found, deparse(f))
    } else if (is.recursive(f)) {
        # compound object, iterate through sub-parts
        v <- lapply(as.list(f), find_ns_functions, found)
        found <- unique( c(found, unlist(v) ))        
    }
    found
}

我们可以用

进行测试
f <- function(n){
  tmp <- digest::digest(n)
  stats::rnorm(n)
}

find_ns_functions(f)
# [1] "digest::digest" "stats::rnorm" 

好的,所以这在以前使用 CodeDepends 是可能的,但比应该的要难一些。我刚刚将版本 0.5-4 提交给 github,现在这真的是 "easy"。本质上你只需要修改默认的 colonshandlers ("::" and/or ":::") 如下:

library(CodeDepends) # version >= 0.5-4
handler = function(e, collector, ..., iscall = FALSE) {
    collector$library(asVarName(e[[2]]))
    ## :: or ::: name, remove if you don't want to count those as functions called
    collector$call(asVarName(e[[1]])) 
    if(iscall)
        collector$call(deparse((e))) #whole expr ie stats::norm
    else
        collector$vars(deparse((e)), input=TRUE) #whole expr ie stats::norm
}

getInputs(quote(stats::rnorm(x,y,z)), collector = inputCollector("::" = handler))
getInputs(quote(lapply( 1:10, stats::rnorm)), collector = inputCollector("::" = handler))

上面的第一个 getInputs 调用给出了结果:

An object of class "ScriptNodeInfo"
Slot "files":
character(0)

Slot "strings":
character(0)

Slot "libraries":
[1] "stats"

Slot "inputs":
[1] "x" "y" "z"

Slot "outputs":
character(0)

Slot "updates":
character(0)

Slot "functions":
          :: stats::rnorm 
          NA           NA 

Slot "removes":
character(0)

Slot "nsevalVars":
character(0)

Slot "sideEffects":
character(0)

Slot "code":
stats::rnorm(x, y, z)

如我所愿。

这里要注意的一件事是我添加到冒号处理程序中的 iscall 参数。默认处理程序和 applyhandlerfactory 现在具有特殊的逻辑,因此当它们在被调用函数的情况下调用其中一个冒号处理程序时,它被设置为 TRUE。

我还没有对当 "stats::rnorm" 代替符号出现时会发生什么进行广泛的测试,特别是在计算依赖性时出现在输入槽中,但我希望所有这些都应该继续工作出色地。如果它不让我知道。

~G