Skip to content

Commit 580a6ad

Browse files
committed
Check @param-immediately-invoked-callable and @param-later-invoked-callable
1 parent 95c0a58 commit 580a6ad

5 files changed

+237
-0
lines changed

conf/config.level2.neon

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ rules:
4040
- PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeRule
4141
- PHPStan\Rules\PhpDoc\IncompatiblePropertyPhpDocTypeRule
4242
- PHPStan\Rules\PhpDoc\InvalidThrowsPhpDocValueRule
43+
- PHPStan\Rules\PhpDoc\IncompatibleParamImmediatelyInvokedCallableRule
4344
- PHPStan\Rules\Properties\AccessPrivatePropertyThroughStaticRule
4445
- PHPStan\Rules\Classes\RequireImplementsRule
4546
- PHPStan\Rules\Classes\RequireExtendsRule

src/PhpDoc/StubValidator.php

+2
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
use PHPStan\Rules\Methods\OverridingMethodRule;
5555
use PHPStan\Rules\MissingTypehintCheck;
5656
use PHPStan\Rules\PhpDoc\GenericCallableRuleHelper;
57+
use PHPStan\Rules\PhpDoc\IncompatibleParamImmediatelyInvokedCallableRule;
5758
use PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeRule;
5859
use PHPStan\Rules\PhpDoc\IncompatiblePropertyPhpDocTypeRule;
5960
use PHPStan\Rules\PhpDoc\InvalidPhpDocTagValueRule;
@@ -197,6 +198,7 @@ private function getRuleRegistry(Container $container): RuleRegistry
197198
$container->getParameter('featureToggles')['allInvalidPhpDocs'],
198199
$container->getParameter('featureToggles')['invalidPhpDocTagLine'],
199200
),
201+
new IncompatibleParamImmediatelyInvokedCallableRule($fileTypeMapper),
200202
new InvalidThrowsPhpDocValueRule($fileTypeMapper),
201203

202204
// level 6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PhpDoc;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\Variable;
7+
use PhpParser\Node\FunctionLike;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Rules\Rule;
10+
use PHPStan\Rules\RuleErrorBuilder;
11+
use PHPStan\ShouldNotHappenException;
12+
use PHPStan\Type\FileTypeMapper;
13+
use PHPStan\Type\VerbosityLevel;
14+
use function is_string;
15+
use function sprintf;
16+
use function trim;
17+
18+
/**
19+
* @implements Rule<FunctionLike>
20+
*/
21+
final class IncompatibleParamImmediatelyInvokedCallableRule implements Rule
22+
{
23+
24+
public function __construct(
25+
private FileTypeMapper $fileTypeMapper,
26+
)
27+
{
28+
}
29+
30+
public function getNodeType(): string
31+
{
32+
return FunctionLike::class;
33+
}
34+
35+
public function processNode(Node $node, Scope $scope): array
36+
{
37+
if ($node instanceof Node\Stmt\ClassMethod) {
38+
$functionName = $node->name->name;
39+
} elseif ($node instanceof Node\Stmt\Function_) {
40+
$functionName = trim($scope->getNamespace() . '\\' . $node->name->name, '\\');
41+
} else {
42+
return [];
43+
}
44+
45+
$docComment = $node->getDocComment();
46+
if ($docComment === null) {
47+
return [];
48+
}
49+
50+
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
51+
$scope->getFile(),
52+
$scope->isInClass() ? $scope->getClassReflection()->getName() : null,
53+
$scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
54+
$functionName,
55+
$docComment->getText(),
56+
);
57+
$nativeParameterTypes = [];
58+
foreach ($node->getParams() as $parameter) {
59+
if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
60+
throw new ShouldNotHappenException();
61+
}
62+
$nativeParameterTypes[$parameter->var->name] = $scope->getFunctionType(
63+
$parameter->type,
64+
$scope->isParameterValueNullable($parameter),
65+
false,
66+
);
67+
}
68+
69+
$errors = [];
70+
foreach ($resolvedPhpDoc->getParamsImmediatelyInvokedCallable() as $parameterName => $immediately) {
71+
$tagName = $immediately ? '@param-immediately-invoked-callable' : '@param-later-invoked-callable';
72+
if (!isset($nativeParameterTypes[$parameterName])) {
73+
$errors[] = RuleErrorBuilder::message(sprintf(
74+
'PHPDoc tag %s references unknown parameter: $%s',
75+
$tagName,
76+
$parameterName,
77+
))->identifier('parameter.notFound')->build();
78+
} elseif ($nativeParameterTypes[$parameterName]->isCallable()->no()) {
79+
$errors[] = RuleErrorBuilder::message(sprintf(
80+
'PHPDoc tag %s is for parameter $%s with non-callable type %s.',
81+
$tagName,
82+
$parameterName,
83+
$nativeParameterTypes[$parameterName]->describe(VerbosityLevel::typeOnly()),
84+
))->identifier(sprintf(
85+
'%s.nonCallable',
86+
$immediately ? 'paramImmediatelyInvokedCallable' : 'paramLaterInvokedCallable',
87+
))->build();
88+
}
89+
}
90+
91+
return $errors;
92+
}
93+
94+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PhpDoc;
4+
5+
use PHPStan\Rules\Rule as TRule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use PHPStan\Type\FileTypeMapper;
8+
9+
/**
10+
* @extends RuleTestCase<IncompatibleParamImmediatelyInvokedCallableRule>
11+
*/
12+
class IncompatibleParamImmediatelyInvokedCallableRuleTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): TRule
16+
{
17+
return new IncompatibleParamImmediatelyInvokedCallableRule(
18+
self::getContainer()->getByType(FileTypeMapper::class),
19+
);
20+
}
21+
22+
public function testRule(): void
23+
{
24+
$this->analyse([__DIR__ . '/data/incompatible-param-immediately-invoked-callable.php'], [
25+
[
26+
'PHPDoc tag @param-immediately-invoked-callable references unknown parameter: $b',
27+
21,
28+
],
29+
[
30+
'PHPDoc tag @param-later-invoked-callable references unknown parameter: $c',
31+
21,
32+
],
33+
[
34+
'PHPDoc tag @param-immediately-invoked-callable is for parameter $b with non-callable type int.',
35+
30,
36+
],
37+
[
38+
'PHPDoc tag @param-later-invoked-callable is for parameter $b with non-callable type int.',
39+
39,
40+
],
41+
[
42+
'PHPDoc tag @param-immediately-invoked-callable references unknown parameter: $b',
43+
59,
44+
],
45+
[
46+
'PHPDoc tag @param-later-invoked-callable references unknown parameter: $c',
47+
59,
48+
],
49+
[
50+
'PHPDoc tag @param-immediately-invoked-callable is for parameter $b with non-callable type int.',
51+
68,
52+
],
53+
[
54+
'PHPDoc tag @param-later-invoked-callable is for parameter $b with non-callable type int.',
55+
77,
56+
],
57+
]);
58+
}
59+
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
namespace IncompatibleParamImmediatelyInvokedCallable;
4+
5+
class Foo
6+
{
7+
8+
/**
9+
* @param-immediately-invoked-callable $a
10+
* @param-later-invoked-callable $b
11+
*/
12+
public function doFoo(callable $a, callable $b): void
13+
{
14+
15+
}
16+
17+
/**
18+
* @param-immediately-invoked-callable $b
19+
* @param-later-invoked-callable $c
20+
*/
21+
public function doBar(callable $a): void
22+
{
23+
24+
}
25+
26+
/**
27+
* @param-immediately-invoked-callable $a
28+
* @param-immediately-invoked-callable $b
29+
*/
30+
public function doBaz(string $a, int $b): void
31+
{
32+
33+
}
34+
35+
/**
36+
* @param-later-invoked-callable $a
37+
* @param-later-invoked-callable $b
38+
*/
39+
public function doBaz2(string $a, int $b): void
40+
{
41+
42+
}
43+
44+
}
45+
46+
/**
47+
* @param-immediately-invoked-callable $a
48+
* @param-later-invoked-callable $b
49+
*/
50+
function doFoo(callable $a, callable $b): void
51+
{
52+
53+
}
54+
55+
/**
56+
* @param-immediately-invoked-callable $b
57+
* @param-later-invoked-callable $c
58+
*/
59+
function doBar(callable $a): void
60+
{
61+
62+
}
63+
64+
/**
65+
* @param-immediately-invoked-callable $a
66+
* @param-immediately-invoked-callable $b
67+
*/
68+
function doBaz(string $a, int $b): void
69+
{
70+
71+
}
72+
73+
/**
74+
* @param-later-invoked-callable $a
75+
* @param-later-invoked-callable $b
76+
*/
77+
function doBaz2(string $a, int $b): void
78+
{
79+
80+
}

0 commit comments

Comments
 (0)