PHPCompatibility icon indicating copy to clipboard operation
PHPCompatibility copied to clipboard

PHP 8.3: Arbitrary static variable initializers

Open afilina opened this issue 2 months ago • 8 comments

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

References

afilina avatar Nov 06 '25 02:11 afilina

@jrfnl Looks pretty common and sniffable, IMO.

afilina avatar Nov 06 '25 02:11 afilina

@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:

  1. static variable declarations are not only available in function scope: https://3v4l.org/8TVIl
  2. static variable 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.

jrfnl avatar Nov 06 '25 13:11 jrfnl

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.

afilina avatar Nov 06 '25 14:11 afilina

@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.

afilina avatar Nov 06 '25 14:11 afilina

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 ?

jrfnl avatar Nov 06 '25 14:11 jrfnl

Does that make it clearer ?

I see now. Yes, that makes a lot more sense than what I initially understood.

afilina avatar Nov 06 '25 14:11 afilina

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 avatar Nov 06 '25 16:11 jrfnl

@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.

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)?

afilina avatar Nov 13 '25 02:11 afilina