Skip to content
Closed
Show file tree
Hide file tree
Changes from 9 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
16 changes: 15 additions & 1 deletion src/Analyser/ExprHandler/MethodCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use PHPStan\Analyser\NodeScopeResolver;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Node\Expr\PropertyInitializationExpr;
use PHPStan\Node\Expr\PossiblyImpureCallExpr;
use PHPStan\Node\InvalidateExprNode;
use PHPStan\Reflection\Callables\SimpleImpurePoint;
Expand Down Expand Up @@ -204,11 +205,24 @@
}
if (
$scope->isInClass()
&& !$scope->isInAnonymousFunction()
&& $scope->getClassReflection()->getName() === $methodReflection->getDeclaringClass()->getName()
&& ($scope->getFunctionName() !== null && strtolower($scope->getFunctionName()) === '__construct')
&& TypeUtils::findThisType($calledOnType) !== null
) {
$calledMethodScope = $nodeScopeResolver->processCalledMethod($methodReflection);
$stackName = sprintf('%s::%s', $methodReflection->getDeclaringClass()->getName(), $methodReflection->getName());
$uninitializedProperties = [];
foreach ($scope->getClassReflection()->getNativeReflection()->getProperties() as $nativeProperty) {
if ($nativeProperty->hasDefaultValue() || $nativeProperty->isStatic()) {
continue;
}
if (!$scope->hasExpressionType(new PropertyInitializationExpr($nativeProperty->getName()))->yes()) {

Check warning on line 219 in src/Analyser/ExprHandler/MethodCallHandler.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ($nativeProperty->hasDefaultValue() || $nativeProperty->isStatic()) { continue; } - if (!$scope->hasExpressionType(new PropertyInitializationExpr($nativeProperty->getName()))->yes()) { + if ($scope->hasExpressionType(new PropertyInitializationExpr($nativeProperty->getName()))->no()) { $uninitializedProperties[$nativeProperty->getName()] = true; } }

Check warning on line 219 in src/Analyser/ExprHandler/MethodCallHandler.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ($nativeProperty->hasDefaultValue() || $nativeProperty->isStatic()) { continue; } - if (!$scope->hasExpressionType(new PropertyInitializationExpr($nativeProperty->getName()))->yes()) { + if ($scope->hasExpressionType(new PropertyInitializationExpr($nativeProperty->getName()))->no()) { $uninitializedProperties[$nativeProperty->getName()] = true; } }
$uninitializedProperties[$nativeProperty->getName()] = true;
}
}
$nodeScopeResolver->registerCalledMethodUninitializedProperties($stackName, $uninitializedProperties);

$calledMethodScope = $nodeScopeResolver->processCalledMethod($methodReflection);
if ($calledMethodScope !== null) {
$scope = $scope->mergeInitializedProperties($calledMethodScope);
return new ExpressionResult(
Expand Down
13 changes: 12 additions & 1 deletion src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -1574,7 +1574,7 @@ public function enterPropertyHook(

$realParameterTypes = $this->getRealParameterTypes($hook);

return $this->enterFunctionLike(
$scope = $this->enterFunctionLike(
new PhpMethodFromParserNodeReflection(
$this->getClassReflection(),
$hook,
Expand Down Expand Up @@ -1606,6 +1606,17 @@ public function enterPropertyHook(
),
true,
);

if ($hookName === 'set') {
foreach ($this->getClassReflection()->getNativeReflection()->getProperties() as $nativeProperty) {
if ($nativeProperty->hasDefaultValue()) {
continue;
}
$scope = $scope->invalidateExpression(new PropertyInitializationExpr($nativeProperty->getName()));
}
}

return $scope;
}

private function transformStaticType(Type $type): Type
Expand Down
24 changes: 24 additions & 0 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ class NodeScopeResolver
/** @var array<string, MutatingScope|null> */
private array $calledMethodResults = [];

/** @var array<string, array<string, true>> */
private array $calledMethodUninitializedProperties = [];

/**
* @param string[][] $earlyTerminatingMethodCalls className(string) => methods(string[])
* @param array<int, string> $earlyTerminatingFunctionCalls
Expand Down Expand Up @@ -751,6 +754,13 @@ public function processStmtNode(
);
$methodScope = $methodScope->assignExpression(new PropertyInitializationExpr($param->var->name), new MixedType(), new MixedType());
}
} elseif (!$stmt->isStatic()) {
$stackName = sprintf('%s::%s', $classReflection->getName(), $stmt->name->toString());
if (array_key_exists($stackName, $this->calledMethodUninitializedProperties)) {
foreach ($this->calledMethodUninitializedProperties[$stackName] as $propertyName => $_) {
$methodScope = $methodScope->invalidateExpression(new PropertyInitializationExpr($propertyName));
}
}
}

if ($stmt->getAttribute('virtual', false) === false) {
Expand Down Expand Up @@ -1017,6 +1027,7 @@ public function processStmtNode(
$this->callNodeCallback($nodeCallback, new ClassConstantsNode($stmt, $classStatementsGatherer->getConstants(), $classStatementsGatherer->getConstantFetches(), $classReflection), $classScope, $storage);
$classReflection->evictPrivateSymbols();
$this->calledMethodResults = [];
$this->calledMethodUninitializedProperties = [];
} elseif ($stmt instanceof Node\Stmt\Property) {
$hasYield = false;
$throwPoints = [];
Expand Down Expand Up @@ -4045,6 +4056,19 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection
}
}

/**
* @param array<string, true> $uninitializedProperties
*/
public function registerCalledMethodUninitializedProperties(string $stackName, array $uninitializedProperties): void
{
if (!array_key_exists($stackName, $this->calledMethodUninitializedProperties)) {
$this->calledMethodUninitializedProperties[$stackName] = $uninitializedProperties;
return;
}

$this->calledMethodUninitializedProperties[$stackName] = $this->calledMethodUninitializedProperties[$stackName] + $uninitializedProperties;
}

public function processCalledMethod(MethodReflection $methodReflection): ?MutatingScope
{
$declaringClass = $methodReflection->getDeclaringClass();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,14 @@ public function testRule(): void
'Readonly property MissingReadOnlyPropertyAssign\AdditionalAssignOfReadonlyPromotedProperty::$x is already assigned.',
188,
],
/*[
[
'Access to an uninitialized readonly property MissingReadOnlyPropertyAssign\MethodCalledFromConstructorBeforeAssign::$foo.',
226,
],
[
'Access to an uninitialized readonly property MissingReadOnlyPropertyAssign\MethodCalledTwice::$foo.',
244,
],*/
],
[
'Class MissingReadOnlyPropertyAssign\PropertyAssignedOnDifferentObjectUninitialized has an uninitialized readonly property $foo. Assign it in the constructor.',
264,
Expand Down
37 changes: 37 additions & 0 deletions tests/PHPStan/Rules/Variables/IssetRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,43 @@ public function testBug9503(): void
$this->analyse([__DIR__ . '/data/bug-9503.php'], []);
}

#[RequiresPhp('>= 8.4')]
public function testBug13473(): void
{
$this->treatPhpDocTypesAsCertain = true;

$this->analyse([__DIR__ . '/data/bug-13473.php'], [
[
'Property Bug13473\Bar::$bar in isset() is not nullable nor uninitialized.',
30,
],
[
'Property Bug13473\Baz::$foo (int) in isset() is not nullable.',
67,
],
]);
}

public function testIssetMethodCalledFromConstructor(): void
{
$this->treatPhpDocTypesAsCertain = true;

$this->analyse([__DIR__ . '/data/isset-method-called-from-constructor.php'], [
[
'Property IssetMethodCalledFromConstructor\MethodCalledFromConstructorWithDefault::$bar in isset() is not nullable nor uninitialized.',
34,
],
[
'Property IssetMethodCalledFromConstructor\MethodNotCalledFromConstructor::$bar in isset() is not nullable nor uninitialized.',
51,
],
[
'Property IssetMethodCalledFromConstructor\MultipleProperties::$bar in isset() is not nullable nor uninitialized.',
72,
],
]);
}

public function testBug14393(): void
{
$this->treatPhpDocTypesAsCertain = true;
Expand Down
81 changes: 81 additions & 0 deletions tests/PHPStan/Rules/Variables/data/bug-13473.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php // lint >= 8.4

declare(strict_types = 1);

namespace Bug13473;

class Foo {
private(set) int $bar {
get => $this->bar;
set(int $bar) {
if (isset($this->bar)) {
throw new \Exception('bar is set');
}
$this->bar = $bar;
}
}

public function __construct(int $bar)
{
$this->bar = $bar;
}
}

$foo = new Foo(10);

class Bar {
private(set) int $bar = 1 {
get => $this->bar;
set(int $bar) {
if (isset($this->bar)) {
throw new \Exception('bar is set');
}
$this->bar = $bar;
}
}

public function __construct(int $bar)
{
$this->bar = $bar;
}
}

class Qux {
private(set) int $foo;
private(set) int $bar {
get => $this->bar;
set(int $bar) {
if (isset($this->foo)) { // $foo has no default, could be uninitialized - no error
throw new \Exception('foo is set');
}
$this->bar = $bar;
}
}

public function __construct(int $bar)
{
$this->bar = $bar;
$this->foo = 42;
}
}

class Baz {
private(set) int $foo = 5;
private(set) int $bar {
get => $this->bar;
set(int $bar) {
if (isset($this->foo)) { // $foo has default value, always initialized - should error
echo 'foo is set';
}
if (isset($this->bar)) { // $bar has no default, could be uninitialized - no error
throw new \Exception('bar is set');
}
$this->bar = $bar;
}
}

public function __construct(int $bar)
{
$this->bar = $bar;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types = 1);

namespace IssetMethodCalledFromConstructor;

final class MethodCalledFromConstructor {
private int $bar;

public function __construct(int $bar)
{
$this->setBar($bar);
}

private function setBar(int $bar): void
{
if (isset($this->bar)) { // $bar has no default, could be uninitialized when called from constructor - no error
throw new \Exception('bar is set');
}
$this->bar = $bar;
}
}

final class MethodCalledFromConstructorWithDefault {
private int $bar = 1;

public function __construct(int $bar)
{
$this->setBar($bar);
}

private function setBar(int $bar): void
{
if (isset($this->bar)) { // $bar has default value, always initialized - should error
throw new \Exception('bar is set');
}
$this->bar = $bar;
}
}

final class MethodNotCalledFromConstructor {
private int $bar;

public function __construct(int $bar)
{
$this->bar = $bar;
}

private function checkBar(): void
{
if (isset($this->bar)) { // Not called from constructor, property is initialized after construction - should error
echo 'bar is set';
}
}
}

final class MultipleProperties {
private int $foo;
private int $bar = 5;

public function __construct(int $bar)
{
$this->init($bar);
$this->foo = 42;
}

private function init(int $bar): void
{
if (isset($this->foo)) { // $foo has no default, could be uninitialized - no error
throw new \Exception('foo is set');
}
if (isset($this->bar)) { // $bar has default value, always initialized - should error
echo 'bar is set';
}
$this->bar = $bar;
}
}
Loading