diff --git a/conf/config.level2.neon b/conf/config.level2.neon index 907c83e394..347bdb2629 100644 --- a/conf/config.level2.neon +++ b/conf/config.level2.neon @@ -40,6 +40,7 @@ rules: - PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeRule - PHPStan\Rules\PhpDoc\IncompatiblePropertyPhpDocTypeRule - PHPStan\Rules\PhpDoc\InvalidThrowsPhpDocValueRule + - PHPStan\Rules\PhpDoc\IncompatibleParamImmediatelyInvokedCallableRule - PHPStan\Rules\Properties\AccessPrivatePropertyThroughStaticRule - PHPStan\Rules\Classes\RequireImplementsRule - PHPStan\Rules\Classes\RequireExtendsRule diff --git a/src/PhpDoc/StubValidator.php b/src/PhpDoc/StubValidator.php index 91ff9d9360..8489946fa7 100644 --- a/src/PhpDoc/StubValidator.php +++ b/src/PhpDoc/StubValidator.php @@ -54,6 +54,7 @@ use PHPStan\Rules\Methods\OverridingMethodRule; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\PhpDoc\GenericCallableRuleHelper; +use PHPStan\Rules\PhpDoc\IncompatibleParamImmediatelyInvokedCallableRule; use PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeRule; use PHPStan\Rules\PhpDoc\IncompatiblePropertyPhpDocTypeRule; use PHPStan\Rules\PhpDoc\InvalidPhpDocTagValueRule; @@ -197,6 +198,7 @@ private function getRuleRegistry(Container $container): RuleRegistry $container->getParameter('featureToggles')['allInvalidPhpDocs'], $container->getParameter('featureToggles')['invalidPhpDocTagLine'], ), + new IncompatibleParamImmediatelyInvokedCallableRule($fileTypeMapper), new InvalidThrowsPhpDocValueRule($fileTypeMapper), // level 6 diff --git a/src/Rules/PhpDoc/IncompatibleParamImmediatelyInvokedCallableRule.php b/src/Rules/PhpDoc/IncompatibleParamImmediatelyInvokedCallableRule.php new file mode 100644 index 0000000000..fa6f36463c --- /dev/null +++ b/src/Rules/PhpDoc/IncompatibleParamImmediatelyInvokedCallableRule.php @@ -0,0 +1,94 @@ + + */ +final class IncompatibleParamImmediatelyInvokedCallableRule implements Rule +{ + + public function __construct( + private FileTypeMapper $fileTypeMapper, + ) + { + } + + public function getNodeType(): string + { + return FunctionLike::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node instanceof Node\Stmt\ClassMethod) { + $functionName = $node->name->name; + } elseif ($node instanceof Node\Stmt\Function_) { + $functionName = trim($scope->getNamespace() . '\\' . $node->name->name, '\\'); + } else { + return []; + } + + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; + } + + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $scope->isInClass() ? $scope->getClassReflection()->getName() : null, + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $functionName, + $docComment->getText(), + ); + $nativeParameterTypes = []; + foreach ($node->getParams() as $parameter) { + if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { + throw new ShouldNotHappenException(); + } + $nativeParameterTypes[$parameter->var->name] = $scope->getFunctionType( + $parameter->type, + $scope->isParameterValueNullable($parameter), + false, + ); + } + + $errors = []; + foreach ($resolvedPhpDoc->getParamsImmediatelyInvokedCallable() as $parameterName => $immediately) { + $tagName = $immediately ? '@param-immediately-invoked-callable' : '@param-later-invoked-callable'; + if (!isset($nativeParameterTypes[$parameterName])) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s references unknown parameter: $%s', + $tagName, + $parameterName, + ))->identifier('parameter.notFound')->build(); + } elseif ($nativeParameterTypes[$parameterName]->isCallable()->no()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s is for parameter $%s with non-callable type %s.', + $tagName, + $parameterName, + $nativeParameterTypes[$parameterName]->describe(VerbosityLevel::typeOnly()), + ))->identifier(sprintf( + '%s.nonCallable', + $immediately ? 'paramImmediatelyInvokedCallable' : 'paramLaterInvokedCallable', + ))->build(); + } + } + + return $errors; + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatibleParamImmediatelyInvokedCallableRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatibleParamImmediatelyInvokedCallableRuleTest.php new file mode 100644 index 0000000000..d19443e644 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/IncompatibleParamImmediatelyInvokedCallableRuleTest.php @@ -0,0 +1,60 @@ + + */ +class IncompatibleParamImmediatelyInvokedCallableRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new IncompatibleParamImmediatelyInvokedCallableRule( + self::getContainer()->getByType(FileTypeMapper::class), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/incompatible-param-immediately-invoked-callable.php'], [ + [ + 'PHPDoc tag @param-immediately-invoked-callable references unknown parameter: $b', + 21, + ], + [ + 'PHPDoc tag @param-later-invoked-callable references unknown parameter: $c', + 21, + ], + [ + 'PHPDoc tag @param-immediately-invoked-callable is for parameter $b with non-callable type int.', + 30, + ], + [ + 'PHPDoc tag @param-later-invoked-callable is for parameter $b with non-callable type int.', + 39, + ], + [ + 'PHPDoc tag @param-immediately-invoked-callable references unknown parameter: $b', + 59, + ], + [ + 'PHPDoc tag @param-later-invoked-callable references unknown parameter: $c', + 59, + ], + [ + 'PHPDoc tag @param-immediately-invoked-callable is for parameter $b with non-callable type int.', + 68, + ], + [ + 'PHPDoc tag @param-later-invoked-callable is for parameter $b with non-callable type int.', + 77, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-param-immediately-invoked-callable.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-param-immediately-invoked-callable.php new file mode 100644 index 0000000000..79cd19f116 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-param-immediately-invoked-callable.php @@ -0,0 +1,80 @@ +