diff --git a/conf/config.level2.neon b/conf/config.level2.neon index 1ef7a9755b..8e41cc7b06 100644 --- a/conf/config.level2.neon +++ b/conf/config.level2.neon @@ -11,6 +11,8 @@ conditionalTags: phpstan.rules.rule: %featureToggles.classConstants% PHPStan\Rules\Methods\CallPrivateMethodThroughStaticRule: phpstan.rules.rule: %featureToggles.privateStaticCall% + PHPStan\Rules\Properties\AccessPrivatePropertyThroughStaticRule: + phpstan.rules.rule: %featureToggles.privateStaticCall% rules: - PHPStan\Rules\Cast\EchoRule @@ -60,6 +62,8 @@ services: class: PHPStan\Rules\Methods\CallPrivateMethodThroughStaticRule - class: PHPStan\Rules\PhpDoc\IncompatibleClassConstantPhpDocTypeRule + - + class: PHPStan\Rules\Properties\AccessPrivatePropertyThroughStaticRule - class: PHPStan\Rules\Generics\InterfaceAncestorsRule arguments: diff --git a/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php b/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php new file mode 100644 index 0000000000..8accf408b2 --- /dev/null +++ b/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php @@ -0,0 +1,63 @@ + + */ +class AccessPrivatePropertyThroughStaticRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Expr\StaticPropertyFetch::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\VarLikeIdentifier) { + return []; + } + if (!$node->class instanceof Name) { + return []; + } + + $propertyName = $node->name->name; + $className = $node->class; + if ($className->toLowerString() !== 'static') { + return []; + } + + $classType = $scope->resolveTypeByName($className); + if (!$classType->hasProperty($propertyName)->yes()) { + return []; + } + + $property = $classType->getProperty($propertyName, $scope); + if (!$property->isPrivate()) { + return []; + } + if (!$property->isStatic()) { + return []; + } + + if ($scope->isInClass() && $scope->getClassReflection()->isFinal()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Unsafe access to private property %s::$%s through static::.', + $property->getDeclaringClass()->getDisplayName(), + $propertyName + ))->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/Properties/AccessPrivatePropertyThroughStaticRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPrivatePropertyThroughStaticRuleTest.php new file mode 100644 index 0000000000..09ce325b1a --- /dev/null +++ b/tests/PHPStan/Rules/Properties/AccessPrivatePropertyThroughStaticRuleTest.php @@ -0,0 +1,27 @@ + */ +class AccessPrivatePropertyThroughStaticRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new AccessPrivatePropertyThroughStaticRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/access-private-property-static.php'], [ + [ + 'Unsafe access to private property AccessPrivatePropertyThroughStatic\Foo::$foo through static::.', + 13, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php index 740455d680..ac320be48d 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php @@ -174,6 +174,14 @@ public function testAccessStaticProperties(): void 209, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], + [ + 'Static access to instance property AccessWithStatic::$bar.', + 223, + ], + [ + 'Access to an undefined static property static(AccessWithStatic)::$nonexistent.', + 224, + ], ]); } diff --git a/tests/PHPStan/Rules/Properties/data/access-private-property-static.php b/tests/PHPStan/Rules/Properties/data/access-private-property-static.php new file mode 100644 index 0000000000..51e6fc3917 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/access-private-property-static.php @@ -0,0 +1,33 @@ +