Skip to content

Commit 14ad31b

Browse files
VincentLangletondrejmirtes
authored andcommitted
Add setShouldNotImplyOppositeCase() on SpecifiedTypes to replace FAUX function call workarounds
- Add `shouldNotImplyOppositeCase` flag to `SpecifiedTypes` with `@api`-tagged setter and getter methods, propagated through all immutable-copy operations (setAlwaysOverwriteTypes, setRootExpr, setNewConditionalExpressionHolders, removeExpr, intersectWith, unionWith, normalize) - Check the flag in `ImpossibleCheckTypeHelper::findSpecifiedType()` to return null early, preventing false "always true/false" reports when sureTypes are side effects of a check rather than its determining condition - Replace `FAUX_FUNCTION` rootExpr in `StrContainingTypeSpecifyingExtension` with `setShouldNotImplyOppositeCase()` - Replace `__PHPSTAN_FAUX_CONSTANT` rootExpr in `ArrayKeyExistsFunctionTypeSpecifyingExtension` with `setShouldNotImplyOppositeCase()` - Use the flag for equality assertions in `TypeSpecifier::specifyTypesFromAsserts()` instead of setting rootExpr to the call expression - Remove unused imports (Arg, BooleanAnd, NotIdentical, String_, Name, Identical, ConstFetch) from the two extension files Closes phpstan/phpstan#14705
1 parent 9251436 commit 14ad31b

12 files changed

Lines changed: 356 additions & 22 deletions

‎src/Analyser/MutatingScope.php‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3496,6 +3496,11 @@ public function filterByTruthyValue(Expr $expr): self
34963496
}
34973497

34983498
$specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($this, $expr, TypeSpecifierContext::createTruthy());
3499+
if ($specifiedTypes->isEquality() && $this->getType($expr)->isBoolean()->yes()) {
3500+
$specifiedTypes = $specifiedTypes->unionWith(
3501+
$this->typeSpecifier->create($expr, new ConstantBooleanType(true), TypeSpecifierContext::createTrue(), $this),
3502+
);
3503+
}
34993504
$scope = $this->applySpecifiedTypes($specifiedTypes);
35003505
$this->truthyScopes[$exprString] = $scope;
35013506

@@ -3513,6 +3518,11 @@ public function filterByFalseyValue(Expr $expr): self
35133518
}
35143519

35153520
$specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($this, $expr, TypeSpecifierContext::createFalsey());
3521+
if ($specifiedTypes->isEquality() && $this->getType($expr)->isBoolean()->yes()) {
3522+
$specifiedTypes = $specifiedTypes->unionWith(
3523+
$this->typeSpecifier->create($expr, new ConstantBooleanType(false), TypeSpecifierContext::createTrue(), $this),
3524+
);
3525+
}
35163526
$scope = $this->applySpecifiedTypes($specifiedTypes);
35173527
$this->falseyScopes[$exprString] = $scope;
35183528

‎src/Analyser/SpecifiedTypes.php‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ final class SpecifiedTypes
2121

2222
private bool $overwrite = false;
2323

24+
private bool $equality = false;
25+
2426
/** @var array<string, ConditionalExpressionHolder[]> */
2527
private array $newConditionalExpressionHolders = [];
2628

@@ -93,6 +95,30 @@ public function setAlwaysOverwriteTypes(): self
9395
return $self;
9496
}
9597

98+
/**
99+
* Marks these types as coming from an equality check, the same concept as
100+
* the "=Type" equality assertions documented at
101+
* https://phpstan.org/writing-php-code/narrowing-types#equality-assertions
102+
*
103+
* The narrowed types are only applied; they do not determine the check
104+
* outcome, so ImpossibleCheckTypeHelper will not use them to report
105+
* always-true/false for the check expression.
106+
*
107+
* @api
108+
*/
109+
public function setEquality(): self
110+
{
111+
$self = clone $this;
112+
$self->equality = true;
113+
114+
return $self;
115+
}
116+
117+
public function isEquality(): bool
118+
{
119+
return $this->equality;
120+
}
121+
96122
/**
97123
* @api
98124
*/
@@ -311,6 +337,9 @@ public function intersectWith(SpecifiedTypes $other): self
311337
if ($this->overwrite && $other->overwrite) {
312338
$result = $result->setAlwaysOverwriteTypes();
313339
}
340+
if ($this->equality || $other->equality) {
341+
$result->equality = true;
342+
}
314343

315344
return $result->setRootExpr($rootExpr);
316345
}
@@ -530,6 +559,9 @@ public function unionWith(SpecifiedTypes $other): self
530559
if ($this->overwrite || $other->overwrite) {
531560
$result = $result->setAlwaysOverwriteTypes();
532561
}
562+
if ($this->equality || $other->equality) {
563+
$result->equality = true;
564+
}
533565

534566
$conditionalExpressionHolders = $this->newConditionalExpressionHolders;
535567
foreach ($other->newConditionalExpressionHolders as $exprString => $holders) {
@@ -568,6 +600,7 @@ public static function unionAll(array $typesList): self
568600
$conditionalExpressionHolders = [];
569601
$recipes = [];
570602
$augments = [];
603+
$equality = false;
571604

572605
foreach ($typesList as $types) {
573606
foreach ($types->sureTypes as $exprString => [$exprNode, $type]) {
@@ -588,6 +621,7 @@ public static function unionAll(array $typesList): self
588621
}
589622

590623
$overwrite = $overwrite || $types->overwrite;
624+
$equality = $equality || $types->equality;
591625
$rootExpr = self::mergeRootExpr($rootExpr, $types->rootExpr);
592626

593627
foreach ($types->newConditionalExpressionHolders as $exprString => $holders) {
@@ -618,6 +652,7 @@ public static function unionAll(array $typesList): self
618652
$result->newConditionalExpressionHolders = $conditionalExpressionHolders;
619653
$result->conditionalExpressionHolderRecipes = $recipes;
620654
$result->deferredAugments = $augments;
655+
$result->equality = $equality;
621656

622657
return $result->setRootExpr($rootExpr);
623658
}

‎src/Analyser/StmtHandler/ExpressionHandler.php‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use PHPStan\Node\NoopExpressionNode;
2424
use PHPStan\Node\PropertyAssignNode;
2525
use PHPStan\Node\VariableAssignNode;
26+
use PHPStan\Type\Constant\ConstantBooleanType;
2627
use PHPStan\Type\NeverType;
2728
use PHPStan\Type\ObjectType;
2829
use function array_filter;
@@ -95,11 +96,21 @@ public function processStmt(
9596
$scope = $result->getScope();
9697
// the expression statement was just processed; read its narrowing from
9798
// the result instead of re-resolving it via specifyTypesInCondition().
98-
$scope = $scope->applySpecifiedTypes($this->typeSpecifier->specifyTypesInCondition(
99+
$specifiedTypes = $this->typeSpecifier->specifyTypesInCondition(
99100
$scope,
100101
$stmt->expr,
101102
TypeSpecifierContext::createNull(),
102-
));
103+
);
104+
$scope = $scope->applySpecifiedTypes($specifiedTypes);
105+
106+
if ($specifiedTypes->isEquality()) {
107+
// Statement counterpart of the equality handling in filterByTruthyValue():
108+
// store the call's true result so a duplicate void assertion statement is
109+
// reported as always-true. We assign directly because void calls have no
110+
// return value to protect, and intersecting true with void would produce never.
111+
$scope = $scope->assignExpression($stmt->expr, new ConstantBooleanType(true), new ConstantBooleanType(true));
112+
}
113+
103114
$hasYield = $result->hasYield();
104115
$throwPoints = $result->getThrowPoints();
105116
$impurePoints = $result->getImpurePoints();

‎src/Analyser/TypeSpecifier.php‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,10 @@ static function (Type $type, callable $traverse) use ($templateTypeMap, &$contai
470470
$assertedType,
471471
$assert->isNegated() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createTrue(),
472472
$scope,
473-
)->setRootExpr($containsUnresolvedTemplate || $assert->isEquality() ? $call : null);
473+
);
474+
if ($containsUnresolvedTemplate || $assert->isEquality()) {
475+
$newTypes = $newTypes->setEquality();
476+
}
474477
$types = $types !== null ? $types->unionWith($newTypes) : $newTypes;
475478

476479
if (!$context->null() || !$assertedType instanceof ConstantBooleanType) {

‎src/Rules/Comparison/ImpossibleCheckTypeHelper.php‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,20 @@ private function getSpecifiedType(
322322
return null;
323323
}
324324

325+
if ($specifiedTypes->isEquality()) {
326+
if ($scope->hasExpressionType($node)->yes()) {
327+
$nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node);
328+
if ($nodeType->isTrue()->yes()) {
329+
return true;
330+
}
331+
if ($nodeType->isFalse()->yes()) {
332+
return false;
333+
}
334+
}
335+
336+
return null;
337+
}
338+
325339
$sureTypes = $specifiedTypes->getSureTypes();
326340
$sureNotTypes = $specifiedTypes->getSureNotTypes();
327341

‎src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33
namespace PHPStan\Type\Php;
44

55
use PhpParser\Node\Expr\ArrayDimFetch;
6-
use PhpParser\Node\Expr\BinaryOp\Identical;
7-
use PhpParser\Node\Expr\ConstFetch;
86
use PhpParser\Node\Expr\FuncCall;
9-
use PhpParser\Node\Name;
107
use PHPStan\Analyser\Scope;
118
use PHPStan\Analyser\SpecifiedTypes;
129
use PHPStan\Analyser\TypeSpecifier;
@@ -115,7 +112,7 @@ public function specifyTypes(
115112
$arrayType->getIterableValueType(),
116113
$context,
117114
$scope,
118-
))->setRootExpr(new Identical($arrayDimFetch, new ConstFetch(new Name('__PHPSTAN_FAUX_CONSTANT'))));
115+
))->setEquality();
119116
}
120117

121118
return new SpecifiedTypes();

‎src/Type/Php/PregMatchTypeSpecifyingExtension.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
106106
$matchedType,
107107
$context,
108108
$scope,
109-
)->setRootExpr($node);
109+
)->setEquality();
110110
if ($overwrite) {
111111
$types = $types->setAlwaysOverwriteTypes();
112112
}

‎src/Type/Php/StrContainingTypeSpecifyingExtension.php‎

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,7 @@
22

33
namespace PHPStan\Type\Php;
44

5-
use PhpParser\Node\Arg;
6-
use PhpParser\Node\Expr\BinaryOp\BooleanAnd;
7-
use PhpParser\Node\Expr\BinaryOp\NotIdentical;
85
use PhpParser\Node\Expr\FuncCall;
9-
use PhpParser\Node\Name;
10-
use PhpParser\Node\Scalar\String_;
116
use PHPStan\Analyser\Scope;
127
use PHPStan\Analyser\SpecifiedTypes;
138
use PHPStan\Analyser\TypeSpecifier;
@@ -100,15 +95,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
10095
new IntersectionType($accessories),
10196
$context,
10297
$scope,
103-
)->setRootExpr(new BooleanAnd(
104-
new NotIdentical(
105-
$args[$needleArg]->value,
106-
new String_(''),
107-
),
108-
new FuncCall(new Name('FAUX_FUNCTION'), [
109-
new Arg($args[$needleArg]->value),
110-
]),
111-
));
98+
)->setEquality();
11299

113100
if (in_array($lowerFunctionName, self::SUBSTRING_PROVING_FUNCTIONS, true)) {
114101
$specifiedTypes = $specifiedTypes

0 commit comments

Comments
 (0)