Summary
Follow-up to #599 (fixed in #613). The demotion approach correctly keeps a promoted parameter's default in the demoted constructor parameter — but the proxy hook property now copies that default verbatim. For a PHP 8.1 new-in-initializer default that's invalid: new is allowed in parameter defaults but not in property initializers.
Reproduction
class Php81NewInInitializers
{
public function __construct(
private Collaborator $service = new Collaborator('ctor-default'),
) {}
}
With a property advice on $service, the proxy contains:
private \Ns\Collaborator $service = new Collaborator('ctor-default') {
get { ... }
PHP Fatal error: New expressions are not supported in this context
Reproduced by audit fixture tests/Stubs/Audit/Php81NewInInitializers.php (audit branch PR #597).
Proposed fix
In AbstractInterceptedPropertyGenerator (the default-from-Param logic added by #613): only carry the default onto the hook property when the expression is a valid property initializer — i.e. contains no New_ node (a small AST check; new is the only param-default construct excluded from property initializers). When skipped, the property stays uninitialized and still gets its value through the injected constructor assignment ($this->service = $service;), which the demotion already generates; the existing isInitialized() guard in the get hook handles the pre-construction window.
Environment
Summary
Follow-up to #599 (fixed in #613). The demotion approach correctly keeps a promoted parameter's default in the demoted constructor parameter — but the proxy hook property now copies that default verbatim. For a PHP 8.1
new-in-initializer default that's invalid:newis allowed in parameter defaults but not in property initializers.Reproduction
With a property advice on
$service, the proxy contains:Reproduced by audit fixture
tests/Stubs/Audit/Php81NewInInitializers.php(audit branch PR #597).Proposed fix
In
AbstractInterceptedPropertyGenerator(the default-from-Paramlogic added by #613): only carry the default onto the hook property when the expression is a valid property initializer — i.e. contains noNew_node (a small AST check;newis the only param-default construct excluded from property initializers). When skipped, the property stays uninitialized and still gets its value through the injected constructor assignment ($this->service = $service;), which the demotion already generates; the existingisInitialized()guard in the get hook handles the pre-construction window.Environment