Skip to content

Commit 8d69bd0

Browse files
authored
Fix phpstan/phpstan#14249: Propagate @phpstan-assert-* annotations to First-class callables (Closures) and string callables (#5201)
1 parent 024a65c commit 8d69bd0

13 files changed

+176
-0
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use PHPStan\Node\Printer\ExprPrinter;
2727
use PHPStan\Php\PhpVersion;
2828
use PHPStan\Reflection\Assertions;
29+
use PHPStan\Reflection\Callables\CallableParametersAcceptor;
2930
use PHPStan\Reflection\ExtendedParametersAcceptor;
3031
use PHPStan\Reflection\ParametersAcceptor;
3132
use PHPStan\Reflection\ParametersAcceptorSelector;
@@ -570,6 +571,13 @@ public function specifyTypesInCondition(
570571
}
571572
}
572573

574+
return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope);
575+
} elseif ($expr instanceof FuncCall && !($expr->name instanceof Name)) {
576+
$specifiedTypes = $this->specifyTypesFromCallableCall($context, $expr, $scope);
577+
if ($specifiedTypes !== null) {
578+
return $specifiedTypes;
579+
}
580+
573581
return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope);
574582
} elseif ($expr instanceof MethodCall && $expr->name instanceof Node\Identifier) {
575583
$methodCalledOnType = $scope->getType($expr->var);
@@ -1764,6 +1772,38 @@ static function (Type $type, callable $traverse) use ($templateTypeMap, &$contai
17641772
return $types;
17651773
}
17661774

1775+
private function specifyTypesFromCallableCall(TypeSpecifierContext $context, FuncCall $call, Scope $scope): ?SpecifiedTypes
1776+
{
1777+
if (!$call->name instanceof Expr) {
1778+
return null;
1779+
}
1780+
1781+
$calleeType = $scope->getType($call->name);
1782+
1783+
$assertions = null;
1784+
$parametersAcceptor = null;
1785+
if ($calleeType->isCallable()->yes()) {
1786+
$variants = $calleeType->getCallableParametersAcceptors($scope);
1787+
$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $call->getArgs(), $variants);
1788+
if ($parametersAcceptor instanceof CallableParametersAcceptor) {
1789+
$assertions = $parametersAcceptor->getAsserts();
1790+
}
1791+
}
1792+
1793+
if ($assertions === null || $assertions->getAll() === [] || $parametersAcceptor === null) {
1794+
return null;
1795+
}
1796+
1797+
$asserts = $assertions->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes(
1798+
$type,
1799+
$parametersAcceptor->getResolvedTemplateTypeMap(),
1800+
$parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(),
1801+
TemplateTypeVariance::createInvariant(),
1802+
));
1803+
1804+
return $this->specifyTypesFromAsserts($context, $call, $asserts, $parametersAcceptor, $scope);
1805+
}
1806+
17671807
/**
17681808
* @return array<string, ConditionalExpressionHolder[]>
17691809
*/

src/Reflection/Callables/CallableParametersAcceptor.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Reflection\Callables;
44

55
use PHPStan\Node\InvalidateExprNode;
6+
use PHPStan\Reflection\Assertions;
67
use PHPStan\Reflection\ParametersAcceptor;
78
use PHPStan\TrinaryLogic;
89

@@ -57,4 +58,6 @@ public function getUsedVariables(): array;
5758
*/
5859
public function mustUseReturnValue(): TrinaryLogic;
5960

61+
public function getAsserts(): Assertions;
62+
6063
}

src/Reflection/Callables/FunctionCallableVariant.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\Reflection\Callables;
44

5+
use PHPStan\Reflection\Assertions;
56
use PHPStan\Reflection\ExtendedMethodReflection;
67
use PHPStan\Reflection\ExtendedParameterReflection;
78
use PHPStan\Reflection\ExtendedParametersAcceptor;
@@ -173,4 +174,9 @@ public function mustUseReturnValue(): TrinaryLogic
173174
return $this->function->mustUseReturnValue();
174175
}
175176

177+
public function getAsserts(): Assertions
178+
{
179+
return $this->function->getAsserts();
180+
}
181+
176182
}

src/Reflection/ExtendedCallableFunctionVariant.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public function __construct(
3737
private array $usedVariables,
3838
private TrinaryLogic $acceptsNamedArguments,
3939
private TrinaryLogic $mustUseReturnValue,
40+
private ?Assertions $assertions = null,
4041
)
4142
{
4243
parent::__construct(
@@ -86,4 +87,9 @@ public function mustUseReturnValue(): TrinaryLogic
8687
return $this->mustUseReturnValue;
8788
}
8889

90+
public function getAsserts(): Assertions
91+
{
92+
return $this->assertions ?? Assertions::createEmpty();
93+
}
94+
8995
}

src/Reflection/GenericParametersAcceptorResolver.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc
130130
$originalParametersAcceptor->getUsedVariables(),
131131
$originalParametersAcceptor->acceptsNamedArguments(),
132132
$originalParametersAcceptor->mustUseReturnValue(),
133+
$originalParametersAcceptor->getAsserts(),
133134
);
134135
}
135136

src/Reflection/InaccessibleMethod.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,9 @@ public function mustUseReturnValue(): TrinaryLogic
9393
return TrinaryLogic::createMaybe();
9494
}
9595

96+
public function getAsserts(): Assertions
97+
{
98+
return Assertions::createEmpty();
99+
}
100+
96101
}

src/Reflection/InitializerExprTypeResolver.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,7 @@ public function createFirstClassCallable(
958958
}
959959

960960
$parameters = $variant->getParameters();
961+
$assertions = $function !== null ? $function->getAsserts() : Assertions::createEmpty();
961962
$closureTypes[] = new ClosureType(
962963
$parameters,
963964
$returnType,
@@ -970,6 +971,7 @@ public function createFirstClassCallable(
970971
$impurePoints,
971972
acceptsNamedArguments: $acceptsNamedArguments,
972973
mustUseReturnValue: $mustUseReturnValue,
974+
assertions: $assertions,
973975
);
974976
}
975977

src/Reflection/ParametersAcceptorSelector.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,7 @@ private static function wrapAcceptor(ParametersAcceptor $acceptor): ExtendedPara
897897
$acceptor->getUsedVariables(),
898898
$acceptor->acceptsNamedArguments(),
899899
$acceptor->mustUseReturnValue(),
900+
$acceptor->getAsserts(),
900901
);
901902
}
902903

src/Reflection/ResolvedFunctionVariantWithCallable.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public function __construct(
2929
private array $usedVariables,
3030
private TrinaryLogic $acceptsNamedArguments,
3131
private TrinaryLogic $mustUseReturnValue,
32+
private ?Assertions $assertions = null,
3233
)
3334
{
3435
}
@@ -118,4 +119,9 @@ public function mustUseReturnValue(): TrinaryLogic
118119
return $this->mustUseReturnValue;
119120
}
120121

122+
public function getAsserts(): Assertions
123+
{
124+
return $this->assertions ?? Assertions::createEmpty();
125+
}
126+
121127
}

src/Reflection/TrivialParametersAcceptor.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,9 @@ public function mustUseReturnValue(): TrinaryLogic
103103
return TrinaryLogic::createMaybe();
104104
}
105105

106+
public function getAsserts(): Assertions
107+
{
108+
return Assertions::createEmpty();
109+
}
110+
106111
}

0 commit comments

Comments
 (0)