Wednesday 5 August 2015

ColdFusion: it's hard to maintain the CF docs when the language is so buggy

G'day:
I was baking an idea for an article about PHP 7's generator return values - which admittedly had got sidetracked in a thought experiment - but just as I pulled up a pew at the pub, someone said something about ColdFusion and scopes and wrong docs and I decided I need to look at it and fix the docs if they were wrong. FFS.

Here's a simple statement in the ColdFusion docs, regarding scopes ("About scopes"):

Evaluating unscoped variables

If you use a variable name without a scope prefix, ColdFusion checks the scopes in the following order to find the variable:
  1. Local (function-local, UDFs and CFCs only)
  2. Arguments
  3. Thread local (inside threads only)
  4. Query (not a true scope; variables in query loops)
  5. Thread
  6. Variables
  7. CGI
  8. Cffile
  9. URL
  10. Form
  11. Cookie
  12. Client

So that's all easy enough, and given it's a list of only a dozen items, one would think it would be easy enough to test to make sure the docs matched ColdFusion's actual behaviour. And that ColdFusion's behaviour is actually correct.



Wanna know why I greyed-out everything after the second entry? Because that's as far as I needed to go before going [coughbullsh!t]. Even when comparing the first two items, Adobe has got it wrong. Unfortunately in this case they've got the docs right, and ColdFusion itself is wrong.

The docs have this right... when referencing an unscoped variable within a function, local variables should trump arguments. They are - if you will - logically "closer" to the code than the arguments are, so should be found before arguments are. Makes sense.

However here's some over-emphasised test code:

// argsLocalVarUsingStatements.cfm

function withLocalAndRuntimeArg(){
    local.where = "local";

    writeDump(var={
        arguments = arguments.where,
        local = local.where,
        unscoped = where
    }, label="#getFunctionCalledName()#()");

    return where;
}
withLocalAndRuntimeArg(where="arg");
writeOutput("<hr>");


function withLocalAndDeclaredArg(where="default"){
    local.where = "local";

    writeDump(var={
        arguments = arguments.where,
        local = local.where,
        unscoped = where
    }, label="#getFunctionCalledName()#()");

    return where;
}
withLocalAndDeclaredArg(where="arg");
writeOutput("<hr>");


function withLocalAndDeclaredArgDefault(where="default"){
    local.where = "local";

    writeDump(var={
        arguments = arguments.where,
        local = local.where,
        unscoped = where
    }, label="#getFunctionCalledName()#()");

    return where;
}
withLocalAndDeclaredArgDefault();
writeOutput("<hr>");


function withVar(){
    var where = "var";

    writeDump(var={
        arguments = arguments.where,
        local = local.where,
        unscoped = where
    }, label="#getFunctionCalledName()#()");

    return where;
}
withVar(where="arg");
writeOutput("<hr>");


function withBothLocalSecond(){
    var where = "var";
    local.where = "local";

    writeDump(var={
        arguments = arguments.where,
        local = local.where,
        unscoped = where
    }, label="#getFunctionCalledName()#()");

    return where;
}

withBothLocalSecond(where="arg");
writeOutput("<hr>");


function withBothVarSecond(){
    local.where = "local";
    var where = "var";

    writeDump(var={
        arguments = arguments.where,
        local = local.where,
        unscoped = where
    }, label="#getFunctionCalledName()#()");

    return where;
}

withBothVarSecond(where="arg");
writeOutput("<hr>");

It's all fairly straight forward so I won't do a blow-by-blow, but it's basically testing various permutations of argument and local variable usage (via the local scope or the var statement), and then checking which of the candidates is found when one references the same variable without a scope.

And here's the output:



The correct result for all of these for the "unscoped" result should be one of "var" or "local" depending on which of the two gets set last in the given test (some of them set both a var and a local for good measure).

So ColdFusion ballses this up quite significantly. If there's a declared argument - ie: in the function signature, the argument is specified - then the argument takes precedence over the local variables. This is wrong. It's only when the argument is not explicitly defined, but is passed-in anyhow, that the correct look-up order is respected.

There's two things wrong here:

  1. the look-up order is wrong when arguments are declared (4031968);
  2. the behaviour is different when the arguments are not declared. Whether or not an argument is declared at compile time should have no impact on the runtime scope look-up! This is daft (4031972).

I'll update the docs shortly to indicate the bug.

For the sake of completeness, how does Lucee deal with all this? Well:



IE: correctly. It alwayys finds the local variable first. Good work, LAS.

Right. Back to my PHP generators....

--
Adam