From d38d4e4e61ebabcdbc6ffb54db09d08ae71d15fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Sun, 8 Dec 2024 02:46:16 +0100 Subject: [PATCH 1/3] Detect first class callable syntax calls This is the syntax supported by PHP 8.1: ``` $callable = print_r(...); $callable(42); ``` or ``` $blade = new \Waldo\Quux\Blade(); $callable = $blade->runner(...); $callable(303); ``` The errors for the disallowed code are reported on lines with `(...)`, not when the callable is called. Ref #275 --- extension.neon | 1 + src/Allowed/Allowed.php | 2 +- src/Calls/FunctionCalls.php | 67 ++------------ src/Calls/FunctionFirstClassCallables.php | 67 ++++++++++++++ src/Calls/MethodFirstClassCallables.php | 66 ++++++++++++++ src/Calls/StaticFirstClassCallables.php | 66 ++++++++++++++ .../DisallowedFunctionRuleErrors.php | 88 +++++++++++++++++++ src/RuleErrors/DisallowedMethodRuleErrors.php | 3 + .../FunctionCallsAllowInFunctionsTest.php | 9 +- .../Calls/FunctionCallsAllowInMethodsTest.php | 9 +- tests/Calls/FunctionCallsDefinedInTest.php | 9 +- .../FunctionCallsInMultipleNamespacesTest.php | 9 +- tests/Calls/FunctionCallsNamedParamsTest.php | 9 +- .../Calls/FunctionCallsParamsMessagesTest.php | 9 +- tests/Calls/FunctionCallsTest.php | 9 +- ...TypeStringParamsInvalidFlagsConfigTest.php | 9 +- .../FunctionCallsTypeStringParamsTest.php | 9 +- ...unctionCallsUnsupportedParamConfigTest.php | 9 +- .../Calls/FunctionFirstClassCallablesTest.php | 66 ++++++++++++++ tests/Calls/MethodFirstClassCallablesTest.php | 70 +++++++++++++++ tests/Calls/StaticFirstClassCallablesTest.php | 78 ++++++++++++++++ .../disallowed-allow/firstClassCallable.php | 27 ++++++ tests/src/disallowed/firstClassCallable.php | 27 ++++++ 23 files changed, 586 insertions(+), 132 deletions(-) create mode 100644 src/Calls/FunctionFirstClassCallables.php create mode 100644 src/Calls/MethodFirstClassCallables.php create mode 100644 src/Calls/StaticFirstClassCallables.php create mode 100644 src/RuleErrors/DisallowedFunctionRuleErrors.php create mode 100644 tests/Calls/FunctionFirstClassCallablesTest.php create mode 100644 tests/Calls/MethodFirstClassCallablesTest.php create mode 100644 tests/Calls/StaticFirstClassCallablesTest.php create mode 100644 tests/src/disallowed-allow/firstClassCallable.php create mode 100644 tests/src/disallowed/firstClassCallable.php diff --git a/extension.neon b/extension.neon index c7950d3..01f73b9 100644 --- a/extension.neon +++ b/extension.neon @@ -254,6 +254,7 @@ services: - Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedAttributeRuleErrors - Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedConstantRuleErrors - Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedControlStructureRuleErrors + - Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors - Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedMethodRuleErrors - Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedNamespaceRuleErrors - Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors diff --git a/src/Allowed/Allowed.php b/src/Allowed/Allowed.php index 44ead1b..2560498 100644 --- a/src/Allowed/Allowed.php +++ b/src/Allowed/Allowed.php @@ -181,7 +181,7 @@ private function getArgType(array $args, Scope $scope, Param $param): ?Type if (!isset($found)) { $found = $args[$param->getPosition() - 1] ?? null; } - return isset($found) ? $scope->getType($found->value) : null; + return isset($found, $found->value) ? $scope->getType($found->value) : null; } diff --git a/src/Calls/FunctionCalls.php b/src/Calls/FunctionCalls.php index 494d651..a9e777b 100644 --- a/src/Calls/FunctionCalls.php +++ b/src/Calls/FunctionCalls.php @@ -5,20 +5,13 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Expr\Variable; -use PhpParser\Node\Name; -use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; use PHPStan\ShouldNotHappenException; use Spaze\PHPStan\Rules\Disallowed\DisallowedCall; use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory; -use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer; -use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors; -use Spaze\PHPStan\Rules\Disallowed\RuleErrors\ErrorIdentifiers; -use Spaze\PHPStan\Rules\Disallowed\Type\TypeResolver; +use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors; /** * Reports on dynamically calling a disallowed function. @@ -29,42 +22,27 @@ class FunctionCalls implements Rule { - private DisallowedCallsRuleErrors $disallowedCallsRuleErrors; + private DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors; /** @var list */ private array $disallowedCalls; - private ReflectionProvider $reflectionProvider; - - private Normalizer $normalizer; - - private TypeResolver $typeResolver; - /** - * @param DisallowedCallsRuleErrors $disallowedCallsRuleErrors + * @param DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors * @param DisallowedCallFactory $disallowedCallFactory - * @param ReflectionProvider $reflectionProvider - * @param Normalizer $normalizer - * @param TypeResolver $typeResolver * @param array $forbiddenCalls * @phpstan-param ForbiddenCallsConfig $forbiddenCalls * @noinspection PhpUndefinedClassInspection ForbiddenCallsConfig is a type alias defined in PHPStan config * @throws ShouldNotHappenException */ public function __construct( - DisallowedCallsRuleErrors $disallowedCallsRuleErrors, + DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors, DisallowedCallFactory $disallowedCallFactory, - ReflectionProvider $reflectionProvider, - Normalizer $normalizer, - TypeResolver $typeResolver, array $forbiddenCalls ) { - $this->disallowedCallsRuleErrors = $disallowedCallsRuleErrors; + $this->disallowedFunctionRuleErrors = $disallowedFunctionRuleErrors; $this->disallowedCalls = $disallowedCallFactory->createFromConfig($forbiddenCalls); - $this->reflectionProvider = $reflectionProvider; - $this->normalizer = $normalizer; - $this->typeResolver = $typeResolver; } @@ -82,40 +60,7 @@ public function getNodeType(): string */ public function processNode(Node $node, Scope $scope): array { - if ($node->name instanceof Name) { - $namespacedName = $node->name->getAttribute('namespacedName'); - if ($namespacedName !== null && !($namespacedName instanceof Name)) { - throw new ShouldNotHappenException(); - } - $names = [$namespacedName, $node->name]; - } elseif ($node->name instanceof String_) { - $names = [new Name($this->normalizer->normalizeNamespace($node->name->value))]; - } elseif ($node->name instanceof Variable) { - $value = $this->typeResolver->getVariableStringValue($node->name, $scope); - if (!is_string($value)) { - return []; - } - $names = [new Name($this->normalizer->normalizeNamespace($value))]; - } else { - return []; - } - $displayName = $node->name->getAttribute('originalName'); - if ($displayName !== null && !($displayName instanceof Name)) { - throw new ShouldNotHappenException(); - } - foreach ($names as $name) { - if ($name && $this->reflectionProvider->hasFunction($name, $scope)) { - $functionReflection = $this->reflectionProvider->getFunction($name, $scope); - $definedIn = $functionReflection->isBuiltin() ? null : $functionReflection->getFileName(); - } else { - $definedIn = null; - } - $message = $this->disallowedCallsRuleErrors->get($node, $scope, (string)$name, (string)($displayName ?? $name), $definedIn, $this->disallowedCalls, ErrorIdentifiers::DISALLOWED_FUNCTION); - if ($message) { - return $message; - } - } - return []; + return $this->disallowedFunctionRuleErrors->get($node, $scope, $this->disallowedCalls); } } diff --git a/src/Calls/FunctionFirstClassCallables.php b/src/Calls/FunctionFirstClassCallables.php new file mode 100644 index 0000000..73a73f9 --- /dev/null +++ b/src/Calls/FunctionFirstClassCallables.php @@ -0,0 +1,67 @@ + + */ +class FunctionFirstClassCallables implements Rule +{ + + private DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors; + + /** @var list */ + private array $disallowedCalls; + + + /** + * @param DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors + * @param DisallowedCallFactory $disallowedCallFactory + * @param array $forbiddenCalls + * @phpstan-param ForbiddenCallsConfig $forbiddenCalls + * @noinspection PhpUndefinedClassInspection ForbiddenCallsConfig is a type alias defined in PHPStan config + * @throws ShouldNotHappenException + */ + public function __construct( + DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors, + DisallowedCallFactory $disallowedCallFactory, + array $forbiddenCalls + ) { + $this->disallowedFunctionRuleErrors = $disallowedFunctionRuleErrors; + $this->disallowedCalls = $disallowedCallFactory->createFromConfig($forbiddenCalls); + } + + + public function getNodeType(): string + { + return FunctionCallableNode::class; + } + + + /** + * @param FunctionCallableNode $node + * @param Scope $scope + * @return list + * @throws ShouldNotHappenException + */ + public function processNode(Node $node, Scope $scope): array + { + $originalNode = $node->getOriginalNode(); + return $this->disallowedFunctionRuleErrors->get($originalNode, $scope, $this->disallowedCalls); + } + +} diff --git a/src/Calls/MethodFirstClassCallables.php b/src/Calls/MethodFirstClassCallables.php new file mode 100644 index 0000000..276dea4 --- /dev/null +++ b/src/Calls/MethodFirstClassCallables.php @@ -0,0 +1,66 @@ +StaticFirstClassCallables + * + * @package Spaze\PHPStan\Rules\Disallowed + * @implements Rule + */ +class MethodFirstClassCallables implements Rule +{ + + private DisallowedMethodRuleErrors $disallowedMethodRuleErrors; + + /** @var list */ + private array $disallowedCalls; + + + /** + * @param DisallowedMethodRuleErrors $disallowedMethodRuleErrors + * @param DisallowedCallFactory $disallowedCallFactory + * @param array $forbiddenCalls + * @phpstan-param ForbiddenCallsConfig $forbiddenCalls + * @noinspection PhpUndefinedClassInspection ForbiddenCallsConfig is a type alias defined in PHPStan config + * @throws ShouldNotHappenException + */ + public function __construct(DisallowedMethodRuleErrors $disallowedMethodRuleErrors, DisallowedCallFactory $disallowedCallFactory, array $forbiddenCalls) + { + $this->disallowedMethodRuleErrors = $disallowedMethodRuleErrors; + $this->disallowedCalls = $disallowedCallFactory->createFromConfig($forbiddenCalls); + } + + + public function getNodeType(): string + { + return MethodCallableNode::class; + } + + + /** + * @param MethodCallableNode $node + * @param Scope $scope + * @return list + * @throws ShouldNotHappenException + */ + public function processNode(Node $node, Scope $scope): array + { + $originalNode = $node->getOriginalNode(); + return $this->disallowedMethodRuleErrors->get($originalNode->var, $originalNode, $scope, $this->disallowedCalls); + } + +} diff --git a/src/Calls/StaticFirstClassCallables.php b/src/Calls/StaticFirstClassCallables.php new file mode 100644 index 0000000..a864a46 --- /dev/null +++ b/src/Calls/StaticFirstClassCallables.php @@ -0,0 +1,66 @@ +MethodFirstClassCallables + * + * @package Spaze\PHPStan\Rules\Disallowed + * @implements Rule + */ +class StaticFirstClassCallables implements Rule +{ + + private DisallowedMethodRuleErrors $disallowedMethodRuleErrors; + + /** @var list */ + private array $disallowedCalls; + + + /** + * @param DisallowedMethodRuleErrors $disallowedMethodRuleErrors + * @param DisallowedCallFactory $disallowedCallFactory + * @param array $forbiddenCalls + * @phpstan-param ForbiddenCallsConfig $forbiddenCalls + * @noinspection PhpUndefinedClassInspection ForbiddenCallsConfig is a type alias defined in PHPStan config + * @throws ShouldNotHappenException + */ + public function __construct(DisallowedMethodRuleErrors $disallowedMethodRuleErrors, DisallowedCallFactory $disallowedCallFactory, array $forbiddenCalls) + { + $this->disallowedMethodRuleErrors = $disallowedMethodRuleErrors; + $this->disallowedCalls = $disallowedCallFactory->createFromConfig($forbiddenCalls); + } + + + public function getNodeType(): string + { + return StaticMethodCallableNode::class; + } + + + /** + * @param StaticMethodCallableNode $node + * @param Scope $scope + * @return list + * @throws ShouldNotHappenException + */ + public function processNode(Node $node, Scope $scope): array + { + $originalNode = $node->getOriginalNode(); + return $this->disallowedMethodRuleErrors->get($originalNode->class, $originalNode, $scope, $this->disallowedCalls); + } + +} diff --git a/src/RuleErrors/DisallowedFunctionRuleErrors.php b/src/RuleErrors/DisallowedFunctionRuleErrors.php new file mode 100644 index 0000000..7725f97 --- /dev/null +++ b/src/RuleErrors/DisallowedFunctionRuleErrors.php @@ -0,0 +1,88 @@ +disallowedCallsRuleErrors = $disallowedCallsRuleErrors; + $this->reflectionProvider = $reflectionProvider; + $this->normalizer = $normalizer; + $this->typeResolver = $typeResolver; + } + + + /** + * @param FuncCall $node + * @param Scope $scope + * @param list $disallowedCalls + * @return list + * @throws ShouldNotHappenException + */ + public function get(FuncCall $node, Scope $scope, array $disallowedCalls): array + { + if ($node->name instanceof Name) { + $namespacedName = $node->name->getAttribute('namespacedName'); + if ($namespacedName !== null && !($namespacedName instanceof Name)) { + throw new ShouldNotHappenException(); + } + $names = [$namespacedName, $node->name]; + } elseif ($node->name instanceof String_) { + $names = [new Name($this->normalizer->normalizeNamespace($node->name->value))]; + } elseif ($node->name instanceof Variable) { + $value = $this->typeResolver->getVariableStringValue($node->name, $scope); + if (!is_string($value)) { + return []; + } + $names = [new Name($this->normalizer->normalizeNamespace($value))]; + } else { + return []; + } + $displayName = $node->name->getAttribute('originalName'); + if ($displayName !== null && !($displayName instanceof Name)) { + throw new ShouldNotHappenException(); + } + foreach ($names as $name) { + if ($name && $this->reflectionProvider->hasFunction($name, $scope)) { + $functionReflection = $this->reflectionProvider->getFunction($name, $scope); + $definedIn = $functionReflection->isBuiltin() ? null : $functionReflection->getFileName(); + } else { + $definedIn = null; + } + $message = $this->disallowedCallsRuleErrors->get($node, $scope, (string)$name, (string)($displayName ?? $name), $definedIn, $disallowedCalls, ErrorIdentifiers::DISALLOWED_FUNCTION); + if ($message) { + return $message; + } + } + return []; + } + +} diff --git a/src/RuleErrors/DisallowedMethodRuleErrors.php b/src/RuleErrors/DisallowedMethodRuleErrors.php index a7d8e06..c5055f3 100644 --- a/src/RuleErrors/DisallowedMethodRuleErrors.php +++ b/src/RuleErrors/DisallowedMethodRuleErrors.php @@ -62,6 +62,9 @@ public function get($class, CallLike $node, Scope $scope, array $disallowedCalls } $calledOnType = $this->typeResolver->getType($class, $scope); + if ($calledOnType->isClassString()->yes()) { + $calledOnType = $calledOnType->getClassStringObjectType(); + } if ($calledOnType->canCallMethods()->yes() && $calledOnType->hasMethod($methodName)->yes()) { $method = $calledOnType->getMethod($methodName, $scope); $declaringClass = $method->getDeclaringClass(); diff --git a/tests/Calls/FunctionCallsAllowInFunctionsTest.php b/tests/Calls/FunctionCallsAllowInFunctionsTest.php index 0e67521..67f0ac4 100644 --- a/tests/Calls/FunctionCallsAllowInFunctionsTest.php +++ b/tests/Calls/FunctionCallsAllowInFunctionsTest.php @@ -7,9 +7,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Testing\RuleTestCase; use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory; -use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer; -use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors; -use Spaze\PHPStan\Rules\Disallowed\Type\TypeResolver; +use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors; class FunctionCallsAllowInFunctionsTest extends RuleTestCase { @@ -21,11 +19,8 @@ protected function getRule(): Rule { $container = self::getContainer(); return new FunctionCalls( - $container->getByType(DisallowedCallsRuleErrors::class), + $container->getByType(DisallowedFunctionRuleErrors::class), $container->getByType(DisallowedCallFactory::class), - $this->createReflectionProvider(), - $container->getByType(Normalizer::class), - $container->getByType(TypeResolver::class), [ [ 'function' => 'md*()', diff --git a/tests/Calls/FunctionCallsAllowInMethodsTest.php b/tests/Calls/FunctionCallsAllowInMethodsTest.php index 61d5dd8..1da872b 100644 --- a/tests/Calls/FunctionCallsAllowInMethodsTest.php +++ b/tests/Calls/FunctionCallsAllowInMethodsTest.php @@ -7,9 +7,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Testing\RuleTestCase; use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory; -use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer; -use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors; -use Spaze\PHPStan\Rules\Disallowed\Type\TypeResolver; +use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors; class FunctionCallsAllowInMethodsTest extends RuleTestCase { @@ -21,11 +19,8 @@ protected function getRule(): Rule { $container = self::getContainer(); return new FunctionCalls( - $container->getByType(DisallowedCallsRuleErrors::class), + $container->getByType(DisallowedFunctionRuleErrors::class), $container->getByType(DisallowedCallFactory::class), - $this->createReflectionProvider(), - $container->getByType(Normalizer::class), - $container->getByType(TypeResolver::class), [ [ 'function' => 'md5_file()', diff --git a/tests/Calls/FunctionCallsDefinedInTest.php b/tests/Calls/FunctionCallsDefinedInTest.php index 2a5b45b..1762b7d 100644 --- a/tests/Calls/FunctionCallsDefinedInTest.php +++ b/tests/Calls/FunctionCallsDefinedInTest.php @@ -7,9 +7,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Testing\RuleTestCase; use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory; -use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer; -use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors; -use Spaze\PHPStan\Rules\Disallowed\Type\TypeResolver; +use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors; class FunctionCallsDefinedInTest extends RuleTestCase { @@ -21,11 +19,8 @@ protected function getRule(): Rule { $container = self::getContainer(); return new FunctionCalls( - $container->getByType(DisallowedCallsRuleErrors::class), + $container->getByType(DisallowedFunctionRuleErrors::class), $container->getByType(DisallowedCallFactory::class), - $this->createReflectionProvider(), - $container->getByType(Normalizer::class), - $container->getByType(TypeResolver::class), [ [ 'function' => '\\Foo\\Bar\\Waldo\\f*()', diff --git a/tests/Calls/FunctionCallsInMultipleNamespacesTest.php b/tests/Calls/FunctionCallsInMultipleNamespacesTest.php index b09c73b..c51eb8e 100644 --- a/tests/Calls/FunctionCallsInMultipleNamespacesTest.php +++ b/tests/Calls/FunctionCallsInMultipleNamespacesTest.php @@ -7,9 +7,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Testing\RuleTestCase; use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory; -use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer; -use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors; -use Spaze\PHPStan\Rules\Disallowed\Type\TypeResolver; +use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors; class FunctionCallsInMultipleNamespacesTest extends RuleTestCase { @@ -21,11 +19,8 @@ protected function getRule(): Rule { $container = self::getContainer(); return new FunctionCalls( - $container->getByType(DisallowedCallsRuleErrors::class), + $container->getByType(DisallowedFunctionRuleErrors::class), $container->getByType(DisallowedCallFactory::class), - $this->createReflectionProvider(), - $container->getByType(Normalizer::class), - $container->getByType(TypeResolver::class), [ [ 'function' => '__()', diff --git a/tests/Calls/FunctionCallsNamedParamsTest.php b/tests/Calls/FunctionCallsNamedParamsTest.php index 3a36c65..4ddd865 100644 --- a/tests/Calls/FunctionCallsNamedParamsTest.php +++ b/tests/Calls/FunctionCallsNamedParamsTest.php @@ -8,9 +8,7 @@ use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory; -use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer; -use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors; -use Spaze\PHPStan\Rules\Disallowed\Type\TypeResolver; +use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors; /** * @requires PHP >= 8.0 @@ -26,11 +24,8 @@ protected function getRule(): Rule { $container = self::getContainer(); return new FunctionCalls( - $container->getByType(DisallowedCallsRuleErrors::class), + $container->getByType(DisallowedFunctionRuleErrors::class), $container->getByType(DisallowedCallFactory::class), - $this->createReflectionProvider(), - $container->getByType(Normalizer::class), - $container->getByType(TypeResolver::class), [ [ 'function' => 'Foo\Bar\Waldo\foo()', diff --git a/tests/Calls/FunctionCallsParamsMessagesTest.php b/tests/Calls/FunctionCallsParamsMessagesTest.php index 8912088..11e41db 100644 --- a/tests/Calls/FunctionCallsParamsMessagesTest.php +++ b/tests/Calls/FunctionCallsParamsMessagesTest.php @@ -7,9 +7,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Testing\RuleTestCase; use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory; -use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer; -use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors; -use Spaze\PHPStan\Rules\Disallowed\Type\TypeResolver; +use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors; class FunctionCallsParamsMessagesTest extends RuleTestCase { @@ -21,11 +19,8 @@ protected function getRule(): Rule { $container = self::getContainer(); return new FunctionCalls( - $container->getByType(DisallowedCallsRuleErrors::class), + $container->getByType(DisallowedFunctionRuleErrors::class), $container->getByType(DisallowedCallFactory::class), - $this->createReflectionProvider(), - $container->getByType(Normalizer::class), - $container->getByType(TypeResolver::class), [ [ 'function' => '\Foo\Bar\Waldo\config()', diff --git a/tests/Calls/FunctionCallsTest.php b/tests/Calls/FunctionCallsTest.php index c123c64..b0a3bdb 100644 --- a/tests/Calls/FunctionCallsTest.php +++ b/tests/Calls/FunctionCallsTest.php @@ -7,9 +7,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Testing\RuleTestCase; use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory; -use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer; -use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors; -use Spaze\PHPStan\Rules\Disallowed\Type\TypeResolver; +use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors; use Waldo\Quux\Blade; class FunctionCallsTest extends RuleTestCase @@ -22,11 +20,8 @@ protected function getRule(): Rule { $container = self::getContainer(); return new FunctionCalls( - $container->getByType(DisallowedCallsRuleErrors::class), + $container->getByType(DisallowedFunctionRuleErrors::class), $container->getByType(DisallowedCallFactory::class), - $this->createReflectionProvider(), - $container->getByType(Normalizer::class), - $container->getByType(TypeResolver::class), [ [ 'function' => '\var_dump()', diff --git a/tests/Calls/FunctionCallsTypeStringParamsInvalidFlagsConfigTest.php b/tests/Calls/FunctionCallsTypeStringParamsInvalidFlagsConfigTest.php index 0572f22..731fc58 100644 --- a/tests/Calls/FunctionCallsTypeStringParamsInvalidFlagsConfigTest.php +++ b/tests/Calls/FunctionCallsTypeStringParamsInvalidFlagsConfigTest.php @@ -6,9 +6,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Testing\PHPStanTestCase; use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory; -use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer; -use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors; -use Spaze\PHPStan\Rules\Disallowed\Type\TypeResolver; +use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors; class FunctionCallsTypeStringParamsInvalidFlagsConfigTest extends PHPStanTestCase { @@ -22,11 +20,8 @@ public function testException(): void $this->expectExceptionMessage("Foo\Bar\Waldo\intParam1(): Parameter #1 has an unsupported type string of 2|'bruh' specified in configuration"); $container = self::getContainer(); new FunctionCalls( - $container->getByType(DisallowedCallsRuleErrors::class), + $container->getByType(DisallowedFunctionRuleErrors::class), $container->getByType(DisallowedCallFactory::class), - $this->createReflectionProvider(), - $container->getByType(Normalizer::class), - $container->getByType(TypeResolver::class), [ [ 'function' => '\Foo\Bar\Waldo\intParam1()', diff --git a/tests/Calls/FunctionCallsTypeStringParamsTest.php b/tests/Calls/FunctionCallsTypeStringParamsTest.php index 8263b38..607bca6 100644 --- a/tests/Calls/FunctionCallsTypeStringParamsTest.php +++ b/tests/Calls/FunctionCallsTypeStringParamsTest.php @@ -7,9 +7,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Testing\RuleTestCase; use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory; -use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer; -use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors; -use Spaze\PHPStan\Rules\Disallowed\Type\TypeResolver; +use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors; class FunctionCallsTypeStringParamsTest extends RuleTestCase { @@ -21,11 +19,8 @@ protected function getRule(): Rule { $container = self::getContainer(); return new FunctionCalls( - $container->getByType(DisallowedCallsRuleErrors::class), + $container->getByType(DisallowedFunctionRuleErrors::class), $container->getByType(DisallowedCallFactory::class), - $this->createReflectionProvider(), - $container->getByType(Normalizer::class), - $container->getByType(TypeResolver::class), [ [ 'function' => '\Foo\Bar\Waldo\config()', diff --git a/tests/Calls/FunctionCallsUnsupportedParamConfigTest.php b/tests/Calls/FunctionCallsUnsupportedParamConfigTest.php index e87ec27..ac69ece 100644 --- a/tests/Calls/FunctionCallsUnsupportedParamConfigTest.php +++ b/tests/Calls/FunctionCallsUnsupportedParamConfigTest.php @@ -6,9 +6,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Testing\PHPStanTestCase; use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory; -use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer; -use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors; -use Spaze\PHPStan\Rules\Disallowed\Type\TypeResolver; +use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors; class FunctionCallsUnsupportedParamConfigTest extends PHPStanTestCase { @@ -22,11 +20,8 @@ public function testUnsupportedArrayInParamConfig(): void $this->expectExceptionMessage('{foo(),bar()}: Parameter #2 $definitelyNotScalar has an unsupported type array specified in configuration'); $container = self::getContainer(); new FunctionCalls( - $container->getByType(DisallowedCallsRuleErrors::class), + $container->getByType(DisallowedFunctionRuleErrors::class), $container->getByType(DisallowedCallFactory::class), - $this->createReflectionProvider(), - $container->getByType(Normalizer::class), - $container->getByType(TypeResolver::class), [ [ 'function' => [ diff --git a/tests/Calls/FunctionFirstClassCallablesTest.php b/tests/Calls/FunctionFirstClassCallablesTest.php new file mode 100644 index 0000000..ffad2d1 --- /dev/null +++ b/tests/Calls/FunctionFirstClassCallablesTest.php @@ -0,0 +1,66 @@ +getByType(DisallowedFunctionRuleErrors::class), + $container->getByType(DisallowedCallFactory::class), + [ + [ + 'function' => 'print_r()', + 'message' => 'nope', + 'allowIn' => [ + __DIR__ . '/../src/disallowed-allow/*.php', + __DIR__ . '/../src/*-allow/*.*', + ], + ], + ] + ); + } + + + /** + * @requires PHP >= 8.1 + */ + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + // Based on the configuration above, in this file: + $this->analyse([__DIR__ . '/../src/disallowed/firstClassCallable.php'], [ + [ + // expect this error message: + 'Calling print_r() is forbidden, nope.', + // on this line: + 6, + ], + ]); + // Based on the configuration above, no errors in this file: + $this->analyse([__DIR__ . '/../src/disallowed-allow/firstClassCallable.php'], []); + } + + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../extension.neon', + ]; + } + +} diff --git a/tests/Calls/MethodFirstClassCallablesTest.php b/tests/Calls/MethodFirstClassCallablesTest.php new file mode 100644 index 0000000..068f1ad --- /dev/null +++ b/tests/Calls/MethodFirstClassCallablesTest.php @@ -0,0 +1,70 @@ +getByType(DisallowedMethodRuleErrors::class), + $container->getByType(DisallowedCallFactory::class), + [ + [ + 'method' => 'Waldo\Quux\Blade::run*()', + 'message' => "I've seen tests you people wouldn't believe", + 'allowIn' => [ + __DIR__ . '/../src/disallowed-allow/*.php', + __DIR__ . '/../src/*-allow/*.*', + ], + ], + ] + ); + } + + + /** + * @requires PHP >= 8.1 + */ + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + // Based on the configuration above, in this file: + $this->analyse([__DIR__ . '/../src/disallowed/firstClassCallable.php'], [ + [ + // expect this error message: + "Calling Waldo\Quux\Blade::runner() is forbidden, I've seen tests you people wouldn't believe. [Waldo\Quux\Blade::runner() matches Waldo\Quux\Blade::run*()]", + // on this line: + 10, + ], + [ + "Calling Waldo\Quux\Blade::runner() is forbidden, I've seen tests you people wouldn't believe. [Waldo\Quux\Blade::runner() matches Waldo\Quux\Blade::run*()]", + 13, + ], + ]); + // Based on the configuration above, no errors in this file: + $this->analyse([__DIR__ . '/../src/disallowed-allow/firstClassCallable.php'], []); + } + + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../extension.neon', + ]; + } + +} diff --git a/tests/Calls/StaticFirstClassCallablesTest.php b/tests/Calls/StaticFirstClassCallablesTest.php new file mode 100644 index 0000000..7f0c954 --- /dev/null +++ b/tests/Calls/StaticFirstClassCallablesTest.php @@ -0,0 +1,78 @@ +getByType(DisallowedMethodRuleErrors::class), + $container->getByType(DisallowedCallFactory::class), + [ + [ + 'method' => 'Fiction\Pulp\Royale::withoutCheese', + 'message' => 'a Quarter Pounder without Cheese!', + 'allowIn' => [ + __DIR__ . '/../src/disallowed-allow/*.php', + __DIR__ . '/../src/*-allow/*.*', + ], + ], + ] + ); + } + + + /** + * @requires PHP >= 8.1 + */ + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + // Based on the configuration above, in this file: + $this->analyse([__DIR__ . '/../src/disallowed/firstClassCallable.php'], [ + [ + // expect this error message: + 'Calling Fiction\Pulp\Royale::withoutCheese() is forbidden, a Quarter Pounder without Cheese!', + // on this line: + 16, + ], + [ + 'Calling Fiction\Pulp\Royale::withoutCheese() is forbidden, a Quarter Pounder without Cheese!', + 19, + ], + [ + 'Calling Fiction\Pulp\Royale::withoutCheese() is forbidden, a Quarter Pounder without Cheese!', + 23, + ], + [ + 'Calling Fiction\Pulp\Royale::withoutCheese() is forbidden, a Quarter Pounder without Cheese!', + 26, + ], + ]); + // Based on the configuration above, no errors in this file: + $this->analyse([__DIR__ . '/../src/disallowed-allow/firstClassCallable.php'], []); + } + + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../extension.neon', + ]; + } + +} diff --git a/tests/src/disallowed-allow/firstClassCallable.php b/tests/src/disallowed-allow/firstClassCallable.php new file mode 100644 index 0000000..c17614b --- /dev/null +++ b/tests/src/disallowed-allow/firstClassCallable.php @@ -0,0 +1,27 @@ +runner(...); +$callable(303); +$method = 'runner'; +$callable = $blade->$method(...); +$callable(808); + +$callable = \Fiction\Pulp\Royale::withoutCheese(...); +$callable(303); +$method = 'withoutCheese'; +$callable = \Fiction\Pulp\Royale::$method(...); +$callable(808); + +$class = \Fiction\Pulp\Royale::class; +$callable = $class::withoutCheese(...); +$callable(303); +$method = 'withoutCheese'; +$callable = $class::$method(...); +$callable(808); diff --git a/tests/src/disallowed/firstClassCallable.php b/tests/src/disallowed/firstClassCallable.php new file mode 100644 index 0000000..a4c236f --- /dev/null +++ b/tests/src/disallowed/firstClassCallable.php @@ -0,0 +1,27 @@ +runner(...); +$callable(303); +$method = 'runner'; +$callable = $blade->$method(...); +$callable(808); + +$callable = \Fiction\Pulp\Royale::withoutCheese(...); +$callable(303); +$method = 'withoutCheese'; +$callable = \Fiction\Pulp\Royale::$method(...); +$callable(808); + +$class = \Fiction\Pulp\Royale::class; +$callable = $class::withoutCheese(...); +$callable(303); +$method = 'withoutCheese'; +$callable = $class::$method(...); +$callable(808); From 2b9fc3342ac637d5496bf9f46b444f8aec31e94b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Sun, 8 Dec 2024 02:51:24 +0100 Subject: [PATCH 2/3] Exclude the test files with first class callables from linting on older PHPs --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index e52f87b..d187535 100644 --- a/composer.json +++ b/composer.json @@ -41,9 +41,9 @@ }, "scripts": { "lint": "vendor/bin/parallel-lint --colors src/ tests/", - "lint-7.x": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/TypesEverywhere.php --exclude tests/src/AttributesEverywhere.php --exclude tests/src/disallowed/functionCallsNamedParams.php --exclude tests/src/disallowed-allow/functionCallsNamedParams.php --exclude tests/src/disallowed/attributeUsages.php --exclude tests/src/disallowed-allow/attributeUsages.php --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php --exclude tests/src/Enums.php --exclude tests/src/disallowed/controlStructures.php --exclude tests/src/disallowed-allow/controlStructures.php", - "lint-8.0": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/TypesEverywhere.php --exclude tests/src/AttributesEverywhere.php --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php --exclude tests/src/Enums.php", - "lint-8.1": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/AttributesEverywhere.php --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php", + "lint-7.x": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/TypesEverywhere.php --exclude tests/src/AttributesEverywhere.php --exclude tests/src/disallowed/functionCallsNamedParams.php --exclude tests/src/disallowed-allow/functionCallsNamedParams.php --exclude tests/src/disallowed/attributeUsages.php --exclude tests/src/disallowed-allow/attributeUsages.php --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php --exclude tests/src/Enums.php --exclude tests/src/disallowed/controlStructures.php --exclude tests/src/disallowed-allow/controlStructures.php --exclude tests/src/disallowed/firstClassCallable.php --exclude tests/src/disallowed-allow/firstClassCallable.php", + "lint-8.0": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/TypesEverywhere.php --exclude tests/src/AttributesEverywhere.php --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php --exclude tests/src/Enums.php --exclude tests/src/disallowed/firstClassCallable.php --exclude tests/src/disallowed-allow/firstClassCallable.php", + "lint-8.1": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/AttributesEverywhere.php --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php --exclude tests/src/disallowed/firstClassCallable.php --exclude tests/src/disallowed-allow/firstClassCallable.php", "lint-8.2": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php", "lint-neon": "vendor/bin/neon-lint .", "phpcs": "vendor/bin/phpcs src/ tests/", From 4b55ba5b8f78e5485e8502cf0d3ae33a927fdc0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Sun, 8 Dec 2024 05:38:32 +0100 Subject: [PATCH 3/3] PHPStan 1.x doesn't have Type::isClassString() but has Type::isClassStringType() Ref #273 --- phpstan.neon | 3 ++ src/PHPStan1Compatibility.php | 31 +++++++++++++++++++ src/RuleErrors/DisallowedMethodRuleErrors.php | 3 +- 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 src/PHPStan1Compatibility.php diff --git a/phpstan.neon b/phpstan.neon index 9b15f43..362ceb3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -10,6 +10,9 @@ parameters: ForbiddenCallsConfig: 'array, method?:string|list, exclude?:string|list, definedIn?:string|list, message?:string, %typeAliases.AllowDirectives%, errorIdentifier?:string, errorTip?:string}>' DisallowedAttributesConfig: 'array, exclude?:string|list, message?:string, %typeAliases.AllowDirectives%, errorIdentifier?:string, errorTip?:string}>' AllowDirectivesConfig: 'array{%typeAliases.AllowDirectives%}' + excludePaths: + analyse: + - src/PHPStan1Compatibility.php includes: - vendor/phpstan/phpstan/conf/bleedingEdge.neon diff --git a/src/PHPStan1Compatibility.php b/src/PHPStan1Compatibility.php new file mode 100644 index 0000000..da2a3e3 --- /dev/null +++ b/src/PHPStan1Compatibility.php @@ -0,0 +1,31 @@ +isClassStringType(); + } else { + // PHPStan 2.x + return $type->isClassString(); + } + } + +} diff --git a/src/RuleErrors/DisallowedMethodRuleErrors.php b/src/RuleErrors/DisallowedMethodRuleErrors.php index c5055f3..fd4469b 100644 --- a/src/RuleErrors/DisallowedMethodRuleErrors.php +++ b/src/RuleErrors/DisallowedMethodRuleErrors.php @@ -17,6 +17,7 @@ use PHPStan\ShouldNotHappenException; use Spaze\PHPStan\Rules\Disallowed\DisallowedCall; use Spaze\PHPStan\Rules\Disallowed\Formatter\Formatter; +use Spaze\PHPStan\Rules\Disallowed\PHPStan1Compatibility; use Spaze\PHPStan\Rules\Disallowed\Type\TypeResolver; class DisallowedMethodRuleErrors @@ -62,7 +63,7 @@ public function get($class, CallLike $node, Scope $scope, array $disallowedCalls } $calledOnType = $this->typeResolver->getType($class, $scope); - if ($calledOnType->isClassString()->yes()) { + if (PHPStan1Compatibility::isClassString($calledOnType)->yes()) { $calledOnType = $calledOnType->getClassStringObjectType(); } if ($calledOnType->canCallMethods()->yes() && $calledOnType->hasMethod($methodName)->yes()) {