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
17 changes: 17 additions & 0 deletions src/Node/ClassPropertiesNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Node;

use ArrayAccess;
use Override;
use PhpParser\Node;
use PhpParser\Node\Expr\Array_;
Expand All @@ -13,6 +14,8 @@
use PhpParser\NodeAbstract;
use PHPStan\Analyser\Scope;
use PHPStan\Node\Expr\PropertyInitializationExpr;
use PHPStan\Node\Expr\SetOffsetValueTypeExpr;
use PHPStan\Node\Expr\UnsetOffsetExpr;
use PHPStan\Node\Method\MethodCall;
use PHPStan\Node\Property\PropertyAssign;
use PHPStan\Node\Property\PropertyRead;
Expand All @@ -22,6 +25,7 @@
use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider;
use PHPStan\TrinaryLogic;
use PHPStan\Type\NeverType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\TypeUtils;
use function array_diff_key;
use function array_key_exists;
Expand Down Expand Up @@ -211,6 +215,19 @@

if ($usage instanceof PropertyWrite) {
if (array_key_exists($propertyName, $initializedPropertiesMap)) {
$originalNode = $usage->getOriginalNode();

if ($originalNode instanceof PropertyAssignNode) {
$assignedExpr = $originalNode->getAssignedExpr();

if (
($assignedExpr instanceof SetOffsetValueTypeExpr || $assignedExpr instanceof UnsetOffsetExpr)
&& (new ObjectType(ArrayAccess::class))->isSuperTypeOf($scope->getType($assignedExpr->getVar()))->yes()

Check warning on line 225 in src/Node/ClassPropertiesNode.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $originalNode = $usage->getOriginalNode(); if ($originalNode instanceof PropertyAssignNode) { $assignedExpr = $originalNode->getAssignedExpr(); - if (($assignedExpr instanceof SetOffsetValueTypeExpr || $assignedExpr instanceof UnsetOffsetExpr) && (new ObjectType(ArrayAccess::class))->isSuperTypeOf($scope->getType($assignedExpr->getVar()))->yes()) { + if (($assignedExpr instanceof SetOffsetValueTypeExpr || $assignedExpr instanceof UnsetOffsetExpr) && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($scope->getType($assignedExpr->getVar()))->no()) { continue; } }

Check warning on line 225 in src/Node/ClassPropertiesNode.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $originalNode = $usage->getOriginalNode(); if ($originalNode instanceof PropertyAssignNode) { $assignedExpr = $originalNode->getAssignedExpr(); - if (($assignedExpr instanceof SetOffsetValueTypeExpr || $assignedExpr instanceof UnsetOffsetExpr) && (new ObjectType(ArrayAccess::class))->isSuperTypeOf($scope->getType($assignedExpr->getVar()))->yes()) { + if (($assignedExpr instanceof SetOffsetValueTypeExpr || $assignedExpr instanceof UnsetOffsetExpr) && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($scope->getType($assignedExpr->getVar()))->no()) { continue; } }

Check warning on line 225 in src/Node/ClassPropertiesNode.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $originalNode = $usage->getOriginalNode(); if ($originalNode instanceof PropertyAssignNode) { $assignedExpr = $originalNode->getAssignedExpr(); - if (($assignedExpr instanceof SetOffsetValueTypeExpr || $assignedExpr instanceof UnsetOffsetExpr) && (new ObjectType(ArrayAccess::class))->isSuperTypeOf($scope->getType($assignedExpr->getVar()))->yes()) { + if (($assignedExpr instanceof SetOffsetValueTypeExpr || $assignedExpr instanceof UnsetOffsetExpr) && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($scope->getType($assignedExpr->getVar()))->no()) { continue; } }
) {
continue;
}
}

$hasInitialization = $initializedPropertiesMap[$propertyName]->or($usageScope->hasExpressionType(new PropertyInitializationExpr($propertyName)));
if (
!$hasInitialization->no()
Expand Down
5 changes: 3 additions & 2 deletions src/Node/ClassStatementsGatherer.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ private function gatherNodes(Node $node, Scope $scope): void
new PropertyFetch(new Expr\Variable('this'), new Identifier($node->getName())),
$scope,
true,
$node,
);
}
return;
Expand Down Expand Up @@ -194,7 +195,7 @@ private function gatherNodes(Node $node, Scope $scope): void
return;
}
if ($node instanceof PropertyAssignNode) {
$this->propertyUsages[] = new PropertyWrite($node->getPropertyFetch(), $scope, false);
$this->propertyUsages[] = new PropertyWrite($node->getPropertyFetch(), $scope, false, $node);
$this->propertyAssigns[] = new PropertyAssign($node, $scope);
return;
}
Expand All @@ -212,7 +213,7 @@ private function gatherNodes(Node $node, Scope $scope): void
}

$this->propertyUsages[] = new PropertyRead($node->expr, $scope);
$this->propertyUsages[] = new PropertyWrite($node->expr, $scope, false);
$this->propertyUsages[] = new PropertyWrite($node->expr, $scope, false, $node);
return;
}
if ($node instanceof FunctionCallableNode) {
Expand Down
10 changes: 9 additions & 1 deletion src/Node/Property/PropertyWrite.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@

namespace PHPStan\Node\Property;

use PhpParser\Node\Expr\AssignRef;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\StaticPropertyFetch;
use PHPStan\Analyser\Scope;
use PHPStan\Node\ClassPropertyNode;
use PHPStan\Node\PropertyAssignNode;

/**
* @api
*/
final class PropertyWrite
{

public function __construct(private PropertyFetch|StaticPropertyFetch $fetch, private Scope $scope, private bool $promotedPropertyWrite)
public function __construct(private PropertyFetch|StaticPropertyFetch $fetch, private Scope $scope, private bool $promotedPropertyWrite, private ClassPropertyNode|PropertyAssignNode|AssignRef|null $originalNode = null)
{
}

Expand All @@ -34,4 +37,9 @@ public function isPromotedPropertyWrite(): bool
return $this->promotedPropertyWrite;
}

public function getOriginalNode(): ClassPropertyNode|PropertyAssignNode|AssignRef|null
{
return $this->originalNode;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,15 @@ public static function getAdditionalConfigFiles(): array
);
}

#[RequiresPhp('>= 8.1')]
public function testBug13856(): void
{
$this->analyse([__DIR__ . '/data/bug-13856.php'], [
[
'Readonly property Bug13856\foo2::$store is already assigned.',
28,
],
]);
}

}
30 changes: 30 additions & 0 deletions tests/PHPStan/Rules/Properties/data/bug-13856.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php // lint >= 8.1

namespace Bug13856;

use SplObjectStorage;

class foo
{
/** @var SplObjectStorage<object, mixed> */
private readonly SplObjectStorage $store;

public function __construct()
{
$this->store = new SplObjectStorage();
$this->store[(object) ['foo' => 'bar']] = true;
unset($this->store[(object) ['foo' => 'bar']]);
}
}

class foo2
{
/** @var array<int, bool> */
private readonly array $store;

public function __construct()
{
$this->store[1] = true;
$this->store[2] = false;
}
}
Loading