Rubocop's Style/EvalWithLocation
cop dislikes the following:
eval "->(a) { a.date }"
^^^^^^^^^^^^^^^^^^^^^^^ Pass a binding, `__FILE__`, and `__LINE__` to `eval`.
Yes, I know that eval
is a security problem. The issue of security is out of scope for this question.
The Ruby documentation on binding says:
Objects of class
Binding
encapsulate the execution context at some particular place in the code and retain this context for future use. The variables, methods, value ofself
, and possibly an iterator block that can be accessed in this context are all retained. Binding objects can be created usingKernel#binding
, and are made available to the callback ofKernel#set_trace_func
and instances ofTracePoint
.
These binding objects can be passed as the second argument of the
Kernel#eval method
, establishing an environment for the evaluation.
The lambda being created does not need to access any variables in any scopes.
A quick and dirty binding to the scope where the eval
is invoked from would look like this:
sort_lambda = eval "->(a) { a.date }", self.binding, __FILE__, __LINE__
Ideally, a null binding (a binding without anything defined in it, nothing from self
, etc.) should be passed to this eval
instead.
How could this be done?
Not exactly, but you can approximate it.
Before I go further, I know you've already said this, but I want to emphasize it for future readers of this question as well. What I'm describing below is NOT a sandbox. This will NOT protect you from malicious users. If you pass user input to eval
, it can still do a lot of damage with the binding I show you below. Consult a cybersecurity expert before trying this in production.
Great, with that out of the way, let's move on. You can't really have an empty binding in Ruby. The Binding
class is sort of compile-time magic. Although the class proper only exposes a way to get local variables, it also captures any constant names (including class names) that are in scope at the time, as well as the current receiver object self
and all methods on self
that can be invoked from the point of execution. The problem with an empty binding is that Ruby is a lot like Smalltalk sometimes. Everything exists in one big world of Platonic ideals called "objects", and no Ruby code can truly run in isolation.
In fact, trying to do so is really just putting up obstacles and awkward goalposts. Think you can block me from accessing BasicObject
? If I have literally any object a
in Ruby, then a.class.ancestors.last
is BasicObject
. Using this technique, we can get any global class by simply having an instance of that class or a subclass. Once we have classes, we have modules, and once we have modules we have Kernel
, and at that point we have most of the Ruby built-in functionality.
Likewise, self
always exists. You can't get rid of it. It's a fundamental part of the Ruby object system, and it exists even in situations where you don't think it does (see this question of mine from awhile back, for instance). Every method or block of code in Ruby has a receiver, so the most you can do is try to limit the receiver to be as small an object as possible. One might think you want self
to be BasicObject
, but amusingly there's not really a way to do that either, since you can only get a binding if Kernel
is in scope, and BasicObject
doesn't include Kernel
. So at minimum, you're getting all of Kernel
. You might be able to skimp by somehow and use some subclass of BasicObject
that includes Kernel
, thereby avoiding other Object
methods, but that's likely to cause confusion down the road too.
All of this is to emphasize that a hypothetical null binding would really only make it slightly more complicated to get all of the global names, not impossible. And that's why it doesn't exist.
That being said, if your goal is to eliminate local variables and to try, you can get that easily by creating a binding inside of a module.
module F
module_function def get_binding
binding
end
end
sort_lambda = eval "->(a) { a.date }", F.get_binding
This binding will never have local variables, and the methods and constants it has access to are limited to those available in Kernel
or at the global scope. That's about as close to "null" as you're going to get in the complex nexus of interconnected types and names we call Ruby.