diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index be65e597a3..6120cbd670 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -25,5 +25,6 @@ parameters: crossCheckInterfaces: true finalByPhpDocTag: true classConstants: true + privateStaticCall: true stubFiles: - ../stubs/arrayFunctions.stub diff --git a/conf/config.level2.neon b/conf/config.level2.neon index f437bce73f..1ef7a9755b 100644 --- a/conf/config.level2.neon +++ b/conf/config.level2.neon @@ -9,6 +9,8 @@ parameters: conditionalTags: PHPStan\Rules\PhpDoc\IncompatibleClassConstantPhpDocTypeRule: phpstan.rules.rule: %featureToggles.classConstants% + PHPStan\Rules\Methods\CallPrivateMethodThroughStaticRule: + phpstan.rules.rule: %featureToggles.privateStaticCall% rules: - PHPStan\Rules\Cast\EchoRule @@ -54,6 +56,8 @@ services: crossCheckInterfaces: %featureToggles.crossCheckInterfaces% tags: - phpstan.rules.rule + - + class: PHPStan\Rules\Methods\CallPrivateMethodThroughStaticRule - class: PHPStan\Rules\PhpDoc\IncompatibleClassConstantPhpDocTypeRule - diff --git a/conf/config.neon b/conf/config.neon index 7125c4ca2b..932dd54217 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -49,6 +49,7 @@ parameters: crossCheckInterfaces: false finalByPhpDocTag: false classConstants: false + privateStaticCall: false fileExtensions: - php checkAlwaysTrueCheckTypeFunctionCall: false @@ -227,7 +228,8 @@ parametersSchema: validateOverridingMethodsInStubs: bool(), crossCheckInterfaces: bool(), finalByPhpDocTag: bool(), - classConstants: bool() + classConstants: bool(), + privateStaticCall: bool() ]) fileExtensions: listOf(string()) checkAlwaysTrueCheckTypeFunctionCall: bool() diff --git a/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php b/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php new file mode 100644 index 0000000000..089cd81613 --- /dev/null +++ b/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php @@ -0,0 +1,61 @@ + + */ +class CallPrivateMethodThroughStaticRule implements Rule +{ + + public function getNodeType(): string + { + return StaticCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Identifier) { + return []; + } + if (!$node->class instanceof Name) { + return []; + } + + $methodName = $node->name->name; + $className = $node->class; + if ($className->toLowerString() !== 'static') { + return []; + } + + $classType = $scope->resolveTypeByName($className); + if (!$classType->hasMethod($methodName)->yes()) { + return []; + } + + $method = $classType->getMethod($methodName, $scope); + if (!$method->isPrivate()) { + return []; + } + + if ($scope->isInClass() && $scope->getClassReflection()->isFinal()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Unsafe call to private method %s::%s() through static::.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName() + ))->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/Methods/CallPrivateMethodThroughStaticRuleTest.php b/tests/PHPStan/Rules/Methods/CallPrivateMethodThroughStaticRuleTest.php new file mode 100644 index 0000000000..d96da60225 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/CallPrivateMethodThroughStaticRuleTest.php @@ -0,0 +1,28 @@ + + */ +class CallPrivateMethodThroughStaticRuleTest extends RuleTestCase +{ + + protected function getRule(): \PHPStan\Rules\Rule + { + return new CallPrivateMethodThroughStaticRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/call-private-method-static.php'], [ + [ + 'Unsafe call to private method CallPrivateMethodThroughStatic\Foo::doBar() through static::.', + 12, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index d1ab7c2347..e3fafa83c9 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -225,6 +225,10 @@ public function testCallStaticMethods(): void 328, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], + [ + 'Call to an undefined static method static(CallStaticMethods\CallWithStatic)::nonexistent().', + 344, + ], ]); } diff --git a/tests/PHPStan/Rules/Methods/data/call-private-method-static.php b/tests/PHPStan/Rules/Methods/data/call-private-method-static.php new file mode 100644 index 0000000000..f273d48d73 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/call-private-method-static.php @@ -0,0 +1,37 @@ +