HenrikBengtsson / globals

🌐 R package: Identify Global Objects in R Expressions
https://globals.futureverse.org
28 stars 3 forks source link

Add custom codetools usage handlers for non-standard evaluation (NSE)? #12

Open HenrikBengtsson opened 8 years ago

HenrikBengtsson commented 8 years ago

The codetools package does indeed handle some non-standard evaluation (NSE) expressions, e.g. library() without quotes:

> codetools::findGlobals(function() library(tools))
[1] "library"

Note how tools is not identified as a global/unknown; this is because codetools knows how library() works.

This is done by (internal) usage-handler functions, e.g. for base::library() it sets up:

addCollectUsageHandler("library", "base", function(e, w) {
    w$enterGlobal("function", "library", e, w)
    if (length(e) > 2)
        for(a in dropMissings(e[-(1:2)])) walkCode(a, w)
})

This can be retrieved as:

> codetools:::collectUsageHandlers$library
function (e, w)
{
    w$enterGlobal("function", "library", e, w)
    if (length(e) > 2)
        for (a in dropMissings(e[-(1:2)])) walkCode(a, w)
}
<bytecode: 0x000000000b844fd0>
<environment: namespace:codetools>

There are several usage handlers set up this way:

> ls(codetools:::collectUsageHandlers)
 [1] "$"             "$<-"           "::"            ":::"
 [5] "@"             "@<-"           "{"             "~"
 [9] "<-"            "<<-"           "="             "assign"
[13] "binomial"      "bquote"        "data"          "detach"
[17] "expression"    "for"           "function"      "Gamma"
[21] "gaussian"      "if"            "library"       "local"
[25] "poisson"       "quasi"         "quasibinomial" "quasipoisson"
[29] "quote"         "Quote"         "require"       "substitute"
[33] "with"
HenrikBengtsson commented 8 years ago

Looking at the usage handler for base::with() we see that it is conditioned on a setting skipWith, which cannot be set via codetools::findGlobals():

addCollectUsageHandler("with", "base", function(e, w) {
    w$enterGlobal("function", "with", e, w)
    if (identical(w$skipWith, TRUE))
        walkCode(e[[2]], w)
    else collectUsageArgs(e, w)
})

This is why we get:

> codetools::findGlobals(function() with(df, sum(a)))
[1] "a"    "df"   "sum"  "with"

However, we can override this as:

codetools:::addCollectUsageHandler("with", "base", function(e, w) {
    w$enterGlobal("function", "with", e, w)
    walkCode(e[[2]], w)
})

such that only the first argument is checked:

> codetools::findGlobals(function() with(df, sum(a)))
[1] "df"   "with"
HenrikBengtsson commented 8 years ago

Importantly, addCollectUsageHandler() completely ignores the 2nd argument (where) right now:

# 'where' is ignored for now
addCollectUsageHandler <- function(v, where, fun)
    assign(v, fun, envir = collectUsageHandlers)