PHP 8.3: Arbitrary static variable initializers
Related to https://github.com/PHPCompatibility/PHPCompatibility/issues/1589
Analysis
Summary of existing PHP 8.2 behavior based on the RFC:
// Constant expressions can be assigned in this context (exact meaning in Syntax Variations below).
function foo1() {
static $i = 1;
}
// Can redeclare, with the last taking precedence.
function foo2() {
static $i = 1;
static $i = 2;
}
// getStaticVariables() is able to get the literal value.
var_dump((new ReflectionFunction('foo2'))->getStaticVariables()['i']); // int(2)
Updated behavior in PHP 8.3:
// Allows arbitrary expressions (exact meaning in Syntax Variations below).
function foo1() {
static $i = min(1, 2);
}
// Re-declaring is now a fatal error.
function foo2() {
static $i = min(1, 2);
static $i = max(1, 2); // Fatal error: Duplicate declaration of static variable $i
}
// getStaticVariables() can get the expression's value or null if not initialized. However, since the above syntax
// is a fatal error in PHP 8.2, there is no observable difference, so we don't need to sniff getStaticVariables().
var_dump((new ReflectionFunction('foo2'))->getStaticVariables()['i']); // int(2)
Everything in the "Other semantics" section of the RFC has no impact on what we need to sniff. Either the syntax exists or it doesn't.
- Exceptions during initialization
- Destructor
- Recursion
Top 2000 Packages
Found quite a few usages in the top 2000 packages ("static $" in a function). See results.
Detection in PHP 8.2
-
function () { static $i; }Without assignment: valid. -
function () { static $i = 1; }Constant expression: valid. -
function () { static $a = 1; static $a = 2; }Duplicate declaration: valid -
function () { static $i = min(1, 2); }Arbitrary expression: error
Detection in PHP 8.3
-
function () { static $i; }Without assignment: valid. -
function () { static $i = 1; }Constant expression: valid. -
function () { static $a = 1; static $a = 2; }Duplicate declaration: error -
function () { static $i = min(1, 2); }Arbitrary expression: valid
Syntax Variations & Detectability
- Expressions valid in both versions
- Expressions valid only in PHP 8.3 - uncomment one at a time to test the fatal error in 8.2
- Static outside of functions
- Duplicate declaration - re-declaring is a fatal error since PHP 8.3.
- Interaction with ReflectionFunction::getStaticVariables() - when using syntax supported in both versions, no difference can be observed.
- More PHP 8.3 from @jrfnl
- Multi-declarations from @jrfnl
-
function () { static $i; }✅ Without assignment: can be handled by PHPCompatibility. -
function () { static $a = 1; static $a = 2; }✅ Duplicate declaration in the same function: can be handled by PHPCompatibility. -
function () { static $i = 1; }✅❌ Various expressions: I identified 52 expression types in the first 2 samples above. I will rely on @jrfnl's knowledge of the tool's capabilities.
References
@jrfnl Looks pretty common and sniffable, IMO.
@afilina Thanks for this initial research, but I have a feeling this needs a deeper layer of research still.
// Only literals can be assigned in this context (anything else is a fatal error).
In my experience, that's not true. Also see: https://3v4l.org/Bg8UT
- As of PHP 5.6 constant scalar expressions are allowed.
- As of PHP 8.1 new expressions are allowed.
- And I'm probably still missing some more changes which were made along the way.
Action: expand research on what is already allowed to prevent flagging false positives.
// Allows arbitrary expressions. function foo1() { static $i = min(1, 2); }
I believe clarification of what qualifies as an "arbitrary expression" is needed, again to prevent false positives as well as false negatives. The eventual sniff should not flag static variable declarations which are still invalid (as that doesn't constitute a change in behaviour for PHP).
I've come up with some additional examples of arbitrary expressions which are now allowed, but I've not found anything unsupported yet: https://3v4l.org/Hm4Zq
Action: see if there is any syntax left which isn't allowed when initializing static variables.
function () { static $a = 1; $a = 2; } Duplicate declaration: valid
👆🏻 This is an incorrect code sample as it will always be valid. The second $a variable is not declared statically.
Here are two variations of code patterns which do trigger the error: https://3v4l.org/SeBdi
function () { static $a = 1, $a = 2; };
function () { static $a = 1; static $a = 2; };
Action: fix the code sample.
And to make things even more interesting, I believe the following syntax variations also needs to be taken into account:
-
staticvariable declarations are not only available in function scope: https://3v4l.org/8TVIl -
staticvariable declarations can use multi-declarations:static $a = 10, $b = trim($foo), $c = $param++;
Action: eventually ensure that the test code includes code samples for this and that the sniff handles these correctly.
As for the sniff itself - I'd recommend using the AbstractInitialValueSniff as a basis (and overloading the register() method to only register T_STATIC) and for the sniff to be in the InitialValue category.
While the abstract adds a little overhead, it also already handles disregarding other uses of static (return type, modifier for properties, modifier for functions etc) and handles multi-declarations correctly, so that should help.
The eventual sniff should not flag static variable declarations which are still invalid
Other sniffs report new syntax on old versions as invalid. That's where I got the idea! Do we have guidelines for what sniffs should and shouldn't flag on pre-behavior-change versions? This seems a bit arbitrary and I'm not sure I'll be able to provide accurate assessments without more explicit guidelines.
@jrfnl I added your examples to the description above:
More PHP 8.3 from @jrfnl Static outside of function from @jrfnl Multi-declarations from @jrfnl
see if there is any syntax left which isn't allowed when initializing static variables
If you have more examples, I can add them here. That said, not sure what else I can research. These are basically just things one needs to know from experience are possible in PHP. I didn't see any of that in the top 2000 or in the docs on the static keyword. I can keep randomly poking around the internet, but not sure that it's a productive strategy. Creating the test cases should probably be a separate activity from this research, as it requires a completely different set of skills and knowledge.
Edit: you can throw a variety of syntaxes you come up with here, and I can sift through to classify behavior compatibility.
The eventual sniff should not flag static variable declarations which are still invalid
Other sniffs report new syntax on old versions as invalid. That's where I got the idea! Do we have guidelines for what sniffs should and shouldn't flag on pre-behavior-change versions? This seems a bit arbitrary and I'm not sure I'll be able to provide accurate assessments without more explicit guidelines.
I should clarify this better I think.
Let's take the PHP 8.4 change for class member access as an example:
// This is fine cross-version.
$a = (new MyClass())->bar;
// This is allowed since PHP 8.4 (not having the wrapping parentheses).
$a = new MyClass()->bar; // <= This is the only one which should be flagged.
// This is still not allowed as the syntax is ambiguous.
$a = new MyClass::BAR;
Now if PHP would introduce a new object operator, let's say: ::?:: (yeah, don't ask, it's just to give an example).
In that case, the sniff would treat that new object operator the same as all the other object operators as the object operator is not the subject of the sniff. The use of that new object operator will be reported by another sniff which flags new operators.
// This is fine cross-version.
$a = (new MyClass())::?::bar;
// This is allowed since PHP 8.4 (not having the wrapping parentheses).
$a = new MyClass()::?::bar; // <= This is the only one which should be flagged.
// This is still not allowed as the syntax is ambiguous.
$a = new MyClass::?::BAR;
@afilina Does that make it clearer ?
Does that make it clearer ?
I see now. Yes, that makes a lot more sense than what I initially understood.
Just read through the RFC myself and while I have a feeling we may well be able to detect recursion, I agree with your assessment that it doesn't add any value to do so.
If you have more examples, I can add them here. That said, not sure what else I can research. These are basically just things one needs to know from experience are possible in PHP. I didn't see any of that in the top 2000 or in the docs on the static keyword. I can keep randomly poking around the internet, but not sure that it's a productive strategy. Creating the test cases should probably be a separate activity from this research, as it requires a completely different set of skills and knowledge.
Edit: you can throw a variety of syntaxes you come up with here, and I can sift through to classify behavior compatibility.
Well, that's kind of why I suggested this may need more research - that research is about finding other syntax varieties/stress-testing the change in PHP 8.3 to ensure we understand it correctly.
It may be that "everything" (safe for parse errors) is allowed now as an initial value for static variables. It may be there are still some limitations. That's what I believe needs clarification.
Code searches will not come up with code samples of those limitations as those would still be parse/compile errors, so we wouldn't (shouldn't) find those in "live" code.
So, yes, this is largely trial and error based on experience and using 3v4l extensively to verify. Sometimes it also helps to go through the RFC list/migration guides for inspiration.
Example:
As of PHP 5.6, the initial value for a constant could be an array.
Now, the presumption would/could be that this would apply to all forms of constant declarations, but looking at the migration guide for 7.0, it becomes clear that the PHP 5.6 change only applied to constants declared using the const keyword and that constant declarations via define() were not supported in PHP 5.6. This was fixed in PHP 7.0.
Does that help ?
P.S.: asking me to come up with more syntax variety is asking me to do that research, so that's kind of counter to the idea of these research tickets ;-)
@jrfnl I revamped the examples by listing all expression types in php-parser, then deduplicating obviously similar ones, like || vs &&. This left me with 52, for which I created snippets and tested them. I avoided variables (obviously not supported) in the expressions as much as the parser allowed me, so we don't trip the wrong wire.
- Expressions valid in both versions
- Expressions valid only in PHP 8.3 - uncomment one at a time to test the fatal error in 8.2
- Static outside of functions - found two differences in behavior between the inside and outside functions (mid-section)
It's all in the body of the ticket.
Edit: the two differences outside of functions are actually a different issue entirely. Best illustrated with this example: https://3v4l.org/LJ8pG#v8.2.29 Have you seen something like this before when both are fatal errors (not a parse vs fatal)?