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
123 changes: 123 additions & 0 deletions src/Rules/Properties/ReadOnlyPropertyIndirectModificationRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Properties;

use ArrayAccess;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\StaticPropertyFetch;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Node\PropertyAssignNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\ObjectType;
use function sprintf;

/**
* @implements Rule<PropertyAssignNode>
*/
#[RegisteredRule(level: 3)]
final class ReadOnlyPropertyIndirectModificationRule implements Rule
{

public function __construct(
private PropertyReflectionFinder $propertyReflectionFinder,
)
{
}

public function getNodeType(): string
{
return PropertyAssignNode::class;
}

public function processNode(Node $node, Scope $scope): array
{
$propertyFetch = $node->getPropertyFetch();
if (!$propertyFetch instanceof PropertyFetch) {
return [];
}

return $this->checkVarChain($propertyFetch->var, $scope);
}

/**
* @return list<IdentifierRuleError>
*/
private function checkVarChain(Expr $expr, Scope $scope): array
{
$errors = [];

while (true) {
if ($expr instanceof ArrayDimFetch) {
while ($expr instanceof ArrayDimFetch) {
$expr = $expr->var;
}

if ($expr instanceof PropertyFetch && $expr->name instanceof Node\Identifier) {
$propertyType = $scope->getType($expr);
if (!(new ObjectType(ArrayAccess::class))->isSuperTypeOf($propertyType)->yes()) {

Check warning on line 63 in src/Rules/Properties/ReadOnlyPropertyIndirectModificationRule.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ($expr instanceof PropertyFetch && $expr->name instanceof Node\Identifier) { $propertyType = $scope->getType($expr); - if (!(new ObjectType(ArrayAccess::class))->isSuperTypeOf($propertyType)->yes()) { + if ((new ObjectType(ArrayAccess::class))->isSuperTypeOf($propertyType)->no()) { $reflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($expr, $scope); foreach ($reflections as $reflection) { $nativeReflection = $reflection->getNativeReflection();

Check warning on line 63 in src/Rules/Properties/ReadOnlyPropertyIndirectModificationRule.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ($expr instanceof PropertyFetch && $expr->name instanceof Node\Identifier) { $propertyType = $scope->getType($expr); - if (!(new ObjectType(ArrayAccess::class))->isSuperTypeOf($propertyType)->yes()) { + if ((new ObjectType(ArrayAccess::class))->isSuperTypeOf($propertyType)->no()) { $reflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($expr, $scope); foreach ($reflections as $reflection) { $nativeReflection = $reflection->getNativeReflection();
$reflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($expr, $scope);
foreach ($reflections as $reflection) {
$nativeReflection = $reflection->getNativeReflection();
if ($nativeReflection === null) {
continue;
}
if ($nativeReflection->isReadOnly()) {
$declaringClass = $nativeReflection->getDeclaringClass();
$errors[] = RuleErrorBuilder::message(sprintf(
'Readonly property %s::$%s is indirectly modified.',
$declaringClass->getDisplayName(),
$reflection->getName(),
))
->line($expr->name->getStartLine())
->identifier('property.readOnlyIndirectModification')
->build();
} elseif ($nativeReflection->isReadOnlyByPhpDoc()) {
if ($nativeReflection->isAllowedPrivateMutation()) {
continue;
}
$declaringClass = $nativeReflection->getDeclaringClass();
$errors[] = RuleErrorBuilder::message(sprintf(
'@readonly property %s::$%s is indirectly modified.',
$declaringClass->getDisplayName(),
$reflection->getName(),
))
->line($expr->name->getStartLine())
->identifier('property.readOnlyByPhpDocIndirectModification')
->build();
}
}
}

$expr = $expr->var;
continue;
}

if ($expr instanceof StaticPropertyFetch) {
$expr = $expr->class instanceof Node\Name ? null : $expr->class;
if ($expr === null) {
break;
}
continue;
}

break;
}

if ($expr instanceof PropertyFetch) {
$expr = $expr->var;
continue;
}

break;
}

return $errors;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Properties;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPUnit\Framework\Attributes\RequiresPhp;

/**
* @extends RuleTestCase<ReadOnlyPropertyIndirectModificationRule>
*/
class ReadOnlyPropertyIndirectModificationRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new ReadOnlyPropertyIndirectModificationRule(
new PropertyReflectionFinder(),
);
}

#[RequiresPhp('>= 8.1.0')]
public function testBug14481(): void
{
$this->analyse([__DIR__ . '/data/bug-14481.php'], [
[
'Readonly property Bug14481\VehicleListFilterForModule::$carTypes is indirectly modified.',
28,
],
[
'Readonly property Bug14481\IndirectModificationOutsideConstructor::$items is indirectly modified.',
47,
],
[
'Readonly property Bug14481\DirectPropertyAssignThroughReadonlyArray::$items is indirectly modified.',
65,
],
[
'Readonly property Bug14481\NestedArrayDimFetch::$nested is indirectly modified.',
86,
],
[
'Readonly property Bug14481\IncrementThroughReadonlyArray::$items is indirectly modified.',
131,
],
[
'Readonly property Bug14481\DeeperChain::$wrappers is indirectly modified.',
165,
],
[
'@readonly property Bug14481\ReadonlyByPhpDocClass::$items is indirectly modified.',
195,
],
[
'@readonly property Bug14481\ReadonlyByPhpDocProperty::$items is indirectly modified.',
217,
],
]);
}

}
Loading
Loading