Skip to content

Commit

Permalink
Check invalid @param-closure-this
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Aug 23, 2024
1 parent c79d03c commit 95c0a58
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 24 deletions.
8 changes: 7 additions & 1 deletion src/PhpDoc/PhpDocNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
use PHPStan\Type\Generic\TemplateTypeScope;
use PHPStan\Type\Generic\TemplateTypeVariance;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function array_key_exists;
Expand Down Expand Up @@ -421,7 +422,12 @@ public function resolveParamClosureThisTags(PhpDocNode $phpDocNode, NameScope $n
foreach (['@param-closure-this', '@phpstan-param-closure-this'] as $tagName) {
foreach ($phpDocNode->getParamClosureThisTagValues($tagName) as $tagValue) {
$parameterName = substr($tagValue->parameterName, 1);
$closureThisTypes[$parameterName] = new ParamClosureThisTag($this->typeNodeResolver->resolve($tagValue->type, $nameScope));
$closureThisTypes[$parameterName] = new ParamClosureThisTag(
TypeCombinator::intersect(
$this->typeNodeResolver->resolve($tagValue->type, $nameScope),
new ObjectWithoutClassType(),
),
);
}
}

Expand Down
61 changes: 38 additions & 23 deletions src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\ClosureType;
use PHPStan\Type\FileTypeMapper;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Type;
use PHPStan\Type\VerbosityLevel;
use function array_merge;
use function in_array;
use function is_string;
use function sprintf;
use function trim;
Expand Down Expand Up @@ -68,10 +70,9 @@ public function processNode(Node $node, Scope $scope): array

$errors = [];

foreach ([$resolvedPhpDoc->getParamTags(), $resolvedPhpDoc->getParamOutTags()] as $parameters) {
foreach (['@param' => $resolvedPhpDoc->getParamTags(), '@param-out' => $resolvedPhpDoc->getParamOutTags(), '@param-closure-this' => $resolvedPhpDoc->getParamClosureThisTags()] as $tagName => $parameters) {
foreach ($parameters as $parameterName => $phpDocParamTag) {
$phpDocParamType = $phpDocParamTag->getType();
$tagName = $phpDocParamTag instanceof ParamTag ? '@param' : '@param-out';

if (!isset($nativeParameterTypes[$parameterName])) {
$errors[] = RuleErrorBuilder::message(sprintf(
Expand Down Expand Up @@ -99,7 +100,6 @@ public function processNode(Node $node, Scope $scope): array
) {
$phpDocParamType = $phpDocParamType->getIterableValueType();
}
$isParamSuperType = $nativeParamType->isSuperTypeOf($phpDocParamType);

$escapedParameterName = SprintfHelper::escapeFormatString($parameterName);
$escapedTagName = SprintfHelper::escapeFormatString($tagName);
Expand Down Expand Up @@ -160,28 +160,43 @@ public function processNode(Node $node, Scope $scope): array
continue;
}

if ($isParamSuperType->no()) {
$errors[] = RuleErrorBuilder::message(sprintf(
'PHPDoc tag %s for parameter $%s with type %s is incompatible with native type %s.',
$tagName,
$parameterName,
$phpDocParamType->describe(VerbosityLevel::typeOnly()),
$nativeParamType->describe(VerbosityLevel::typeOnly()),
))->identifier('parameter.phpDocType')->build();

} elseif ($isParamSuperType->maybe()) {
$errorBuilder = RuleErrorBuilder::message(sprintf(
'PHPDoc tag %s for parameter $%s with type %s is not subtype of native type %s.',
$tagName,
$parameterName,
$phpDocParamType->describe(VerbosityLevel::typeOnly()),
$nativeParamType->describe(VerbosityLevel::typeOnly()),
))->identifier('parameter.phpDocType');
if ($phpDocParamType instanceof TemplateType) {
$errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocParamType->getName(), $nativeParamType->describe(VerbosityLevel::typeOnly())));
if (in_array($tagName, ['@param', '@param-out'], true)) {
$isParamSuperType = $nativeParamType->isSuperTypeOf($phpDocParamType);
if ($isParamSuperType->no()) {
$errors[] = RuleErrorBuilder::message(sprintf(
'PHPDoc tag %s for parameter $%s with type %s is incompatible with native type %s.',
$tagName,
$parameterName,
$phpDocParamType->describe(VerbosityLevel::typeOnly()),
$nativeParamType->describe(VerbosityLevel::typeOnly()),
))->identifier('parameter.phpDocType')->build();

} elseif ($isParamSuperType->maybe()) {
$errorBuilder = RuleErrorBuilder::message(sprintf(
'PHPDoc tag %s for parameter $%s with type %s is not subtype of native type %s.',
$tagName,
$parameterName,
$phpDocParamType->describe(VerbosityLevel::typeOnly()),
$nativeParamType->describe(VerbosityLevel::typeOnly()),
))->identifier('parameter.phpDocType');
if ($phpDocParamType instanceof TemplateType) {
$errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocParamType->getName(), $nativeParamType->describe(VerbosityLevel::typeOnly())));
}

$errors[] = $errorBuilder->build();
}
}

$errors[] = $errorBuilder->build();
if ($tagName === '@param-closure-this') {
$isNonClosure = (new ClosureType())->isSuperTypeOf($nativeParamType)->no();
if ($isNonClosure) {
$errors[] = RuleErrorBuilder::message(sprintf(
'PHPDoc tag %s is for parameter $%s with non-Closure type %s.',
$tagName,
$parameterName,
$nativeParamType->describe(VerbosityLevel::typeOnly()),
))->identifier('paramClosureThis.nonClosure')->build();
}
}
}
}
Expand Down
38 changes: 38 additions & 0 deletions tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -442,4 +442,42 @@ public function testBug10622B(): void
$this->analyse([__DIR__ . '/data/bug-10622b.php'], []);
}

public function testParamClosureThis(): void
{
$this->analyse([__DIR__ . '/data/param-closure-this.php'], [
[
'PHPDoc tag @param-closure-this references unknown parameter: $b',
20,
],
[
'PHPDoc tag @param-closure-this for parameter $i contains unresolvable type.',
27,
],
[
'PHPDoc tag @param-closure-this for parameter $i contains unresolvable type.',
34,
],
[
'PHPDoc tag @param-closure-this is for parameter $i with non-Closure type string.',
41,
],
[
'PHPDoc tag @param-closure-this for parameter $i contains generic type Exception<int, float> but class Exception is not generic.',
48,
],
[
'Generic type ParamClosureThisPhpDocRule\FooBar<mixed> in PHPDoc tag @param-closure-this for parameter $i does not specify all template types of class ParamClosureThisPhpDocRule\FooBar: T, TT',
55,
],
[
'Type mixed in generic type ParamClosureThisPhpDocRule\FooBar<mixed> in PHPDoc tag @param-closure-this for parameter $i is not subtype of template type T of int of class ParamClosureThisPhpDocRule\FooBar.',
55,
],
[
'Generic type ParamClosureThisPhpDocRule\FooBar<int> in PHPDoc tag @param-closure-this for parameter $i does not specify all template types of class ParamClosureThisPhpDocRule\FooBar: T, TT',
62,
],
]);
}

}
72 changes: 72 additions & 0 deletions tests/PHPStan/Rules/PhpDoc/data/param-closure-this.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

namespace ParamClosureThisPhpDocRule;

class Foo
{

}

/**
* @param-closure-this Foo $i
*/
function validParamClosureThis(callable $i) {

}

/**
* @param-closure-this Foo $b
*/
function invalidParamClosureThisParamName($a) {

}

/**
* @param-closure-this string $i
*/
function nonObjectParamClosureThis(callable $i) {

}

/**
* @param-closure-this \stdClass&\Exception $i
*/
function unresolvableParamClosureThis(callable $i) {

}

/**
* @param-closure-this Foo $i
*/
function paramClosureThisAboveNonClosure(string $i) {

}

/**
* @param-closure-this \Exception<int, float> $i
*/
function invalidParamClosureThisGeneric(callable $i) {

}

/**
* @param-closure-this FooBar<mixed> $i
*/
function invalidParamClosureThisWrongGenericParams(callable $i) {

}

/**
* @param-closure-this FooBar<int> $i
*/
function invalidParamClosureThisNotAllGenericParams(callable $i) {

}

/**
* @template T of int
* @template TT of string
*/
class FooBar {

}

0 comments on commit 95c0a58

Please sign in to comment.