Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use PHPStan\Node\Printer\ExprPrinter;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\Assertions;
use PHPStan\Reflection\Callables\CallableParametersAcceptor;
use PHPStan\Reflection\ExtendedParametersAcceptor;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\ParametersAcceptorSelector;
Expand Down Expand Up @@ -570,6 +571,13 @@ public function specifyTypesInCondition(
}
}

return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope);
} elseif ($expr instanceof FuncCall && !($expr->name instanceof Name)) {
$specifiedTypes = $this->specifyTypesFromCallableCall($context, $expr, $scope);
if ($specifiedTypes !== null) {
return $specifiedTypes;
}

return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope);
} elseif ($expr instanceof MethodCall && $expr->name instanceof Node\Identifier) {
$methodCalledOnType = $scope->getType($expr->var);
Expand Down Expand Up @@ -1764,6 +1772,38 @@ static function (Type $type, callable $traverse) use ($templateTypeMap, &$contai
return $types;
}

private function specifyTypesFromCallableCall(TypeSpecifierContext $context, FuncCall $call, Scope $scope): ?SpecifiedTypes
{
if (!$call->name instanceof Expr) {
return null;
}

$calleeType = $scope->getType($call->name);

$assertions = null;
$parametersAcceptor = null;
if ($calleeType->isCallable()->yes()) {
$variants = $calleeType->getCallableParametersAcceptors($scope);
$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $call->getArgs(), $variants);
if ($parametersAcceptor instanceof CallableParametersAcceptor) {
$assertions = $parametersAcceptor->getAsserts();
}
}

if ($assertions === null || $assertions->getAll() === [] || $parametersAcceptor === null) {
return null;
}

$asserts = $assertions->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes(
$type,
$parametersAcceptor->getResolvedTemplateTypeMap(),
$parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(),
TemplateTypeVariance::createInvariant(),
));

return $this->specifyTypesFromAsserts($context, $call, $asserts, $parametersAcceptor, $scope);
}

/**
* @return array<string, ConditionalExpressionHolder[]>
*/
Expand Down
3 changes: 3 additions & 0 deletions src/Reflection/Callables/CallableParametersAcceptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Reflection\Callables;

use PHPStan\Node\InvalidateExprNode;
use PHPStan\Reflection\Assertions;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\TrinaryLogic;

Expand Down Expand Up @@ -57,4 +58,6 @@ public function getUsedVariables(): array;
*/
public function mustUseReturnValue(): TrinaryLogic;

public function getAsserts(): Assertions;

}
6 changes: 6 additions & 0 deletions src/Reflection/Callables/FunctionCallableVariant.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Reflection\Callables;

use PHPStan\Reflection\Assertions;
use PHPStan\Reflection\ExtendedMethodReflection;
use PHPStan\Reflection\ExtendedParameterReflection;
use PHPStan\Reflection\ExtendedParametersAcceptor;
Expand Down Expand Up @@ -173,4 +174,9 @@ public function mustUseReturnValue(): TrinaryLogic
return $this->function->mustUseReturnValue();
}

public function getAsserts(): Assertions
{
return $this->function->getAsserts();
}

}
6 changes: 6 additions & 0 deletions src/Reflection/ExtendedCallableFunctionVariant.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public function __construct(
private array $usedVariables,
private TrinaryLogic $acceptsNamedArguments,
private TrinaryLogic $mustUseReturnValue,
private ?Assertions $assertions = null,
)
{
parent::__construct(
Expand Down Expand Up @@ -86,4 +87,9 @@ public function mustUseReturnValue(): TrinaryLogic
return $this->mustUseReturnValue;
}

public function getAsserts(): Assertions
{
return $this->assertions ?? Assertions::createEmpty();
}

}
1 change: 1 addition & 0 deletions src/Reflection/GenericParametersAcceptorResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc
$originalParametersAcceptor->getUsedVariables(),
$originalParametersAcceptor->acceptsNamedArguments(),
$originalParametersAcceptor->mustUseReturnValue(),
$originalParametersAcceptor->getAsserts(),
);
}

Expand Down
5 changes: 5 additions & 0 deletions src/Reflection/InaccessibleMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,9 @@ public function mustUseReturnValue(): TrinaryLogic
return TrinaryLogic::createMaybe();
}

public function getAsserts(): Assertions
{
return Assertions::createEmpty();
}

}
2 changes: 2 additions & 0 deletions src/Reflection/InitializerExprTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,7 @@ public function createFirstClassCallable(
}

$parameters = $variant->getParameters();
$assertions = $function !== null ? $function->getAsserts() : Assertions::createEmpty();
$closureTypes[] = new ClosureType(
$parameters,
$returnType,
Expand All @@ -970,6 +971,7 @@ public function createFirstClassCallable(
$impurePoints,
acceptsNamedArguments: $acceptsNamedArguments,
mustUseReturnValue: $mustUseReturnValue,
assertions: $assertions,
);
}

Expand Down
1 change: 1 addition & 0 deletions src/Reflection/ParametersAcceptorSelector.php
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,7 @@ private static function wrapAcceptor(ParametersAcceptor $acceptor): ExtendedPara
$acceptor->getUsedVariables(),
$acceptor->acceptsNamedArguments(),
$acceptor->mustUseReturnValue(),
$acceptor->getAsserts(),
);
}

Expand Down
6 changes: 6 additions & 0 deletions src/Reflection/ResolvedFunctionVariantWithCallable.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public function __construct(
private array $usedVariables,
private TrinaryLogic $acceptsNamedArguments,
private TrinaryLogic $mustUseReturnValue,
private ?Assertions $assertions = null,
)
{
}
Expand Down Expand Up @@ -118,4 +119,9 @@ public function mustUseReturnValue(): TrinaryLogic
return $this->mustUseReturnValue;
}

public function getAsserts(): Assertions
{
return $this->assertions ?? Assertions::createEmpty();
}

}
5 changes: 5 additions & 0 deletions src/Reflection/TrivialParametersAcceptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,9 @@ public function mustUseReturnValue(): TrinaryLogic
return TrinaryLogic::createMaybe();
}

public function getAsserts(): Assertions
{
return Assertions::createEmpty();
}

}
6 changes: 6 additions & 0 deletions src/Type/CallableType.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\PhpDocParser\Printer\Printer;
use PHPStan\Reflection\Assertions;
use PHPStan\Reflection\Callables\CallableParametersAcceptor;
use PHPStan\Reflection\Callables\SimpleImpurePoint;
use PHPStan\Reflection\Callables\SimpleThrowPoint;
Expand Down Expand Up @@ -398,6 +399,11 @@ public function mustUseReturnValue(): TrinaryLogic
return TrinaryLogic::createMaybe();
}

public function getAsserts(): Assertions
{
return Assertions::createEmpty();
}

public function toNumber(): Type
{
return new ErrorType();
Expand Down
12 changes: 12 additions & 0 deletions src/Type/ClosureType.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\PhpDocParser\Printer\Printer;
use PHPStan\Reflection\Assertions;
use PHPStan\Reflection\Callables\CallableParametersAcceptor;
use PHPStan\Reflection\Callables\SimpleImpurePoint;
use PHPStan\Reflection\Callables\SimpleThrowPoint;
Expand Down Expand Up @@ -84,6 +85,8 @@ class ClosureType implements TypeWithClassName, CallableParametersAcceptor

private TrinaryLogic $mustUseReturnValue;

private Assertions $assertions;

/**
* @api
* @param list<ParameterReflection>|null $parameters
Expand All @@ -107,6 +110,7 @@ public function __construct(
private array $usedVariables = [],
?TrinaryLogic $acceptsNamedArguments = null,
?TrinaryLogic $mustUseReturnValue = null,
?Assertions $assertions = null,
)
{
if ($acceptsNamedArguments === null) {
Expand All @@ -126,6 +130,12 @@ public function __construct(
$this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap ?? TemplateTypeMap::createEmpty();
$this->callSiteVarianceMap = $callSiteVarianceMap ?? TemplateTypeVarianceMap::createEmpty();
$this->impurePoints = $impurePoints ?? [new SimpleImpurePoint('functionCall', 'call to an unknown Closure', false)];
$this->assertions = $assertions ?? Assertions::createEmpty();
}

public function getAsserts(): Assertions
{
return $this->assertions;
}

/**
Expand Down Expand Up @@ -664,6 +674,7 @@ public function traverse(callable $cb): Type
$this->usedVariables,
$this->acceptsNamedArguments,
$this->mustUseReturnValue,
$this->assertions,
);
}

Expand Down Expand Up @@ -715,6 +726,7 @@ public function traverseSimultaneously(Type $right, callable $cb): Type
$this->usedVariables,
$this->acceptsNamedArguments,
$this->mustUseReturnValue,
$this->assertions,
);
}

Expand Down
83 changes: 83 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14249.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php // lint >= 8.1

declare(strict_types = 1);

namespace Bug14249;

use function PHPStan\Testing\assertType;

/**
* @phpstan-assert-if-true positive-int $value
*/
function is_positive_int(mixed $value): bool {
return is_int($value) && $value > 0;
}

function f(mixed $v): void {
$f1 = is_positive_int(...);
$f2 = 'Bug14249\is_positive_int';

if (is_positive_int($v)) {
assertType('int<1, max>', $v);
}

if ($f1($v)) {
assertType('int<1, max>', $v);
}

if ($f2($v)) {
assertType('int<1, max>', $v);
}
}


/**
* @template T of bool
* @param T $if
* @phpstan-assert (T is true ? true : false) $condition
*/
function assertIfTemplated(mixed $condition, bool $if)
{
}

function doTemplated(): void {
$f1 = assertIfTemplated(...);
$f2 = 'Bug14249\assertIfTemplated';

$v = getMixed();
assertIfTemplated($v, true);
assertType('true', $v);

$v = getMixed();
$f1($v, true);
assertType('true', $v);

$v = getMixed();
$f2($v, true);
assertType('true', $v);

$v = getMixed();
assertIfTemplated($v, false);
assertType('false', $v);

$v = getMixed();
$f1($v, false);
assertType('false', $v);

$v = getMixed();
$f2($v, false);
assertType('false', $v);
}

function getMixed(): mixed {}

function maybeCallable() {
$f2 = 'Bug14249\assertIfTemplated';
if (rand(0,1)) {
$f2 = 'notCallable';
}

$v = getMixed();
$f2($v, false);
assertType('mixed', $v);
}
Loading