Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a69d4eb
Add `setShouldNotImplyOppositeCase()` on `SpecifiedTypes` to replace …
VincentLanglet May 26, 2026
1cadef4
Add comment explaining why shouldNotImplyOppositeCase causes early re…
phpstan-bot May 26, 2026
fb3005e
Rename `shouldNotImplyOppositeCase` to `shouldNotDetermineCheckResult`
phpstan-bot May 26, 2026
9d1f4c9
Rename `shouldNotDetermineCheckResult` to `specifyOnly`
phpstan-bot May 26, 2026
0c16397
Keep rootExpr for equality assertions, move specifyOnly after rootExp…
phpstan-bot May 27, 2026
d2eb35a
Rework
VincentLanglet May 27, 2026
cad1b66
Remove unused specifyOnly flag, document setRootExpr
phpstan-bot May 27, 2026
cd692ef
Add duplicate call detection for rootExpr-based type specifying
phpstan-bot May 27, 2026
275cede
Remove duplicate array_key_exists check
phpstan-bot May 27, 2026
959e85d
Add setSideEffectOnly() flag on SpecifiedTypes, replace rootExpr work…
phpstan-bot May 28, 2026
10103e3
Split bug-14705 test into PHP 7.4-compatible and PHP 8.0+ parts
phpstan-bot May 28, 2026
4c9d7f5
Rename sideEffectOnly to specifyOnly on SpecifiedTypes
phpstan-bot May 28, 2026
221d0c5
Revert unrelated duplicate array_key_exists removal
phpstan-bot May 28, 2026
2dc1f5a
Document setSpecifyOnly() for third-party extension migration
phpstan-bot May 28, 2026
4e7a81b
Correct setSpecifyOnly() PHPDoc: clarify filterByTruthyValue scope
phpstan-bot May 28, 2026
24e3809
Store specifyOnly boolean marker via overwrite to fix duplicate detec…
phpstan-bot May 29, 2026
66eadc4
Fix specifyOnly boolean marker overwriting function return types
phpstan-bot May 29, 2026
6f76184
Explain specifyOnly expression-statement handling in NodeScopeResolver
phpstan-bot May 29, 2026
f9fd38e
Move bug-14705 test into nsrt with assertType, merge realpath elvis c…
phpstan-bot May 29, 2026
5367cbf
Rename SpecifiedTypes getter to shouldSpecifyOnly() for consistency
phpstan-bot May 29, 2026
6c5f50c
Add array_key_exists duplicate-in-loop assertType case to bug-14705
phpstan-bot May 29, 2026
f898a44
Improvement
VincentLanglet May 30, 2026
0c04055
Revert "Improvement"
VincentLanglet May 30, 2026
af61ebd
Remove duplicate-detection paragraph from setSpecifyOnly() PHPDoc
phpstan-bot May 30, 2026
aed8f0f
Shorten specifyOnly expression-statement comment
phpstan-bot May 30, 2026
ecafc82
Annotate duplicate array_key_exists assertType with '// could be true'
phpstan-bot May 30, 2026
7a70a0b
Avoid setAlwaysOverwriteTypes() in specifyOnly statement path
phpstan-bot Jun 4, 2026
17d1bee
Rename specifyOnly to equality on SpecifiedTypes
phpstan-bot Jun 4, 2026
5495f04
Skip equality boolean marker for non-boolean expressions
phpstan-bot Jun 25, 2026
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 @@ -3302,6 +3302,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 @@ -3319,6 +3324,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 @@ -1142,11 +1143,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 @@ -473,7 +473,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 @@ -309,6 +309,20 @@
return null;
}

if ($specifiedTypes->isEquality()) {
if ($scope->hasExpressionType($node)->yes()) {

Check warning on line 313 in src/Rules/Comparison/ImpossibleCheckTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } if ($specifiedTypes->isEquality()) { - if ($scope->hasExpressionType($node)->yes()) { + if (!$scope->hasExpressionType($node)->no()) { $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node); if ($nodeType->isTrue()->yes()) { return true;

Check warning on line 313 in src/Rules/Comparison/ImpossibleCheckTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } if ($specifiedTypes->isEquality()) { - if ($scope->hasExpressionType($node)->yes()) { + if (!$scope->hasExpressionType($node)->no()) { $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node); if ($nodeType->isTrue()->yes()) { return true;
$nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node);
if ($nodeType->isTrue()->yes()) {

Check warning on line 315 in src/Rules/Comparison/ImpossibleCheckTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\LooseBooleanMutator": @@ @@ if ($specifiedTypes->isEquality()) { if ($scope->hasExpressionType($node)->yes()) { $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node); - if ($nodeType->isTrue()->yes()) { + if ($nodeType->toBoolean()->isTrue()->yes()) { return true; } if ($nodeType->isFalse()->yes()) {

Check warning on line 315 in src/Rules/Comparison/ImpossibleCheckTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ($specifiedTypes->isEquality()) { if ($scope->hasExpressionType($node)->yes()) { $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node); - if ($nodeType->isTrue()->yes()) { + if (!$nodeType->toBoolean()->isTrue()->no()) { return true; } if ($nodeType->isFalse()->yes()) {

Check warning on line 315 in src/Rules/Comparison/ImpossibleCheckTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\LooseBooleanMutator": @@ @@ if ($specifiedTypes->isEquality()) { if ($scope->hasExpressionType($node)->yes()) { $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node); - if ($nodeType->isTrue()->yes()) { + if ($nodeType->toBoolean()->isTrue()->yes()) { return true; } if ($nodeType->isFalse()->yes()) {
return true;
}
if ($nodeType->isFalse()->yes()) {

Check warning on line 318 in src/Rules/Comparison/ImpossibleCheckTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\LooseBooleanMutator": @@ @@ if ($nodeType->isTrue()->yes()) { return true; } - if ($nodeType->isFalse()->yes()) { + if ($nodeType->toBoolean()->isFalse()->yes()) { return false; } }

Check warning on line 318 in src/Rules/Comparison/ImpossibleCheckTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ($nodeType->isTrue()->yes()) { return true; } - if ($nodeType->isFalse()->yes()) { + if (!$nodeType->toBoolean()->isFalse()->no()) { return false; } }
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 @@ -89,15 +84,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();
}
}

Expand Down
Loading
Loading