I keep running into issues with Lucee's handling o...
# lucee
d
I keep running into issues with Lucee's handling of isNull() and it's handling of scope precedence. In a nutshell, if a local variable ends up being null and you use isNull() to check the variable, it will use scope precedence to check for another variable with the same name, which means parameters passed via the URL/FORM scope can change the behavior. Here's a simple example of the behavior: https://trycf.com/gist/926837625eb97f5cd4c79258cffc2ed7/lucee5?theme=monokai In Lucee 4.x and all versions of ACF, the dump returns
null
, but in Lucee 5 (and Railo) it returns
test
. Since the
modelId
is declared within the
local
scope, I would expect the
isNull(modelId)
to return true if
var modelId
exists in the function. However, you have to explicitly specify the
local
scope to get it to work (i.e.
isNull(local.modelId)
). I opened up a Lucee issue about this in May 2021, but there's been not traction: https://luceeserver.atlassian.net/browse/LDEV-3523 Am I wrong? Is this the behavior everyone else would expect? I see it as a real problem because any UDF in which you're using
isNull()
to check a local variable could end up returning drastically different results than what you're expected.
r
My intuition says that the variable would be 'resolved' by looking through the scopes before passing to isNull, so this would be expected to me.
I would consider the way you describe lucee 4 to work to be the bug.
w
it's an interesting side effect of cf's 'check all scopes in the following order to resolve the variable before saying it's undefined'. however, i would argue the side effect you're seeing is due in no small part to how your function is written, specifically it has no arguments and is setting itself up for scope cascade resolution by referencing an unscoped variable reference. i think you'd have to write your own udf to wrap the isNull check and explicitly check local and any other scopes you may want to consider
That's obviously not a comprehensive list, just code that would be vulnerable to exploit this issue if a user puts a matching variable name in the URL or FORM scope (or it just happens to exist in another scope, like VARIABLES).
b
@dswitzer To me, this is just a reason why you should always be putting the scope along with your isNull() check.
Copy code
isNull(local.modelId)
and your problem is solved.
IMO Adobe's behavior is incorrect.
w
would have to agree. in exchange for terse code you open yourself up to the cascade, i don't see how it could be otherwise
b
It would be interesting to hear one of their engineers justify why they don't perform the standard CFML scope lookup, but it's probably dur to some crappy old code inside the ACF engine making assumptions about what you wanted.
I'll note, ColdBox has had a couple nasty bugs over the years from doing just this. Suddenly a random
url.something
variable was able to modify an
isNull()
code path deep inside the framework. Scoping our check was the fix.
w
the examples you posted just illustrate how common that tradeoff is made
does the issue still exist with modern scoping turned on in lucee?
b
I would suspect not
w
personally, i guess i'd just assume some scope cascade will occur if it's an unscoped reference, so i don't see it as a bug per se. but i can get behind a change that if it is an unscoped reference within a function that isNull should just check local and stop (unsure if arguments should also be checked in this case, but that's an argument for the cascade behavior to be left as is)
b
I just checked the wording of the modern scope setting real quick to confirm if it applies only to setting a variable or also to accessing a variable
Copy code
Defines how the local scope of a function is invoked when a variable with no scope definition is used.
-- Modern - the local scope is always invoked
-- Classic (CFML Default) - the local scope is only invoked when the key already exists in it
Hmm, so based on the wording of the setting, I would have assumed that even accessing
modelID
would have defaulted as well to
local.modelID
, but running the trycf code locally and toggling on that setting does not change the behavior. Lucee still finds the variable as
url.modelID
Honestly, the more I think about it, the more this seems to be part of null support, not scope lookup. The crux of the ticket is not that the scopes are searched, but that the value being null means its undefined.
Yep, if I turn on full null support, then the code returns null, which makes sense
👍 1
d
@bdw429s I will say in a very cursory glance, I found some Coldbox code that is affected by this issue. While scoping the check does solve the issue, based upon what you quoted in the docs, it's not the behavior I was expecting. That could because it's now how ACF works and that's what I've been working in for 25+ years. simple smile My expectation is that if a
local
variable is declared, then scope cascading should stop. I suspect there's a lot of CFML code that would be affected by this issue based on some searches on Github.
What's strange is that Lucee 4.5 changed the behavior from Railo to match ACF, but then in Lucee 5 it switched back again.
b
But you're forgetting how CFML treats nulls. A null values doesn't exist. So it makes perfect sense that it would be ignored.
Furthermore, dumping out the variable proves the point
Copy code
writeDUmp(modelId)
That code "finds" the url variable, as it should based on CFML's rules of when vars exist and when to hunt scopes
Therefore, is isn't null!
d
The behavior doesn't match ACF and it appears a lot of devs aren't expecting the behavior.
b
ACF is wrong ¯\_(ツ)_/¯
When you type
modelID
, you're thinking in your head, "I mean the one in the local scope", but that's not what the CF engine is thinking
It's thinking • null support is partial (so null vars don't exist) • and hunt scopes until you find an existing value and based on that,
modelID
is not null and has a value
At the end of the day, the code is sloppy and made assumptions
if you wanted to ask CF whether
local.modelID
was null, then your could should read
Copy code
writeDUmp(local.modelId)
right now the code
Copy code
writeDUmp(modelId)
is just challenging CF to go find a non-null value in any scope in the lookup order
d
I guess I disagree. var x = nullValue(); isNull(x); I would expect that to be
true
. The variable
x
was scoped and is
null
. It will return
true
unless
x
exists in a different scope. I think at a bare minimum the Lucee docs for isNull() should state you should explicitly declare the
local
scope if you do not want cascading scope check to occur. https://docs.lucee.org/reference/functions/isnull.html
b
The fact that Adobe has a bug where it doesn't keep looking is undocumented at best, and a lucky break.
d
The fact that ColdBox, FW/1, DI/1, etc all could be bitten by this issue is concerning.
b
The variable 
x
 was scoped
No it isn't scoped. I'm unclear why you would say that.
Copy code
isNull(x);
☝️ That is not a scoped variable. The connection you have to the variable of the same name in the
local
scope is coincidental
d
var x
scopes to the
local
scope.
b
The fact that ColdBox, FW/1, DI/1, etc all could be bitten by this issue is concerning.
Programming is hard. We have to be careful. Just because people have run afoul of a language design, doesn't necessarily mean it's bad. This only happened a couple times in ColdBox, and we fixed it. Case close. We learned our lesson.
The setting of
local.x
is scoped, but the access of
x
is not scoped
In your mind, it's the same variable, but to the CF engine it's not
d
Unless you fixed it within the last few minutes, there are still issues in the Coldbox Github (see my links above)
b
x
is not an absolute canonical reference to a specific variable. it's a guess for the CF engine to go track down for you
Looking at the links above, those are recent additions to ColdBox. I shall go yell at @lmajano about them and enter a ticket to fix them.
d
Are argument comes down to the definition of
defined
. When I see
var x = nullValue();
the variable
x
is defined, but the value is
null
. The
isNull()
function isn't supposed to be checking if a variable has value, but if the value is
null
. So the fact that
var x
exists and is
null
it should return
true
.
@bdw429s That kind of gets to my point. This does not seem to be the expected behavior by many.
b
I think at a bare minimum the Lucee docs for isNull() should state you should explicitly declare the 
local
 scope if you do not want cascading scope check to occur.
Scope hunting is a core part of CFML and Lucee is the most consistent engine. Why should Lucee tell us, "Oh, but the way, we're still following the spec here"? That should be the expectation, not the exception. Instead, I think the Adobe docs should be the ones with the note saying, "oh by the way, we're throwing the spec under the bus here and ignoring it in some cases"
d
And that's my fear. I fixed the issues now, but I can see them cropping up again.
b
This does not seem to be the expected behavior by many.
Dunno what to tell you. It's expected to me and it makes perfect since given the rules of CFML.
d
But this isn't scope hunting. This is check a variable to see if it's
null
. As soon as it find a variable which is defined and is
null
, then it should return
true
. It should check
local.x
first, see it exists and is
null
and then return
true
.
b
This is the nature of CF's loose scoping mechanics and questionable decisions about nulls not existing. I don't always care for the monster if created, but Lucee is behaving quite consistently from my perspective.
No, again, null doesn't exist
So when it finds something that's null, that's not a finding. So it moves on
Lucee is the only engine being consistent. Adobe is deciding when it wants to follow the rules
d
But alas,
null
does exist
It's just not well supported.
b
A null variable still doesn't exist unless you've enabled full null support
It exists as a concept you can check for, but not as a first class value you can define or pass around
d
The function isn't
isNotNull()
, it's
isNull()
. That should return
true
for a variable which contains a Java null value.
But you can define it and you can pass it around. Granted, ACF doesn't make it easy, but you can (and our code does).
b
I think it would be wrong for isNUll() to return true yet still be able to dump out the variable and get a value! That would make no sense
If isNull() returns true, I should get an error if I try to dump it that it doesn't exist, but that's not the case
d
My example code wouldn't do that. It would be printing
null
.
b
you can pass it around
How? A null variable cannot be passed directly or you will receive an error as shown here https://trycf.com/gist/7a1198e6e5fddfcc885e9a081e2c96e5/lucee5?theme=monokai
It's not straightforward, but you can pass
null
around.
b
My example code wouldn't do that. It would be printing 
null
.
Lol, take this example for instance. You want CFML to work this way, which would be bonkers
Copy code
if( isNull(modelId) ) {
	    writeDump( modelId )
	}
☝️ In a well-designed language in which nulls don't exist, that should never be able to run without erroring.
If CFML can go out and "find"
modelID
for you and get a non-null value, then isnull() also shuould not return true.
Also, remember, the argument to the
isNull()
BIF are evaluated before the function is even called
So
writeDump()
and
isNull()
are receiving the same argument!
d
There should be a difference between isNull() and isDefined()
b
Lucee resolves
modelID
in the same manner regardless of what it's going to do with the value afterward. The fact that it's being passed to
isNUll()
does not change how the engine resolves it.
There's not a "get this value, but get it differently if I'm about to check the null-ness of it" behavior
d
And that's where I see the problem. Because the purpose of the function is to check if a variable's value is
null
. If there is a variable in the scope chain which is
null
, then it should return
true
. Not just ignore it. That's what ACF does.
We just see it differently and that's okay.
b
There should be a difference between isNull() and isDefined()
This is perhaps another discussion entirely... The main difference here being that the incoming variable is encased in a string so the indefined method is allowed to resolve it internally, whereas the arg to isNull is evaluated before isNull is ever called
Even though this code would error in some cases, think of it to help understand the order of operations
Copy code
var thingToTest = modelID;
isNull( thingToTest );
the CF engine goes and finds
modelID
first and once it's found it, then it passes the result to the
isNull()
function. What you're wanting CF to do is to change how the first line works if the second line is going to use the isNull() method to check the value.
But the resolution of a variable is the same in all circumstances. Whether you're checking it for null, outputting it, etc, etc
Only in ACF is it inconsistent 😕
d
Would you agree that the Lucee docs for
isNull()
should talk about explicitly scoping variables in a UDF?
b
No, not really
That's general knowledge of how CF works
Should every BIF that accepts an input talk about how to scope variables??
I mean, the issue could literally exist in any BIF if you think about it
Or any place you pass a variable with ambiguous scoping
The Lucee docs should defo have a page that covers how nulls are handled and how scopes are hunted for sure. But that doesn't need to be reproduced on every page that handles variables!
d
Thanks for taking the time to discuss your opinion on the matter. I appreciate it.
I wasn't saying on every page, just explicitly in the docs for
isNull()
since the behavior is different from ACF.
Since @lmajano was bitten by it (as well as other popular frameworks), seemed like it might be worth explicitly documenting the difference in behavior, especially since Lucee used to always state that incompatibles were considered bugs.
b
Here's a bit simpler example that shows the basic issue Adobe CF has
Copy code
url.modelId = "test";    
variables.modelID = javaCast('null','');

// If modelID can be found having a value
writeDump( modelId )
// Then this must return false
writeDump( isNull(modelId) )
At the end of the day, I never am against clarifying information in the docs. But what really needs to happen is to get a ticket entered (if one doesn't already exist) for Adobe to fix their bugs.
d
My recollection was that
isNull()
was created just for this purpose, because Java libs often return
null
values and they wanted a way to check if a local function contained a null to avoid scope bleeds.
I seem to recall that discussion along time ago in one of the early MX/7 alpha/betas (whenever
isNull()
was introduced).
b
Sort of. isNUll was added in CF9 with the addition of Hibernate since Hibernate returned null values for not found entities and CFML needed a way to detect if a variable was null.
It really had nothing to do with "scope bleeding" or whatnot.
And just like isDefined(), you need to be explicit with scopes or you'll get unexpected results when scope hunting happens.
d
But the purpose of isDefined() is different. For example if you take your code example and add the isDefined() check, it return
yes
: url.modelId = "test"; variables.modelID = javaCast('null',''); // If modelID has a value writeDump( modelId ); // Then this must return false writeDump( isNull(modelId) ); writeDump( isDefined("modelId") );
b
It's worth noting, Lucee has always treated
isNull()
and
isDefined()
nearly the same, which I thought made sense.
d
@bdw429s, that's my point about isNull() w/the ORM. It's supposed to explicitly check if a value is
null
.
b
But the purpose of isDefined() is different
How? Why? By definition in CFML, null means not defined. That literally ties the definition of defined-ness to null-ness. Why would they be different?
It's supposed to explicitly check if a value is 
null
And that's what it does. The issue here is what you are describing as a 'variable'. You're still not seeing the ambiguity between
x
and
local.x
d
We're just going round and round. We disagree. That's okay. I could be wrong. That's okay too. That's why I asked the question, because the ticket has no traction.
b
When you call a BIF, any BIF.
Copy code
myBIF( myVar )
the
myVar
is first found using the CFML rules of scope hunting and what makes a variable defined. Then, and only then, is the resulting value passed to
myBIF()
d
I'm seeing what you're saying. I just disagree. As soon as the scope checking finds that
local.x
is
null
it should stop and return
true
.
It doesn't do that today.
b
As soon as the scope checking finds that 
local.x
 is 
null
 it should stop and return 
true
.
According to whom? Null mean not-defined in CFML and has since I can remember. Why should that change now?
d
Anyway, thanks for taking the time to respond!
b
You're just making up new rules for CFML, lol
d
I guess I've just been playing by the ACF rules too long. 😄
b
it should stop and return 
true
.
☝️ And again, you're conflating the action of the isNUll method with the resolution of the variable
These are two. separate. opertions.
The resolution of the varaible does not take into account that it will be checking it for null. That is a separate concern
The variable is resolved first fully, before any sort of isnull check is performed
You're trying to squash them all into some single operation in your head where the scope hunting is interspersed with "is this one null?" checks
With your change,
Copy code
writeDump( modelId )
would also dump null, which would break CFML
I think it's great that we disagree and I'm enjoying the discussion 🙂 but at some point we have to agree on common ground that is the rules and definitions of what CFML is and what CFML isn't. These are not our opinions and they are non-negotiable. Once we do that, there is only one behavior that is consistent with CFML's defined behaviors
At the end of the day, it's not that we felt differently about it-- it's that Lucee follows the well-known rules of how CFML behaves and Adobe does not here.
Here is an even more alarming example of how inconsistent Adobe is
Copy code
url.foo = "I have a value, I am not null!";
variables.foo = javaCast('null','');

thingtoTest = foo;

writeDump( isNull(foo) );
writeDump( isNull(thingtoTest) );
Both of those isnull checks should return the same thing as they are looking at the same value! Yet Adobe is changing the behavior based on which variable it receives which is wrong.
z
cfml is quirky with structs, a struct value which is null, structKeyExists says yes, but then it errors
b
I've always hated that one.
I assume it says yes there because you can iterate over the struct keys and see it.
z
hmmm, so i figured that should be documented on lucee docs, but now I'm confused https://trycf.com/gist/9fc260fc781d3361b623fe51682cadc2/lucee5?theme=monokai
🙃
b
I guess it was only
structKeyList()
that showed the null keys.
We'll see if Adobe moves on this at all https://tracker.adobe.com/#/view/CF-4212777
z
but not b4 echo() and dump() and updating their loader revision (lol)
🤣 1
b
d
@bdw429s I'm glad something good came out (re: DBOX-1097) of this thread (for those willing to keep up with the it!)
👍 1
b
Adobe also moved my ticket to "bug verified" and "to fix" 🙂
But that can also just be the sign of a ticket about to language in the backlog for the next 10 years, lol
a
My take on this: Historically, before "null" was a concept that CFML really acknowledged, it conflated "define`d-ness" with "null-ness". It considered that a variable was not even defined if it didn't have a value. This was never correct, but it it what it is.
Copy code
myVariableWithNullValue = javaCast("null", "")
myVariableWithNullValue
very much is defined there. You can see it. Being defined. It just has "no value" (in the context of CFML not understanding null). This was always two different things, and CFML messed it up. --- Once CFML acknowledged null is actually a thing, part of that ought to have been to revise the behaviour of the distinction between "define`d-ness" and "null-ness". This would necessarily be a breaking change. So be it: it's the price the language pays for having previously poor design. Using pseudocode (not intended to be pseudoCFML, don't read anything into the similarity between some CFML functions):
Copy code
// myProgram.notCfml

isDefined(notEverMentionedPreviously) // false
isNull(notEverMentionedPreviously) // error


var definedButNoValue
isDefined(definedButNoValue) // true
isNull(definedButNoValue) // true


var definedWithExplicitNullValue = null
isDefined(definedWithExplicitNullValue) // true
isNull(definedWithExplicitNullValue) // true


var definedWithExplicitNonNullValue = "some value"
isDefined(definedWithExplicitNonNullValue) // true
isNull(definedWithExplicitNonNullValue) // false
To that end, I'm with @dswitzer: scope-look-up should not come in to play here. In his example
modelId
is defined in that function, so there's no need to go looking elsewhere for it. It is irrelevant what its value is; that should not come into it. I cannot test on Lucee5 ATM cos it seems to be crocked on trycf, but this also seems like a regression in Lucee5, if the Lucee4 behaviour was a) different; b) in-keeping with CF's behaviour; c) which is behaviour that makes more sense anyhow. Why did the behaviour change (sorry if this was explained, it's a long thread and I skimmed)?
b
If Adobe were to "go back to the drawing" board and re-think how they treated nulls and scope lookups, I'd have no issues with your proposals above. Your suggested behavior however, would not be consistent with the current design of CFML. As much fun as it is to dream about what CF could have become in an alternate universe, I'd just be happy getting Adobe to minimally be consistent with their own existing design decisions 🙂
I wouldn't mind Adobe trying to push full null support as the default of the engine in future releases, which would solve some of this. I have no idea how much people would complain about that tho. ColdBox has made the changes to support this years ago, and it really was comprised mostly of replacing
structKeyExists()
checks with
isNull()
. The ability to override the engine's null behavior at a CFC level would at least help people juggle 3rd party libs that didn't support it in the mean time 🤔
a
Eeek, @bdw429s... I am having a horrible nightmare to the effect of "ColdFusion hasn't actually implemented full null support yet, has it?". I overlooked that.
b
Yeah, they added it though I've never really played with it. Not on ACF anyway.
So I don't know how closely they matched Lucee on it
I do know for lucee it was a compiler setting at first (couldn't be modified in application.cfc, only in the admin UI). But Adobe had an application,cfc flag for it so Lucee had to go back and figure out how to add that
I think, generally speaking, Adobe's user base is far less likely than Lucee's user base to play with settings like that. That's my take anyway
I can say when ColdBox has had issues with full null support, it's always a Lucee user reporting it, never an Adobe user.
a

https://i2.paste.pics/4ee9ac690b197275bb65530c9c0aa2ad.png

b
It's quite difficult for most people to try out the setting if they rely on any 3rd part libs at all, most of which don't support it. And without the option to enable it at a CFC level, it's still mostly all-or-nothing even at the application level.
a
Ah maybe @abram has it switched off.
b
Yeah, trycf doesn't have it on for either Lucee or Adobe
Which is sort of too bad-- again there's no run time switch you can include in your code
actually, Lucee DOES let you switch it on and off at runtime, but most people don't know about it-- and it will likely screw with anyone hitting the same trycf node, lol
Copy code
application action="update" NULLSupport=true;
// test stuff with null support on
application action="update" NULLSupport=false;
of course, that's still toggling the entire application, so it's only useful in a test case-- you wouldn't want to do that on a real app
Adobe still has yet to add the concept of updating the application settings at runtime, despite it being the year of our Lord 2022, so you have no way to do it in ACF-land 😕
a
fuck sake. both vendors should have just pissed or got off the pot with this. "Breaking change in [insert next major version number here]: null handling has been improved but it is a breaking change" This switch it on / switch it off thing is just ballocks. It basically means it's unusable in a shared-code situation.
b
Correct
a
What do you guys say "we don't support it switched on, so just don't"? Or have you wasted time and effort making it work in all cases?
b
Who is "you guys" in that sentence?
a
*Box
b
We support it in ColdBox, to the best we can (we don't tend to use it ourselves so rely on bug reports if we've broken it).
That said, you'll likley find a few modules we've written were we neglected to test it
We'd have to double all our Travis-CI tests to run a second server to test it on.
Demand has been fairly small over the years, but it was enough for us to try and support it.
a
I think you should just post in the readme.md "not testing with null support switched on. Caveat emptor", and move on.
b
We don't use the
null
keyword anyhere since that's not backwards compat, but we just don't rely on things like
strcutKeyExists()
in our code, opting to use
!isnull()
since that works in both cases
2
The issues would normally crop up in code like
Copy code
var results = methodCall();
if( structKeyExists( local, 'results' ) ) {
  return results;
}
That sort of code comes up a lot in the framework. With null support on, the key will always exist, so we'll use
Copy code
var results = methodCall();
if( !isNull( local.results ) ) {
  return results;
}
instead now
1
a
Not such a hardship then.