Skip to content

Commit 645777d

Browse files
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 be6f4de commit 645777d

12 files changed

Lines changed: 359 additions & 22 deletions

‎src/Analyser/MutatingScope.php‎

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

32303230
$specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($this, $expr, TypeSpecifierContext::createTruthy());
3231+
if ($specifiedTypes->isEquality() && $this->getType($expr)->isBoolean()->yes()) {
3232+
$specifiedTypes = $specifiedTypes->unionWith(
3233+
$this->typeSpecifier->create($expr, new ConstantBooleanType(true), TypeSpecifierContext::createTrue(), $this),
3234+
);
3235+
}
32313236
$scope = $this->filterBySpecifiedTypes($specifiedTypes);
32323237
$this->truthyScopes[$exprString] = $scope;
32333238

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

32473252
$specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($this, $expr, TypeSpecifierContext::createFalsey());
3253+
if ($specifiedTypes->isEquality() && $this->getType($expr)->isBoolean()->yes()) {
3254+
$specifiedTypes = $specifiedTypes->unionWith(
3255+
$this->typeSpecifier->create($expr, new ConstantBooleanType(false), TypeSpecifierContext::createTrue(), $this),
3256+
);
3257+
}
32483258
$scope = $this->filterBySpecifiedTypes($specifiedTypes);
32493259
$this->falseyScopes[$exprString] = $scope;
32503260

‎src/Analyser/NodeScopeResolver.php‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@
143143
use PHPStan\ShouldNotHappenException;
144144
use PHPStan\TrinaryLogic;
145145
use PHPStan\Type\ClosureType;
146+
use PHPStan\Type\Constant\ConstantBooleanType;
146147
use PHPStan\Type\Constant\ConstantIntegerType;
147148
use PHPStan\Type\Constant\ConstantStringType;
148149
use PHPStan\Type\FileTypeMapper;
@@ -1148,11 +1149,19 @@ public function processStmtNode(
11481149
$this->callNodeCallback($nodeCallback, new NoopExpressionNode($stmt->expr, $hasAssign), $scope, $storage);
11491150
}
11501151
$scope = $result->getScope();
1151-
$scope = $scope->filterBySpecifiedTypes($this->typeSpecifier->specifyTypesInCondition(
1152+
$specifiedTypes = $this->typeSpecifier->specifyTypesInCondition(
11521153
$scope,
11531154
$stmt->expr,
11541155
TypeSpecifierContext::createNull(),
1155-
));
1156+
);
1157+
$scope = $scope->filterBySpecifiedTypes($specifiedTypes);
1158+
if ($specifiedTypes->isEquality()) {
1159+
// Statement counterpart of the equality handling in filterByTruthyValue():
1160+
// store the call's true result so a duplicate void assertion statement is
1161+
// reported as always-true. We assign directly because void calls have no
1162+
// return value to protect, and intersecting true with void would produce never.
1163+
$scope = $scope->assignExpression($stmt->expr, new ConstantBooleanType(true), new ConstantBooleanType(true));
1164+
}
11561165
$hasYield = $result->hasYield();
11571166
$throwPoints = $result->getThrowPoints();
11581167
$impurePoints = $result->getImpurePoints();

‎src/Analyser/SpecifiedTypes.php‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ final class SpecifiedTypes
1313

1414
private bool $overwrite = false;
1515

16+
private bool $equality = false;
17+
1618
/** @var array<string, ConditionalExpressionHolder[]> */
1719
private array $newConditionalExpressionHolders = [];
1820

@@ -51,19 +53,48 @@ public function setAlwaysOverwriteTypes(): self
5153
{
5254
$self = new self($this->sureTypes, $this->sureNotTypes);
5355
$self->overwrite = true;
56+
$self->equality = $this->equality;
57+
$self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders;
58+
$self->rootExpr = $this->rootExpr;
59+
60+
return $self;
61+
}
62+
63+
/**
64+
* Marks these types as coming from an equality check, the same concept as
65+
* the "=Type" equality assertions documented at
66+
* https://phpstan.org/writing-php-code/narrowing-types#equality-assertions
67+
*
68+
* The narrowed types are only applied; they do not determine the check
69+
* outcome, so ImpossibleCheckTypeHelper will not use them to report
70+
* always-true/false for the check expression.
71+
*
72+
* @api
73+
*/
74+
public function setEquality(): self
75+
{
76+
$self = new self($this->sureTypes, $this->sureNotTypes);
77+
$self->overwrite = $this->overwrite;
78+
$self->equality = true;
5479
$self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders;
5580
$self->rootExpr = $this->rootExpr;
5681

5782
return $self;
5883
}
5984

85+
public function isEquality(): bool
86+
{
87+
return $this->equality;
88+
}
89+
6090
/**
6191
* @api
6292
*/
6393
public function setRootExpr(?Expr $rootExpr): self
6494
{
6595
$self = new self($this->sureTypes, $this->sureNotTypes);
6696
$self->overwrite = $this->overwrite;
97+
$self->equality = $this->equality;
6798
$self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders;
6899
$self->rootExpr = $rootExpr;
69100

@@ -77,6 +108,7 @@ public function setNewConditionalExpressionHolders(array $newConditionalExpressi
77108
{
78109
$self = new self($this->sureTypes, $this->sureNotTypes);
79110
$self->overwrite = $this->overwrite;
111+
$self->equality = $this->equality;
80112
$self->newConditionalExpressionHolders = $newConditionalExpressionHolders;
81113
$self->rootExpr = $this->rootExpr;
82114

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

129161
$self = new self($sureTypes, $sureNotTypes);
130162
$self->overwrite = $this->overwrite;
163+
$self->equality = $this->equality;
131164
$self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders;
132165
$self->rootExpr = $this->rootExpr;
133166

@@ -167,6 +200,9 @@ public function intersectWith(SpecifiedTypes $other): self
167200
if ($this->overwrite && $other->overwrite) {
168201
$result = $result->setAlwaysOverwriteTypes();
169202
}
203+
if ($this->equality || $other->equality) {
204+
$result->equality = true;
205+
}
170206

171207
return $result->setRootExpr($rootExpr);
172208
}
@@ -204,6 +240,9 @@ public function unionWith(SpecifiedTypes $other): self
204240
if ($this->overwrite || $other->overwrite) {
205241
$result = $result->setAlwaysOverwriteTypes();
206242
}
243+
if ($this->equality || $other->equality) {
244+
$result->equality = true;
245+
}
207246

208247
$conditionalExpressionHolders = $this->newConditionalExpressionHolders;
209248
foreach ($other->newConditionalExpressionHolders as $exprString => $holders) {
@@ -235,6 +274,7 @@ public function normalize(Scope $scope): self
235274
if ($this->overwrite) {
236275
$result = $result->setAlwaysOverwriteTypes();
237276
}
277+
$result->equality = $this->equality;
238278

239279
return $result->setRootExpr($this->rootExpr);
240280
}

‎src/Analyser/TypeSpecifier.php‎

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

475478
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)