Search code examples
rmetaprogrammingnon-standard-evaluation

subvert external function's `deparse(substitute())` without `eval`


I'd like to wrap around the checkmate library's qassert to check multiple variable's specification at a time. Importantly, I'd like assertion errors to still report the variable name that's out of spec.

So I create checkargs to loop through input arguments. But to get the variable passed on to qassert, I use the same code for each loop -- that ambigious code string gets used for for the error message instead of the problematic variable name.

qassert() (via vname()) is getting what to display in the assertion error like deparse(eval.parent(substitute(substitute(x))). Is there any way to box up get(var) such that that R will see e.g. 'x' on deparse instead?

At least one work around is eval(parse()). But something like checkargs(x="n', system('echo malicious',intern=T),'") has me hoping for an alternative.

checkargs <- function(...) {
   args<-list(...)
   for(var in names(args)) 
      checkmate::qassert(get(var,envir=parent.frame()),args[[var]])
      # scary string interpolation alternative
      #eval(parse(text=paste0("qassert(",var,",'",args[[var]], "')")),parent.frame())
} 

test_checkargs <- function(x, y) {checkargs(x='b',y='n'); print(y)}

# checkargs is working!
test_checkargs(T, 1)
# [1] 1

# but the error message isn't helpful. 
test_checkargs(1, 1)
# Error in checkargs(x = "b", y = "n") : 
#   Assertion on 'get(var, envir = parent.frame())' failed. Must be of class 'logical', not 'double'.
#
# want:
#   Assertion on 'x' failed. ...

Solution

  • substitute() with as.name seems to do the trick. This still uses eval but without string interpolation.

    eval(substitute(
          qassert(x,spec),
          list(x=as.name(var),
              spec=args[[var]])), 
         envir=parent.frame())