Skip to content

Commit

Permalink
Check @param-immediately-invoked-callable and `@param-later-invoked…
Browse files Browse the repository at this point in the history
…-callable`
  • Loading branch information
ondrejmirtes committed Aug 23, 2024
1 parent 95c0a58 commit 580a6ad
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 0 deletions.
1 change: 1 addition & 0 deletions conf/config.level2.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/PhpDoc/StubValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\PhpDoc;

use PhpParser\Node;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\FunctionLike;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\FileTypeMapper;
use PHPStan\Type\VerbosityLevel;
use function is_string;
use function sprintf;
use function trim;

/**
* @implements Rule<FunctionLike>
*/
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;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\PhpDoc;

use PHPStan\Rules\Rule as TRule;
use PHPStan\Testing\RuleTestCase;
use PHPStan\Type\FileTypeMapper;

/**
* @extends RuleTestCase<IncompatibleParamImmediatelyInvokedCallableRule>
*/
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,
],
]);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

namespace IncompatibleParamImmediatelyInvokedCallable;

class Foo
{

/**
* @param-immediately-invoked-callable $a
* @param-later-invoked-callable $b
*/
public function doFoo(callable $a, callable $b): void
{

}

/**
* @param-immediately-invoked-callable $b
* @param-later-invoked-callable $c
*/
public function doBar(callable $a): void
{

}

/**
* @param-immediately-invoked-callable $a
* @param-immediately-invoked-callable $b
*/
public function doBaz(string $a, int $b): void
{

}

/**
* @param-later-invoked-callable $a
* @param-later-invoked-callable $b
*/
public function doBaz2(string $a, int $b): void
{

}

}

/**
* @param-immediately-invoked-callable $a
* @param-later-invoked-callable $b
*/
function doFoo(callable $a, callable $b): void
{

}

/**
* @param-immediately-invoked-callable $b
* @param-later-invoked-callable $c
*/
function doBar(callable $a): void
{

}

/**
* @param-immediately-invoked-callable $a
* @param-immediately-invoked-callable $b
*/
function doBaz(string $a, int $b): void
{

}

/**
* @param-later-invoked-callable $a
* @param-later-invoked-callable $b
*/
function doBaz2(string $a, int $b): void
{

}

0 comments on commit 580a6ad

Please sign in to comment.