Affects Version/s: None
Fix Version/s: 3.15.0
For edge cases (unresolved variables) if and unless have unexpected behavior.
Case in point:
- if => <x> should always be opposite to if => not(<x>) (even for edge cases).
- if => <x> should always be opposite to unless => <x> (even for edge cases).
- unless => <x> should always be equivalent to if => not(<x>) (even for edge cases).
- if => <x> should always be equivalent to unless => not(<x>) (even for edge cases).
- if => <x> should always be equivalent to if => not(not(<x>)) (even for edge cases).
- And so on...
Note that not() is not special, it's just a function call. The proper way to fix this problem, would be to evaluate all unexpanded strings to false / undefined on the last evaluation pass, and then evaluate all the possibly nested function calls to receive a consistent result.
Additionally, we should consider what cases should emit an error and what is considered normal and allowed.
Needless to say, this would be a big change to evaluation, not to be considered for a patch release, and has to be communicated clearly with users.
- Promises using if are only evaluated if the right side is evaluated to true
- If the right side is never evaluated, or evaluates to false, the promise is skipped.
- For cases where the right side is never evaluated, if and if => not() are both skipped (not opposite).
- unless evaluates a promise, unless the right hand side evaluates to true
- unless is a true opposite of if, it defaults to evaluate, so in all cases where if would skip, unless will evaluate.
- if the right side is never evaluated, or evaluates to false, the promise is evaluated
- For cases where the right side is never evaluated, unless and unless => not() are both evaluated (not opposite).
- unless is not equivalent to if => not(), since it defaults to evaluating the promise, rather than skipping. This means that unless has practical use outside of being a shorthand for if => not()
(Another benefit of this approach is that it's very close to how it currently works, implementing it would require a lot less work, and likely not break any existing policy).
This example best illustrates a case where it is very useful that if and if => not() are not opposites:
If filename is missing, I want both promises to be skipped. I don't want to pretend to know that the key is missing, if I didn't truly evaluate the right hand side, with properly expanded variables.
Please note that these examples don't resemble real life use cases,
they are provided to explain / specify how the 2 approaches work, in a rather technical way, in certain edge cases.
In normal cases (marked as "obvious") the outcome and behavior of both approaches are the same / similar.
Assuming literal_class is set,
$(undefined_var) is an undefined variable (doesn't expand),
and $(defined_var) expands to a class (or class expression) which is set / true.
|#||Policy||Approach A - Strict logic||Approach B - Usefully skip||Comments|
|1||if => "literal_class"||Evaluated||Evaluated||Obvious|
|2||if => "$(defined_var)"||Evaluated||Evaluated||Obvious|
|3||if => "$(undefined_var)"||Skipped||Skipped||Obvious|
|4||if => not("literal_class")||Skipped||Skipped||Obvious|
|5||if => not("$(defined_var)")||Skipped||Skipped||Obvious|
|6||if => not("$(undefined_var)")||Evaluated||Skipped|||
|7||if => strcmp("$(undefined_var)", "string")||Skipped||Skipped||Obvious|
|8||if => not(strcmp("$(undefined_var)", "string"))||Evaluated||Skipped|||
|9||unless => "literal_class"||Skipped||Skipped||Obvious|
|10||unless => "$(defined_var)"||Skipped||Skipped||Obvious|
|11||unless => "$(undefined_var)"||Evaluated||Evaluated|||
|12||unless => not("$(undefined_var)")||Skipped||Evaluated|||
|13||unless => strcmp("$(undefined_var)", "string")||Evaluated||Evaluated|||
|14||unless => not(strcmp("$(undefined_var)", "string"))||Skipped||Evaluated|||
Green outcomes are backwards compatible, red outcomes are breaking changes, yellow is new behavior (where it would previously error).
: A would evaluate, opposite of #3. B is how it works currently, skipped because of unexpanded variable.
: A would evaluate, opposite of #7. B is how it works currently, skipped because of unexpanded variable.
: Maybe obvious, but to spell it out: both would evaluate, because it should have opposite behavior to #3.
: A would skip, opposite to #11, and equivalent to #3. B would evaluate, opposite to #6.
: Although evaluation is different internally, here both cases should have opposite outcome to #7.
: A is skipped, opposite to #13. B is evaluated, opposite to #8.
As a side note: Many of these examples don't make that much sense, from the perspective of Approach B.
In approach B, when there are unresolved variables, it doesn't care about the function calls.
not() is just a function call.
Adding a not() or any other function call in these cases, doesn't make a difference.
That is the main difference, in approach A, function calls are evaluated when variables are unresolved,
meaning if => <x> is always opposite to if => not(<x>).
In Approach B this is normally true, but not in edge cases (when there are unresolved variables).