Skip to content
Open
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
10 changes: 10 additions & 0 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -3228,6 +3228,11 @@ public function filterByTruthyValue(Expr $expr): self
}

$specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($this, $expr, TypeSpecifierContext::createTruthy());
if ($specifiedTypes->isEquality() && $this->getType($expr)->isBoolean()->yes()) {
$specifiedTypes = $specifiedTypes->unionWith(
$this->typeSpecifier->create($expr, new ConstantBooleanType(true), TypeSpecifierContext::createTrue(), $this),
);
}
$scope = $this->filterBySpecifiedTypes($specifiedTypes);
$this->truthyScopes[$exprString] = $scope;

Expand All @@ -3245,6 +3250,11 @@ public function filterByFalseyValue(Expr $expr): self
}

$specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($this, $expr, TypeSpecifierContext::createFalsey());
if ($specifiedTypes->isEquality() && $this->getType($expr)->isBoolean()->yes()) {
$specifiedTypes = $specifiedTypes->unionWith(
$this->typeSpecifier->create($expr, new ConstantBooleanType(false), TypeSpecifierContext::createTrue(), $this),
);
}
$scope = $this->filterBySpecifiedTypes($specifiedTypes);
$this->falseyScopes[$exprString] = $scope;

Expand Down
13 changes: 11 additions & 2 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Type\ClosureType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\FileTypeMapper;
Expand Down Expand Up @@ -1148,11 +1149,19 @@ public function processStmtNode(
$this->callNodeCallback($nodeCallback, new NoopExpressionNode($stmt->expr, $hasAssign), $scope, $storage);
}
$scope = $result->getScope();
$scope = $scope->filterBySpecifiedTypes($this->typeSpecifier->specifyTypesInCondition(
$specifiedTypes = $this->typeSpecifier->specifyTypesInCondition(
$scope,
$stmt->expr,
TypeSpecifierContext::createNull(),
));
);
$scope = $scope->filterBySpecifiedTypes($specifiedTypes);
if ($specifiedTypes->isEquality()) {
// Statement counterpart of the equality handling in filterByTruthyValue():
// store the call's true result so a duplicate void assertion statement is
// reported as always-true. We assign directly because void calls have no
// return value to protect, and intersecting true with void would produce never.
$scope = $scope->assignExpression($stmt->expr, new ConstantBooleanType(true), new ConstantBooleanType(true));
}
$hasYield = $result->hasYield();
$throwPoints = $result->getThrowPoints();
$impurePoints = $result->getImpurePoints();
Expand Down
40 changes: 40 additions & 0 deletions src/Analyser/SpecifiedTypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ final class SpecifiedTypes

private bool $overwrite = false;

private bool $equality = false;

/** @var array<string, ConditionalExpressionHolder[]> */
private array $newConditionalExpressionHolders = [];

Expand Down Expand Up @@ -51,19 +53,48 @@ public function setAlwaysOverwriteTypes(): self
{
$self = new self($this->sureTypes, $this->sureNotTypes);
$self->overwrite = true;
$self->equality = $this->equality;
$self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders;
$self->rootExpr = $this->rootExpr;

return $self;
}

/**
* Marks these types as coming from an equality check, the same concept as
* the "=Type" equality assertions documented at
* https://phpstan.org/writing-php-code/narrowing-types#equality-assertions
*
* The narrowed types are only applied; they do not determine the check
* outcome, so ImpossibleCheckTypeHelper will not use them to report
* always-true/false for the check expression.
*
* @api
*/
public function setEquality(): self
{
$self = new self($this->sureTypes, $this->sureNotTypes);
$self->overwrite = $this->overwrite;
$self->equality = true;
$self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders;
$self->rootExpr = $this->rootExpr;

return $self;
}

public function isEquality(): bool
{
return $this->equality;
}

/**
* @api
*/
public function setRootExpr(?Expr $rootExpr): self
{
$self = new self($this->sureTypes, $this->sureNotTypes);
$self->overwrite = $this->overwrite;
$self->equality = $this->equality;
$self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders;
$self->rootExpr = $rootExpr;

Expand All @@ -77,6 +108,7 @@ public function setNewConditionalExpressionHolders(array $newConditionalExpressi
{
$self = new self($this->sureTypes, $this->sureNotTypes);
$self->overwrite = $this->overwrite;
$self->equality = $this->equality;
$self->newConditionalExpressionHolders = $newConditionalExpressionHolders;
$self->rootExpr = $this->rootExpr;

Expand Down Expand Up @@ -128,6 +160,7 @@ public function removeExpr(string $exprString): self

$self = new self($sureTypes, $sureNotTypes);
$self->overwrite = $this->overwrite;
$self->equality = $this->equality;
$self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders;
$self->rootExpr = $this->rootExpr;

Expand Down Expand Up @@ -167,6 +200,9 @@ public function intersectWith(SpecifiedTypes $other): self
if ($this->overwrite && $other->overwrite) {
$result = $result->setAlwaysOverwriteTypes();
}
if ($this->equality || $other->equality) {
$result->equality = true;
}

return $result->setRootExpr($rootExpr);
}
Expand Down Expand Up @@ -204,6 +240,9 @@ public function unionWith(SpecifiedTypes $other): self
if ($this->overwrite || $other->overwrite) {
$result = $result->setAlwaysOverwriteTypes();
}
if ($this->equality || $other->equality) {
$result->equality = true;
}

$conditionalExpressionHolders = $this->newConditionalExpressionHolders;
foreach ($other->newConditionalExpressionHolders as $exprString => $holders) {
Expand Down Expand Up @@ -235,6 +274,7 @@ public function normalize(Scope $scope): self
if ($this->overwrite) {
$result = $result->setAlwaysOverwriteTypes();
}
$result->equality = $this->equality;

return $result->setRootExpr($this->rootExpr);
}
Expand Down
5 changes: 4 additions & 1 deletion src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,10 @@ static function (Type $type, callable $traverse) use ($templateTypeMap, &$contai
$assertedType,
$assert->isNegated() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createTrue(),
$scope,
)->setRootExpr($containsUnresolvedTemplate || $assert->isEquality() ? $call : null);
);
if ($containsUnresolvedTemplate || $assert->isEquality()) {
$newTypes = $newTypes->setEquality();
}
$types = $types !== null ? $types->unionWith($newTypes) : $newTypes;

if (!$context->null() || !$assertedType instanceof ConstantBooleanType) {
Expand Down
14 changes: 14 additions & 0 deletions src/Rules/Comparison/ImpossibleCheckTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,20 @@ private function getSpecifiedType(
return null;
}

if ($specifiedTypes->isEquality()) {
if ($scope->hasExpressionType($node)->yes()) {
$nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node);
if ($nodeType->isTrue()->yes()) {
return true;
}
if ($nodeType->isFalse()->yes()) {
return false;
}
}

return null;
}

$sureTypes = $specifiedTypes->getSureTypes();
$sureNotTypes = $specifiedTypes->getSureNotTypes();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\BinaryOp\Identical;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\SpecifiedTypes;
use PHPStan\Analyser\TypeSpecifier;
Expand Down Expand Up @@ -115,7 +112,7 @@ public function specifyTypes(
$arrayType->getIterableValueType(),
$context,
$scope,
))->setRootExpr(new Identical($arrayDimFetch, new ConstFetch(new Name('__PHPSTAN_FAUX_CONSTANT'))));
))->setEquality();
}

return new SpecifiedTypes();
Expand Down
2 changes: 1 addition & 1 deletion src/Type/Php/PregMatchTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
$matchedType,
$context,
$scope,
)->setRootExpr($node);
)->setEquality();
if ($overwrite) {
$types = $types->setAlwaysOverwriteTypes();
}
Expand Down
15 changes: 1 addition & 14 deletions src/Type/Php/StrContainingTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@

namespace PHPStan\Type\Php;

use PhpParser\Node\Arg;
use PhpParser\Node\Expr\BinaryOp\BooleanAnd;
use PhpParser\Node\Expr\BinaryOp\NotIdentical;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\SpecifiedTypes;
use PHPStan\Analyser\TypeSpecifier;
Expand Down Expand Up @@ -100,15 +95,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
new IntersectionType($accessories),
$context,
$scope,
)->setRootExpr(new BooleanAnd(
new NotIdentical(
$args[$needleArg]->value,
new String_(''),
),
new FuncCall(new Name('FAUX_FUNCTION'), [
new Arg($args[$needleArg]->value),
]),
));
)->setEquality();

if (in_array($lowerFunctionName, self::SUBSTRING_PROVING_FUNCTIONS, true)) {
$specifiedTypes = $specifiedTypes
Expand Down
Loading
Loading