Unexpected Variable Assignment Using Function LocalMode Modern With Nested Closures In Lucee 5.3.2.77
Last week, I took a look at how the Function localmode
feature in Lucee makes it safer to dynamically render CFML templates. In response to that post, John Pansewicz told me that he ran into an issue using localmode
with nested Functions (for which he filed a ticket). It's one of those situations where the code is running "according to spec"; but, it's doing so in a way that is counter to what you might expect. As such, I thought it would be fun to dig a little deeper into JP's issue, along with some possible work-arounds, using Lucee 5.3.2.77.
In my previous localmode
explorations, I was explicitly setting localmode="modern"
on one of my Functions. But, this is not the only way to define the localmode
in Lucee. You can also define it in your Application.cfc
configuration such that your desired localmode
will be applied to all Functions in your CFML Application:
component
output = false
hint = "I define the application settings and event handlers."
{
this.name = hash( getCurrentTemplatePath() );
// By using "modern" localmode, any unscoped variable assignment will be applied to
// current Function's local scope (never to the Variable's scope).
this.localmode = "modern";
}
By setting this.localmode
, we are implicitly configuring the default behavior of every Function in the Application. Now, let's look at why this was causing an issue for JP:
<cfscript>
echo( "[A] Value: " & test() );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
public string function test() {
var value = "Original";
(() => {
// CAUTION: Because the entire Application is running in localmode "modern",
// this "unscoped assignment" will be applied to the current Local scope,
// which traps it inside of this closure.
value = "Changed";
})();
return( value );
}
</cfscript>
As you can see, all we're doing here is invoking a Function that turns around and invokes a Closure (as an IIFE - Immediately Invoked Function Expression). The Closure then tries to update a variable that is stored within the parent's local
scope. And, when we run this CFML code, we get the following output:
[A] Value: Original
As you can see, the value that is returned from the test()
function is unaffected by the Closure execution. That's because unscoped variable assignment within the Closure body is being applied to the local
scope of the Closure, not to the pre-existing variable in the parent Function. After all, Closures are Functions. As such, they abide by the Application-wide localmode
settings that we have in our Application.cfc
file.
Now, assuming that you can't turn off the localmode
setting in the Application.cfc
file, there are a few ways to get around this issue. The first way is to simply override the localmode
setting for the Closure itself:
<cfscript>
echo( "[B] Value: " & test() );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
public string function test() {
var value = "Original";
(function() localmode = "classic" { // <=== We are overriding LOCALMODE.
// Since the entire Application is running in localmode "modern", in order
// for this assignment to affect the local variable of the parent context,
// we have to override the LocalMode, setting it back to "classic". This
// will allow Lucee to apply the assignment to the "expected" variable.
value = "Changed";
})();
return( value );
}
</cfscript>
As you can see, in this case, we're switching from the Fat-arrow syntax to the traditional Function expression so that we can add the localmode="classic"
directive to the Function signature. Even though the default behavior of the ColdFusion Application is to run in "modern"
mode, we can still override that default behavior for specific Functions and Closures.
And, when we run this CFML code, we get the following output:
[B] Value: Changed
As you can see, the unscoped variable assignment in our Closure is getting applied to the local
scope of the parent Function - just as we would "expect" it to.
Another way to get around this issue is to scope the variable assignment. Remember, localmode
only applies to unscoped assignments; so, if we can put a scope in front of our assignment, then localmode
no longer comes into play:
<cfscript>
echo( "[C] Value: " & test() );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
public string function test() {
var value = "Original";
// Create an "alias" for the Local scope of the current function.
var context = local;
(() => {
// By using the "alias" of the parent function to define the variable
// assignment, it allows us to "scope" the assignment. So, even though
// "context" is just a reference to the parent function's Local scope, it's
// enough to bypass "modern" mode.
context.value = "Changed";
})();
return( value );
}
</cfscript>
As you can see, in order to define a "scope" for my variable assignment, I'm just creating an alias for the local
scope of the parent Function. Then, I can use this alias to bypass the localmode
behavior. And, when we run this CFML code, we get the following output:
[C] Value: Changed
As you can see, by using the context.
scope for the variable assignment within the Closure, I am able to affect the value of the original variable.
I think localmode
is a very interesting feature; and I certainly think that the "modern"
mode makes is easier to generate static HTML sites in ColdFusion. But, at this time, I wouldn't use it as an Application-wide setting. It doesn't quite dove-tail with the way that I traditionally think about Function execution. Instead, I'll leave my Lucee CFML applications in "classic"
mode; and then, enable "modern"
mode in the few places that I think it makes sense.
Want to use code from this post? Check out the license.
Reader Comments
Yes. I agree. I would probably only use:
In selective functions, like those that might contain:
Especially, if I am a new developer on a team, tasked with scoping spaghetti legacy functions with nested includes!
Trust me. I was asked to do this once, on a huge UDF library that had been cobbled together by dozens of different developers over many years. It was an utter nightmare of a job. I had to use 'varscoper', which uncovered over a thousand unscoped variables! I can't imagine what kind of leakage problems they had, before I fixed it!!!! The strange thing is that many of the memory leaks had been plugged by other unscoped variables, which meant that as I fixed one problem, it uncovered a new one somewhere else!
@Charles,
Yeah man, those unscoped variable leaks are the worst. It's created some really bad data leaks in my experience. The worst part is that they can be so hard to track down, especially when it "works on my machine" :D