diff --git a/Makefile b/Makefile index 0e7b84ceef..21280e432b 100644 --- a/Makefile +++ b/Makefile @@ -38,6 +38,7 @@ lint: --exclude tests/PHPStan/Levels/data/namedArguments.php \ --exclude tests/PHPStan/Rules/Keywords/data/continue-break.php \ --exclude tests/PHPStan/Rules/Properties/data/read-only-property.php \ + --exclude tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc-and-native.php \ --exclude tests/PHPStan/Rules/Properties/data/overriding-property.php \ --exclude tests/PHPStan/Rules/Constants/data/overriding-final-constant.php \ --exclude tests/PHPStan/Rules/Properties/data/intersection-types.php \ diff --git a/src/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRule.php b/src/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRule.php index 2851e446a8..9a52f0f99c 100644 --- a/src/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRule.php +++ b/src/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRule.php @@ -38,7 +38,7 @@ public function processNode(Node $node, Scope $scope): array $errors = []; foreach ($properties as $propertyName => $propertyNode) { - if (!$propertyNode->isReadOnlyByPhpDoc()) { + if (!$propertyNode->isReadOnlyByPhpDoc() || $propertyNode->isReadOnly()) { continue; } $errors[] = RuleErrorBuilder::message(sprintf( @@ -49,7 +49,7 @@ public function processNode(Node $node, Scope $scope): array } foreach ($prematureAccess as [$propertyName, $line, $propertyNode]) { - if (!$propertyNode->isReadOnlyByPhpDoc()) { + if (!$propertyNode->isReadOnlyByPhpDoc() || $propertyNode->isReadOnly()) { continue; } $errors[] = RuleErrorBuilder::message(sprintf( @@ -60,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array } foreach ($additionalAssigns as [$propertyName, $line, $propertyNode]) { - if (!$propertyNode->isReadOnlyByPhpDoc()) { + if (!$propertyNode->isReadOnlyByPhpDoc() || $propertyNode->isReadOnly()) { continue; } $errors[] = RuleErrorBuilder::message(sprintf( diff --git a/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRule.php b/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRule.php index 2756089b27..51d5b0a665 100644 --- a/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRule.php +++ b/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRule.php @@ -41,7 +41,7 @@ public function processNode(Node $node, Scope $scope): array if (!$scope->canAccessProperty($propertyReflection)) { continue; } - if (!$nativeReflection->isReadOnlyByPhpDoc()) { + if (!$nativeReflection->isReadOnlyByPhpDoc() || $nativeReflection->isReadOnly()) { continue; } diff --git a/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRule.php b/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRule.php index bad510145e..1949a85652 100644 --- a/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRule.php +++ b/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRule.php @@ -50,7 +50,7 @@ public function processNode(Node $node, Scope $scope): array if (!$scope->canAccessProperty($propertyReflection)) { continue; } - if (!$nativeReflection->isReadOnlyByPhpDoc()) { + if (!$nativeReflection->isReadOnlyByPhpDoc() || $nativeReflection->isReadOnly()) { continue; } diff --git a/src/Rules/Properties/ReadOnlyByPhpDocPropertyRule.php b/src/Rules/Properties/ReadOnlyByPhpDocPropertyRule.php index 96e793b776..b6f70ed756 100644 --- a/src/Rules/Properties/ReadOnlyByPhpDocPropertyRule.php +++ b/src/Rules/Properties/ReadOnlyByPhpDocPropertyRule.php @@ -21,7 +21,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node->isReadOnlyByPhpDoc()) { + if (!$node->isReadOnlyByPhpDoc() || $node->isReadOnly()) { return []; } diff --git a/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php index 2a478b578d..30f3872c84 100644 --- a/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php @@ -140,4 +140,13 @@ public function testRule(): void ]); } + public function testRuleIgnoresNativeReadonly(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/missing-readonly-property-assign-phpdoc-and-native.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRuleTest.php index 11b899c34c..d000d4f3f3 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -58,4 +59,13 @@ public function testRule(): void ]); } + public function testRuleIgnoresNativeReadonly(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/readonly-assign-ref-phpdoc-and-native.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php index bf86b85b71..72b9d3ea26 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php @@ -107,6 +107,15 @@ public function testRule(): void ]); } + public function testRuleIgnoresNativeReadonly(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/readonly-assign-phpdoc-and-native.php'], []); + } + public function testBug7361(): void { if (PHP_VERSION_ID < 80100) { @@ -121,4 +130,13 @@ public function testBug7361(): void ]); } + public function testFeature7648(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/feature-7648.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyRuleTest.php index 4f2ea95c41..3259cad332 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyRuleTest.php @@ -43,4 +43,13 @@ public function testRule(): void ]); } + public function testRuleIgnoresNativeReadonly(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/read-only-property-phpdoc-and-native.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php index 600ef2e572..1fc7dceba1 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php @@ -111,4 +111,18 @@ public function testRule(): void ]); } + public function testFeature7648(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/feature-7648.php'], [ + [ + 'Readonly property Feature7648\Request::$offset is assigned outside of the constructor.', + 23, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/feature-7648.php b/tests/PHPStan/Rules/Properties/data/feature-7648.php new file mode 100644 index 0000000000..8596ec9a1a --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/feature-7648.php @@ -0,0 +1,25 @@ += 8.1 + +namespace Feature7648; + +/** @immutable */ +class Request +{ + use OffsetTrait; + + public function __construct(int $offset) + { + $this->populateOffsets($offset); + } +} + +/** @immutable */ +trait OffsetTrait +{ + public readonly int $offset; + + private function populateOffsets(int $offset): void + { + $this->offset = $offset; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign-phpdoc-and-native.php b/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign-phpdoc-and-native.php new file mode 100644 index 0000000000..aa15ff16f0 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign-phpdoc-and-native.php @@ -0,0 +1,51 @@ += 8.1 + +namespace MissingReadOnlyPropertyAssignPhpDocAndNative; + +class Foo +{ + + /** @readonly */ + private readonly int $assigned; + + private int $unassignedButNotReadOnly; + + private int $readBeforeAssignedNotReadOnly; + + /** @readonly */ + private readonly int $unassigned; + + /** @readonly */ + private readonly int $unassigned2; + + /** @readonly */ + private readonly int $readBeforeAssigned; + + /** @readonly */ + private readonly int $doubleAssigned; + + private int $doubleAssignedNotReadOnly; + + public function __construct() + { + $this->assigned = 1; + + echo $this->readBeforeAssignedNotReadOnly; + $this->readBeforeAssignedNotReadOnly = 1; + + echo $this->readBeforeAssigned; + $this->readBeforeAssigned = 1; + + $this->doubleAssigned = 1; + $this->doubleAssigned = 2; + + $this->doubleAssignedNotReadOnly = 1; + $this->doubleAssignedNotReadOnly = 2; + } + + public function setUnassigned2(int $i): void + { + $this->unassigned2 = $i; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc-and-native.php b/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc-and-native.php new file mode 100644 index 0000000000..3e3c94a909 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc-and-native.php @@ -0,0 +1,15 @@ += 8.1 + +namespace ReadonlyPropertyAssignPhpDocAndNative; + +class Foo +{ + + /** @readonly */ + private readonly int $foo; + + /** @readonly */ + protected readonly int $bar; + + /** @readonly */ + public readonly int $baz; + + public function __construct(int $foo) + { + $this->foo = $foo; // constructor - fine + } + + public function setFoo(int $foo): void + { + $this->foo = $foo; // setter - report + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/readonly-assign-ref-phpdoc-and-native.php b/tests/PHPStan/Rules/Properties/data/readonly-assign-ref-phpdoc-and-native.php new file mode 100644 index 0000000000..2ce9bb2e08 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/readonly-assign-ref-phpdoc-and-native.php @@ -0,0 +1,20 @@ += 8.1 + +namespace ReadOnlyPropertyAssignRefPhpDocAndNative; + +class Foo +{ + + /** @readonly */ + private readonly int $foo; + + /** @readonly */ + public readonly int $bar; + + public function doFoo() + { + $foo = &$this->foo; + $bar = &$this->bar; + } + +}