Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/Analyser/ExprHandler/MethodCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use PHPStan\Node\Expr\PossiblyImpureCallExpr;
use PHPStan\Node\InvalidateExprNode;
use PHPStan\Reflection\Callables\SimpleImpurePoint;
use PHPStan\Reflection\ExtendedMethodReflection;
use PHPStan\Reflection\ExtendedParametersAcceptor;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
Expand All @@ -43,6 +44,7 @@
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;
use function array_key_exists;
use function array_map;
use function array_merge;
use function count;
Expand All @@ -56,6 +58,9 @@
final class MethodCallHandler implements ExprHandler
{

/** @var array<string, list<string>> */
private array $promotedParameterNamesCache = [];

public function __construct(
private EarlyTerminatingCallHelper $earlyTerminatingCallHelper,
private MethodCallReturnTypeHelper $methodCallReturnTypeHelper,
Expand Down Expand Up @@ -203,6 +208,19 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
}
}

if (
!$methodReflection->isStatic()
&& $scope->isInClass()
&& $scope->getClassReflection()->getName() === $methodReflection->getDeclaringClass()->getName()
) {
// $calledOnType is the receiver before the arguments were evaluated, which is
// the object the call goes to: an argument reassigning the receiver variable
// does not change who gets called.
foreach ($this->getPromotedParameterNames($methodReflection) as $propertyName) {
$scope = $scope->assignInitializedProperty($calledOnType, $propertyName);
}
}

} else {
$nodeScopeResolver->callNodeCallback($nodeCallback, new InvalidateExprNode($normalizedExpr->var), $scope, $storage);
$scope = $scope->invalidateExpression($normalizedExpr->var, true);
Expand Down Expand Up @@ -375,4 +393,34 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e
return $typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope);
}

/**
* Constructor promotion is not limited to methods called __construct: a trait
* constructor imported under a different name (use T { __construct as init; })
* keeps promoting its parameters, so calling it initializes those properties.
*
* @return list<string>
*/
private function getPromotedParameterNames(ExtendedMethodReflection $methodReflection): array
{
$declaringClass = $methodReflection->getDeclaringClass();
$cacheKey = sprintf('%s::%s', $declaringClass->getName(), $methodReflection->getName());
if (array_key_exists($cacheKey, $this->promotedParameterNamesCache)) {
return $this->promotedParameterNamesCache[$cacheKey];
}

$names = [];
$nativeClassReflection = $declaringClass->getNativeReflection();
if ($nativeClassReflection->hasMethod($methodReflection->getName())) {
foreach ($nativeClassReflection->getMethod($methodReflection->getName())->getParameters() as $parameter) {
if (!$parameter->isPromoted()) {
continue;
}

$names[] = $parameter->getName();
}
}

return $this->promotedParameterNamesCache[$cacheKey] = $names;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ protected function getRule(): Rule
'Bug10523\\MultipleWrites::init',
'Bug10523\\SingleWriteInConstructorCalledMethod::init',
'Bug12253\\PayloadWithAdditionalConstructor::setUp',
'Bug9789\\WithAdditionalConstructor::setUp',
],
),
);
Expand Down Expand Up @@ -222,6 +223,33 @@ public function testBug7649(): void
]);
}

#[RequiresPhp('>= 8.1.0')]
public function testBug9789(): void
{
$this->analyse([__DIR__ . '/data/bug-9789.php'], [
[
'Class Bug9789\InitNeverCalled has an uninitialized readonly property $value. Assign it in the constructor.',
6,
],
[
'Class Bug9789\InitOnAnotherObject has an uninitialized readonly property $value. Assign it in the constructor.',
6,
],
[
'Access to an uninitialized readonly property Bug9789\ReadBeforeInit::$value.',
51,
],
[
'Access to an uninitialized readonly property Bug9789\ConditionalInit::$value.',
69,
],
[
'Access to an uninitialized readonly property Bug9789\InitOnAnotherObject::$value.',
97,
],
]);
}

#[RequiresPhp('>= 8.1.0')]
public function testBug9577(): void
{
Expand Down
11 changes: 11 additions & 0 deletions tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -240,4 +240,15 @@ public function testBug14983(): void
$this->analyse([__DIR__ . '/data/bug-14983-uninitialized.php'], []);
}

#[RequiresPhp('>= 8.1.0')]
public function testBug9789(): void
{
$this->analyse([__DIR__ . '/data/bug-9789.php'], [
[
'Access to an uninitialized property Bug9789\NotReadOnlyReadBeforeInit::$value.',
130,
],
]);
}

}
154 changes: 154 additions & 0 deletions tests/PHPStan/Rules/Properties/data/bug-9789.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<?php declare(strict_types = 1); // lint >= 8.1

namespace Bug9789;

trait T {
public function __construct(public readonly string $value) {}
}

class C {

use T {
__construct as protected init;
}

public function __construct(string $value) {
$this->init($value);
if (!$this->isValid()) {
throw new \Exception();
}
}

private function isValid(): bool {
return !empty($this->value);
}
}

class ReadInConstructor
{

use T {
__construct as protected init;
}

public function __construct(string $value)
{
$this->init($value);
echo $this->value;
}

}

class ReadBeforeInit
{

use T {
__construct as protected init;
}

public function __construct(string $value)
{
echo $this->value;
$this->init($value);
}

}

class ConditionalInit
{

use T {
__construct as protected init;
}

public function __construct(string $value, bool $condition)
{
if ($condition) {
$this->init($value);
}
echo $this->value;
}

}

class InitNeverCalled
{

use T {
__construct as protected init;
}

public function __construct(public int $code)
{
}

}

class InitOnAnotherObject
{

use T {
__construct as public init;
}

public function __construct(string $value, self $other)
{
$other->init($value);
echo $this->value;
}

}

trait NotReadOnlyT {
public function __construct(public string $value) {}
}

class NotReadOnly
{

use NotReadOnlyT {
__construct as protected init;
}

public function __construct(string $value)
{
$this->init($value);
echo $this->value;
}

}

class NotReadOnlyReadBeforeInit
{

use NotReadOnlyT {
__construct as protected init;
}

public function __construct(string $value)
{
echo $this->value;
$this->init($value);
}

}

class WithAdditionalConstructor
{

use T {
__construct as protected init;
}

protected function setUp(): void
{
$this->init('x');
echo $this->readValue();
}

private function readValue(): string
{
return $this->value;
}

}
Loading