From 56b20024386d983927c64dfa895ff026bed2798c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 26 Mar 2024 14:30:05 +0100 Subject: [PATCH] Bleeding edge - run missing type check on `@param-out` --- conf/config.level6.neon | 17 +++++++- src/PhpDoc/StubValidator.php | 4 +- .../MissingFunctionParameterTypehintRule.php | 36 +++++++++++------ .../MissingMethodParameterTypehintRule.php | 40 ++++++++++++------- stubs/core.stub | 6 +-- ...ssingFunctionParameterTypehintRuleTest.php | 12 +++++- .../missing-function-parameter-typehint.php | 18 +++++++++ ...MissingMethodParameterTypehintRuleTest.php | 12 +++++- .../missing-method-parameter-typehint.php | 19 +++++++++ 9 files changed, 128 insertions(+), 36 deletions(-) diff --git a/conf/config.level6.neon b/conf/config.level6.neon index 05f3616832..545fac6ad2 100644 --- a/conf/config.level6.neon +++ b/conf/config.level6.neon @@ -9,8 +9,21 @@ parameters: rules: - PHPStan\Rules\Constants\MissingClassConstantTypehintRule - - PHPStan\Rules\Functions\MissingFunctionParameterTypehintRule - PHPStan\Rules\Functions\MissingFunctionReturnTypehintRule - - PHPStan\Rules\Methods\MissingMethodParameterTypehintRule - PHPStan\Rules\Methods\MissingMethodReturnTypehintRule - PHPStan\Rules\Properties\MissingPropertyTypehintRule + +services: + - + class: PHPStan\Rules\Functions\MissingFunctionParameterTypehintRule + arguments: + paramOut: %featureToggles.paramOutType% + tags: + - phpstan.rules.rule + + - + class: PHPStan\Rules\Methods\MissingMethodParameterTypehintRule + arguments: + paramOut: %featureToggles.paramOutType% + tags: + - phpstan.rules.rule diff --git a/src/PhpDoc/StubValidator.php b/src/PhpDoc/StubValidator.php index f7948a09df..137a825b06 100644 --- a/src/PhpDoc/StubValidator.php +++ b/src/PhpDoc/StubValidator.php @@ -194,9 +194,9 @@ private function getRuleRegistry(Container $container): RuleRegistry new InvalidThrowsPhpDocValueRule($fileTypeMapper), // level 6 - new MissingFunctionParameterTypehintRule($missingTypehintCheck), + new MissingFunctionParameterTypehintRule($missingTypehintCheck, $container->getParameter('featureToggles')['paramOutType']), new MissingFunctionReturnTypehintRule($missingTypehintCheck), - new MissingMethodParameterTypehintRule($missingTypehintCheck), + new MissingMethodParameterTypehintRule($missingTypehintCheck, $container->getParameter('featureToggles')['paramOutType']), new MissingMethodReturnTypehintRule($missingTypehintCheck), new MissingPropertyTypehintRule($missingTypehintCheck), ]; diff --git a/src/Rules/Functions/MissingFunctionParameterTypehintRule.php b/src/Rules/Functions/MissingFunctionParameterTypehintRule.php index 1cafd36892..6997a05d4e 100644 --- a/src/Rules/Functions/MissingFunctionParameterTypehintRule.php +++ b/src/Rules/Functions/MissingFunctionParameterTypehintRule.php @@ -6,13 +6,13 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InFunctionNode; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function implode; use function sprintf; @@ -25,6 +25,7 @@ final class MissingFunctionParameterTypehintRule implements Rule public function __construct( private MissingTypehintCheck $missingTypehintCheck, + private bool $paramOut, ) { } @@ -40,7 +41,18 @@ public function processNode(Node $node, Scope $scope): array $messages = []; foreach (ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getParameters() as $parameterReflection) { - foreach ($this->checkFunctionParameter($functionReflection, $parameterReflection) as $parameterMessage) { + foreach ($this->checkFunctionParameter($functionReflection, sprintf('parameter $%s', $parameterReflection->getName()), $parameterReflection->getType()) as $parameterMessage) { + $messages[] = $parameterMessage; + } + + if (!$this->paramOut) { + continue; + } + if ($parameterReflection->getOutType() === null) { + continue; + } + + foreach ($this->checkFunctionParameter($functionReflection, sprintf('@param-out PHPDoc tag for parameter $%s', $parameterReflection->getName()), $parameterReflection->getOutType()) as $parameterMessage) { $messages[] = $parameterMessage; } } @@ -51,16 +63,14 @@ public function processNode(Node $node, Scope $scope): array /** * @return list */ - private function checkFunctionParameter(FunctionReflection $functionReflection, ParameterReflection $parameterReflection): array + private function checkFunctionParameter(FunctionReflection $functionReflection, string $parameterMessage, Type $parameterType): array { - $parameterType = $parameterReflection->getType(); - if ($parameterType instanceof MixedType && !$parameterType->isExplicitMixed()) { return [ RuleErrorBuilder::message(sprintf( - 'Function %s() has parameter $%s with no type specified.', + 'Function %s() has %s with no type specified.', $functionReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, ))->identifier('missingType.parameter')->build(), ]; } @@ -69,9 +79,9 @@ private function checkFunctionParameter(FunctionReflection $functionReflection, foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($parameterType) as $iterableType) { $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); $messages[] = RuleErrorBuilder::message(sprintf( - 'Function %s() has parameter $%s with no value type specified in iterable type %s.', + 'Function %s() has %s with no value type specified in iterable type %s.', $functionReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $iterableTypeDescription, )) ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) @@ -81,9 +91,9 @@ private function checkFunctionParameter(FunctionReflection $functionReflection, foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($parameterType) as [$name, $genericTypeNames]) { $messages[] = RuleErrorBuilder::message(sprintf( - 'Function %s() has parameter $%s with generic %s but does not specify its types: %s', + 'Function %s() has %s with generic %s but does not specify its types: %s', $functionReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $name, implode(', ', $genericTypeNames), )) @@ -94,9 +104,9 @@ private function checkFunctionParameter(FunctionReflection $functionReflection, foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($parameterType) as $callableType) { $messages[] = RuleErrorBuilder::message(sprintf( - 'Function %s() has parameter $%s with no signature specified for %s.', + 'Function %s() has %s with no signature specified for %s.', $functionReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $callableType->describe(VerbosityLevel::typeOnly()), ))->identifier('missingType.callable')->build(); } diff --git a/src/Rules/Methods/MissingMethodParameterTypehintRule.php b/src/Rules/Methods/MissingMethodParameterTypehintRule.php index 7b2ffe290e..4122b3af3f 100644 --- a/src/Rules/Methods/MissingMethodParameterTypehintRule.php +++ b/src/Rules/Methods/MissingMethodParameterTypehintRule.php @@ -6,13 +6,13 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InClassMethodNode; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function implode; use function sprintf; @@ -23,7 +23,10 @@ final class MissingMethodParameterTypehintRule implements Rule { - public function __construct(private MissingTypehintCheck $missingTypehintCheck) + public function __construct( + private MissingTypehintCheck $missingTypehintCheck, + private bool $paramOut, + ) { } @@ -38,7 +41,18 @@ public function processNode(Node $node, Scope $scope): array $messages = []; foreach (ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getParameters() as $parameterReflection) { - foreach ($this->checkMethodParameter($methodReflection, $parameterReflection) as $parameterMessage) { + foreach ($this->checkMethodParameter($methodReflection, sprintf('parameter $%s', $parameterReflection->getName()), $parameterReflection->getType()) as $parameterMessage) { + $messages[] = $parameterMessage; + } + + if (!$this->paramOut) { + continue; + } + if ($parameterReflection->getOutType() === null) { + continue; + } + + foreach ($this->checkMethodParameter($methodReflection, sprintf('@param-out PHPDoc tag for parameter $%s', $parameterReflection->getName()), $parameterReflection->getOutType()) as $parameterMessage) { $messages[] = $parameterMessage; } } @@ -49,17 +63,15 @@ public function processNode(Node $node, Scope $scope): array /** * @return list */ - private function checkMethodParameter(MethodReflection $methodReflection, ParameterReflection $parameterReflection): array + private function checkMethodParameter(MethodReflection $methodReflection, string $parameterMessage, Type $parameterType): array { - $parameterType = $parameterReflection->getType(); - if ($parameterType instanceof MixedType && !$parameterType->isExplicitMixed()) { return [ RuleErrorBuilder::message(sprintf( - 'Method %s::%s() has parameter $%s with no type specified.', + 'Method %s::%s() has %s with no type specified.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, ))->identifier('missingType.parameter')->build(), ]; } @@ -68,10 +80,10 @@ private function checkMethodParameter(MethodReflection $methodReflection, Parame foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($parameterType) as $iterableType) { $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); $messages[] = RuleErrorBuilder::message(sprintf( - 'Method %s::%s() has parameter $%s with no value type specified in iterable type %s.', + 'Method %s::%s() has %s with no value type specified in iterable type %s.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $iterableTypeDescription, )) ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) @@ -81,10 +93,10 @@ private function checkMethodParameter(MethodReflection $methodReflection, Parame foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($parameterType) as [$name, $genericTypeNames]) { $messages[] = RuleErrorBuilder::message(sprintf( - 'Method %s::%s() has parameter $%s with generic %s but does not specify its types: %s', + 'Method %s::%s() has %s with generic %s but does not specify its types: %s', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $name, implode(', ', $genericTypeNames), )) @@ -95,10 +107,10 @@ private function checkMethodParameter(MethodReflection $methodReflection, Parame foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($parameterType) as $callableType) { $messages[] = RuleErrorBuilder::message(sprintf( - 'Method %s::%s() has parameter $%s with no signature specified for %s.', + 'Method %s::%s() has %s with no signature specified for %s.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $callableType->describe(VerbosityLevel::typeOnly()), ))->identifier('missingType.callable')->build(); } diff --git a/stubs/core.stub b/stubs/core.stub index c38bd53226..0c6c6236d7 100644 --- a/stubs/core.stub +++ b/stubs/core.stub @@ -75,13 +75,13 @@ function str_shuffle(string $string): string {} /** * @param array $result - * @param-out array $result + * @param-out array|string> $result */ function parse_str(string $string, array &$result): void {} /** * @param array $result - * @param-out array $result + * @param-out array|string> $result */ function mb_parse_str(string $string, array &$result): bool {} @@ -193,7 +193,7 @@ function sscanf(string $string, string $format, &$war, &...$vars) {} * ? list> * : (TFlags is 770 * ? list> - * : array + * : array * ) * ) * ) diff --git a/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php index db5cc3d857..ca3f4f39f7 100644 --- a/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php @@ -14,7 +14,7 @@ class MissingFunctionParameterTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - return new MissingFunctionParameterTypehintRule(new MissingTypehintCheck(true, true, true, true, [])); + return new MissingFunctionParameterTypehintRule(new MissingTypehintCheck(true, true, true, true, []), true); } public function testRule(): void @@ -82,6 +82,16 @@ public function testRule(): void 'Function MissingFunctionParameterTypehint\missingCallableSignature() has parameter $cb with no signature specified for callable.', 161, ], + [ + 'Function MissingParamOutType\oneArray() has @param-out PHPDoc tag for parameter $a with no value type specified in iterable type array.', + 173, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], + [ + 'Function MissingParamOutType\generics() has @param-out PHPDoc tag for parameter $a with generic class ReflectionClass but does not specify its types: T', + 181, + 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/missing-function-parameter-typehint.php b/tests/PHPStan/Rules/Functions/data/missing-function-parameter-typehint.php index 87bfdff02a..54a127ccf3 100644 --- a/tests/PHPStan/Rules/Functions/data/missing-function-parameter-typehint.php +++ b/tests/PHPStan/Rules/Functions/data/missing-function-parameter-typehint.php @@ -164,3 +164,21 @@ function missingCallableSignature(callable $cb) } } + +namespace MissingParamOutType { + /** + * @param array $a + * @param-out array $a + */ + function oneArray(&$a): void { + + } + + /** + * @param mixed $a + * @param-out \ReflectionClass $a + */ + function generics(&$a): void { + + } +} diff --git a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php index f311149397..8e885f360e 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php @@ -14,7 +14,7 @@ class MissingMethodParameterTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - return new MissingMethodParameterTypehintRule(new MissingTypehintCheck(true, true, true, true, [])); + return new MissingMethodParameterTypehintRule(new MissingTypehintCheck(true, true, true, true, []), true); } public function testRule(): void @@ -69,6 +69,16 @@ public function testRule(): void 'Method MissingMethodParameterTypehint\CallableSignature::doFoo() has parameter $cb with no signature specified for callable.', 180, ], + [ + 'Method MissingMethodParameterTypehint\MissingParamOutType::oneArray() has @param-out PHPDoc tag for parameter $a with no value type specified in iterable type array.', + 207, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], + [ + 'Method MissingMethodParameterTypehint\MissingParamOutType::generics() has @param-out PHPDoc tag for parameter $a with generic class ReflectionClass but does not specify its types: T', + 215, + 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + ], ]; $this->analyse([__DIR__ . '/data/missing-method-parameter-typehint.php'], $errors); diff --git a/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php b/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php index 8346689258..2554cc5bbc 100644 --- a/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php +++ b/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php @@ -197,3 +197,22 @@ public function unserialize($data): void } } + +class MissingParamOutType { + + /** + * @param array $a + * @param-out array $a + */ + function oneArray(&$a): void { + + } + + /** + * @param mixed $a + * @param-out \ReflectionClass $a + */ + function generics(&$a): void { + + } +}