Skip to content
Closed
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
18 changes: 16 additions & 2 deletions src/Analyser/ExprHandler/FuncCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -406,9 +406,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
// inside resolveReturnType), the acceptor is re-derived from the
// already-processed argument results on the asking scope.
$storageRef = WeakReference::create($storage);
$hasConditionalAsserts = $functionReflection !== null && $functionReflection->getAsserts()->hasConditionalAsserts();
$typeCallback = $isEarlyTerminating
? static fn (bool $nativeTypesPromoted): Type => new NeverType(true)
: function (bool $nativeTypesPromoted) use ($nodeScopeResolver, $beforeScope, $expr, $nameResult, $resolvedParametersAcceptor, $argsResult, $storageRef): Type {
: function (bool $nativeTypesPromoted) use ($nodeScopeResolver, $beforeScope, $expr, $nameResult, $resolvedParametersAcceptor, $argsResult, $storageRef, $hasConditionalAsserts): Type {
// for always-true/always-false type checks the call's own narrowing
// (already produced as this result's specifyTypesCallback) decides
// the return type - the verdict is a read of that narrowing, not a
Expand All @@ -430,7 +431,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
}
}

return $this->resolveReturnType(
$returnType = $this->resolveReturnType(
$nodeScopeResolver,
$beforeScope,
$nativeTypesPromoted,
Expand All @@ -439,6 +440,19 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$nativeTypesPromoted ? null : $resolvedParametersAcceptor,
$argsResult,
);
if ($nativeTypesPromoted || !$hasConditionalAsserts) {
return $returnType;
}

$callStorage = $storageRef->get();

return $this->impossibleCheckTypeHelper->resolveConditionalAssertReturnType(
$beforeScope,
$expr,
$callStorage === null ? null : $callStorage->findExpressionResult($expr),
$argsResult,
$returnType,
);
};
$specifyTypesCallback = fn (TypeSpecifierContext $specifyContext, bool $nativeTypesPromoted): SpecifiedTypes => $this->specifyTypes(
$nativeTypesPromoted ? $beforeScope->doNotTreatPhpDocTypesAsCertain() : $beforeScope,
Expand Down
41 changes: 32 additions & 9 deletions src/Analyser/ExprHandler/MethodCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper;
use PHPStan\ShouldNotHappenException;
use PHPStan\Turbo\ShadowedByTurboExtension;
use PHPStan\Type\ErrorType;
Expand All @@ -55,6 +56,7 @@
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;
use WeakReference;
use function array_map;
use function array_merge;
use function count;
Expand All @@ -81,6 +83,7 @@ public function __construct(
private DynamicReturnTypeStoragePrimer $storagePrimer,
private EarlyTerminatingCallHelper $earlyTerminatingHelper,
private ArgumentsHandler $argumentsHandler,
private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper,
)
{
}
Expand Down Expand Up @@ -216,17 +219,37 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
// evolving scope (type-driven, generics resolved). When null
// (native-types-promoted, or on-demand / synthetic pricing) the acceptor is
// re-derived from the already-processed argument results on the asking scope.
$storageRef = WeakReference::create($storage);
$hasConditionalAsserts = $methodReflection !== null && $methodReflection->getAsserts()->hasConditionalAsserts();
$typeCallback = $isEarlyTerminating
? static fn (bool $nativeTypesPromoted): Type => new NeverType(true)
: fn (bool $nativeTypesPromoted): Type => $this->resolveReturnType(
$beforeScope,
$nativeTypesPromoted,
$expr,
$varResult,
$nameResult,
$nativeTypesPromoted ? null : $resolvedParametersAcceptor,
$argsResult,
);
: function (bool $nativeTypesPromoted) use ($beforeScope, $expr, $varResult, $nameResult, $resolvedParametersAcceptor, $argsResult, $storageRef, $hasConditionalAsserts): Type {
$returnType = $this->resolveReturnType(
$beforeScope,
$nativeTypesPromoted,
$expr,
$varResult,
$nameResult,
$nativeTypesPromoted ? null : $resolvedParametersAcceptor,
$argsResult,
);
if ($nativeTypesPromoted || !$hasConditionalAsserts) {
return $returnType;
}

// the call's own narrowing decides a conditional @phpstan-assert;
// its result is looked up through a weak storage reference, as
// this callback is owned by that very result
$callStorage = $storageRef->get();

return $this->impossibleCheckTypeHelper->resolveConditionalAssertReturnType(
$beforeScope,
$expr,
$callStorage === null ? null : $callStorage->findExpressionResult($expr),
$argsResult,
$returnType,
);
};
$walkMethodReflection = $methodReflection;
$specifyTypesCallback = fn (TypeSpecifierContext $specifyContext, bool $nativeTypesPromoted): SpecifiedTypes => $this->specifyTypes(
$nativeTypesPromoted ? $beforeScope->doNotTreatPhpDocTypesAsCertain() : $beforeScope,
Expand Down
41 changes: 32 additions & 9 deletions src/Analyser/ExprHandler/StaticCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper;
use PHPStan\ShouldNotHappenException;
use PHPStan\Turbo\ShadowedByTurboExtension;
use PHPStan\Type\ErrorType;
Expand All @@ -56,6 +57,7 @@
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeWithClassName;
use ReflectionProperty;
use WeakReference;
use function array_map;
use function array_merge;
use function count;
Expand All @@ -82,6 +84,7 @@ public function __construct(
private DynamicReturnTypeStoragePrimer $storagePrimer,
private EarlyTerminatingCallHelper $earlyTerminatingHelper,
private ArgumentsHandler $argumentsHandler,
private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper,
)
{
}
Expand Down Expand Up @@ -299,17 +302,37 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
// evolving scope (type-driven, generics resolved). When null
// (native-types-promoted, or on-demand / synthetic pricing) the acceptor is
// re-derived from the already-processed argument results on the asking scope.
$storageRef = WeakReference::create($storage);
$hasConditionalAsserts = $methodReflection !== null && $methodReflection->getAsserts()->hasConditionalAsserts();
$typeCallback = $isEarlyTerminating
? static fn (bool $nativeTypesPromoted): Type => new NeverType(true)
: fn (bool $nativeTypesPromoted): Type => $this->resolveReturnType(
$beforeScope,
$nativeTypesPromoted,
$expr,
$classResult,
$nameResult,
$nativeTypesPromoted ? null : $resolvedParametersAcceptor,
$argsResult,
);
: function (bool $nativeTypesPromoted) use ($beforeScope, $expr, $classResult, $nameResult, $resolvedParametersAcceptor, $argsResult, $storageRef, $hasConditionalAsserts): Type {
$returnType = $this->resolveReturnType(
$beforeScope,
$nativeTypesPromoted,
$expr,
$classResult,
$nameResult,
$nativeTypesPromoted ? null : $resolvedParametersAcceptor,
$argsResult,
);
if ($nativeTypesPromoted || !$hasConditionalAsserts) {
return $returnType;
}

// the call's own narrowing decides a conditional @phpstan-assert;
// its result is looked up through a weak storage reference, as
// this callback is owned by that very result
$callStorage = $storageRef->get();

return $this->impossibleCheckTypeHelper->resolveConditionalAssertReturnType(
$beforeScope,
$expr,
$callStorage === null ? null : $callStorage->findExpressionResult($expr),
$argsResult,
$returnType,
);
};
$walkMethodReflection = $methodReflection;
$specifyTypesCallback = fn (TypeSpecifierContext $specifyContext, bool $nativeTypesPromoted): SpecifiedTypes => $this->specifyTypes(
$nativeTypesPromoted ? $beforeScope->doNotTreatPhpDocTypesAsCertain() : $beforeScope,
Expand Down
12 changes: 12 additions & 0 deletions src/Reflection/Assertions.php
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,16 @@ public static function createFromAssertTags(array $assertTags): self
return self::create($assertTags);
}

/** Whether any tag is @phpstan-assert-if-true or @phpstan-assert-if-false. */
public function hasConditionalAsserts(): bool
{
foreach ($this->asserts as $assert) {
if ($assert->getIf() !== AssertTag::NULL) {
return true;
}
}

return false;
}

}
25 changes: 25 additions & 0 deletions src/Rules/Comparison/ImpossibleCheckTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,31 @@ public function findSpecifiedType(
return $specifiedValue;
}

/**
* Pins a boolean call with @phpstan-assert-if-true/-if-false to true/false when
* the argument types already decide the assertion - the same verdict the
* ImpossibleCheckType* rules report, read from the call's own narrowing.
*/
public function resolveConditionalAssertReturnType(
Scope $scope,
Expr $node,
?ExpressionResult $nodeResult,
?ArgsResult $argsResult,
Type $returnType,
): Type
{
if ($nodeResult === null || !$returnType->isBoolean()->yes() || $returnType->isConstantScalarValue()->yes()) {
return $returnType;
}

$isAlways = $this->findSpecifiedType($scope, $node, $nodeResult, $argsResult);
if ($isAlways === null) {
return $returnType;
}

return new ConstantBooleanType($isAlways);
}

/**
* Reads an argument's type from its already-computed ExpressionResult when the
* caller passed the call's ArgsResult (the engine-side verdict in
Expand Down
78 changes: 78 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-11374.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php // lint >= 8.0

declare(strict_types = 1);

namespace Bug11374Types;

use function PHPStan\Testing\assertType;

final class Checker
{
/** @phpstan-assert-if-true float $value */
public function isFloat(mixed $value): bool
{
return is_float($value);
}

/** @phpstan-assert-if-false float $value */
public function isNotFloat(mixed $value): bool
{
return !is_float($value);
}

/** @phpstan-assert-if-true !float $value */
public function isNotFloatNegated(mixed $value): bool
{
return !is_float($value);
}

/** @phpstan-assert-if-true float $value */
public static function isFloatStatic(mixed $value): bool
{
return is_float($value);
}

/** @phpstan-assert float $value */
public function assertFloat(mixed $value): bool
{
return true;
}

/** @phpstan-assert-if-true =float $value */
public function isFloatEquality(mixed $value): bool
{
return is_float($value);
}
}

/** @phpstan-assert-if-true float $value */
function isFloat(mixed $value): bool
{
return is_float($value);
}

function (Checker $c, float $f, int $i, float|int $fi, mixed $m): void {
assertType('true', $c->isFloat($f));
assertType('false', $c->isFloat($i));
assertType('bool', $c->isFloat($fi));
assertType('bool', $c->isFloat($m));

assertType('false', $c->isNotFloat($f));
assertType('true', $c->isNotFloat($i));
assertType('bool', $c->isNotFloat($fi));

assertType('false', $c->isNotFloatNegated($f));
assertType('true', $c->isNotFloatNegated($i));
assertType('bool', $c->isNotFloatNegated($fi));

assertType('true', Checker::isFloatStatic($f));
assertType('false', Checker::isFloatStatic($i));
assertType('bool', Checker::isFloatStatic($fi));

assertType('true', isFloat($f));
assertType('false', isFloat($i));
assertType('bool', isFloat($fi));

assertType('bool', $c->assertFloat($i));
assertType('bool', $c->isFloatEquality($f));
};
46 changes: 46 additions & 0 deletions tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,52 @@ public function testNullInMultiConditionArm(): void
$this->analyse([__DIR__ . '/data/match-null-in-multi-condition-arm.php'], []);
}

#[RequiresPhp('>= 8.0.0')]
public function testBug11374(): void
{
$this->analyse([__DIR__ . '/data/bug-11374.php'], [
[
'Call to function is_float() with float will always evaluate to true.',
13,
],
[
'Call to method Bug11374\\HelloWorld::isFloat() with float will always evaluate to true.',
21,
],
[
'Call to method Bug11374\\HelloWorld::isFloat() with float will always evaluate to true.',
29,
],
[
'Call to function is_float() with float will always evaluate to true.',
37,
],
]);
}

#[RequiresPhp('>= 8.0.0')]
public function testBug11374Analogues(): void
{
$this->analyse([__DIR__ . '/data/bug-11374-2.php'], [
[
'Call to function isFloat() with float will always evaluate to true.',
13,
],
[
'Call to static method Bug11374Two\\HelloWorld::isFloatStatic() with float will always evaluate to true.',
21,
],
[
'Call to method Bug11374Two\\HelloWorld::isNotFloat() with float will always evaluate to false.',
29,
],
[
'Match expression does not handle remaining value: true',
35,
],
]);
}

#[RequiresPhp('>= 8.0.0')]
public function testBug11453(): void
{
Expand Down
Loading
Loading