Skip to content

Commit

Permalink
Bleeding edge - run missing type check on @param-out
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Mar 26, 2024
1 parent f46c11c commit 56b2002
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 36 deletions.
17 changes: 15 additions & 2 deletions conf/config.level6.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions src/PhpDoc/StubValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
];
Expand Down
36 changes: 23 additions & 13 deletions src/Rules/Functions/MissingFunctionParameterTypehintRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,6 +25,7 @@ final class MissingFunctionParameterTypehintRule implements Rule

public function __construct(
private MissingTypehintCheck $missingTypehintCheck,
private bool $paramOut,
)
{
}
Expand All @@ -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;
}
}
Expand All @@ -51,16 +63,14 @@ public function processNode(Node $node, Scope $scope): array
/**
* @return list<IdentifierRuleError>
*/
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(),
];
}
Expand All @@ -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)
Expand All @@ -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),
))
Expand All @@ -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();
}
Expand Down
40 changes: 26 additions & 14 deletions src/Rules/Methods/MissingMethodParameterTypehintRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,7 +23,10 @@
final class MissingMethodParameterTypehintRule implements Rule
{

public function __construct(private MissingTypehintCheck $missingTypehintCheck)
public function __construct(
private MissingTypehintCheck $missingTypehintCheck,
private bool $paramOut,
)
{
}

Expand All @@ -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;
}
}
Expand All @@ -49,17 +63,15 @@ public function processNode(Node $node, Scope $scope): array
/**
* @return list<IdentifierRuleError>
*/
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(),
];
}
Expand All @@ -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)
Expand All @@ -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),
))
Expand All @@ -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();
}
Expand Down
6 changes: 3 additions & 3 deletions stubs/core.stub
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,13 @@ function str_shuffle(string $string): string {}

/**
* @param array<mixed> $result
* @param-out array<int|string, array|string> $result
* @param-out array<int|string, array<mixed>|string> $result
*/
function parse_str(string $string, array &$result): void {}

/**
* @param array<mixed> $result
* @param-out array<string, array|string> $result
* @param-out array<string, array<mixed>|string> $result
*/
function mb_parse_str(string $string, array &$result): bool {}

Expand Down Expand Up @@ -193,7 +193,7 @@ function sscanf(string $string, string $format, &$war, &...$vars) {}
* ? list<array<?string>>
* : (TFlags is 770
* ? list<array<array{?string, int}>>
* : array
* : array<mixed>
* )
* )
* )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <fg=cyan>checkGenericClassInNonGenericObjectType: false</> in your <fg=cyan>%configurationFile%</>.',
],
]);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,21 @@ function missingCallableSignature(callable $cb)
}

}

namespace MissingParamOutType {
/**
* @param array<int> $a
* @param-out array $a
*/
function oneArray(&$a): void {

}

/**
* @param mixed $a
* @param-out \ReflectionClass $a
*/
function generics(&$a): void {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <fg=cyan>checkGenericClassInNonGenericObjectType: false</> in your <fg=cyan>%configurationFile%</>.',
],
];

$this->analyse([__DIR__ . '/data/missing-method-parameter-typehint.php'], $errors);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,22 @@ public function unserialize($data): void
}

}

class MissingParamOutType {

/**
* @param array<int> $a
* @param-out array $a
*/
function oneArray(&$a): void {

}

/**
* @param mixed $a
* @param-out \ReflectionClass $a
*/
function generics(&$a): void {

}
}

0 comments on commit 56b2002

Please sign in to comment.