Skip to content

Commit b454cb5

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 cdd76d2 commit b454cb5

7 files changed

Lines changed: 127 additions & 19 deletions

File tree

‎src/Analyser/SpecifiedTypes.php‎

Lines changed: 42 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 $shouldNotImplyOppositeCase = false;
17+
1618
/** @var array<string, ConditionalExpressionHolder[]> */
1719
private array $newConditionalExpressionHolders = [];
1820

@@ -51,6 +53,29 @@ public function setAlwaysOverwriteTypes(): self
5153
{
5254
$self = new self($this->sureTypes, $this->sureNotTypes);
5355
$self->overwrite = true;
56+
$self->shouldNotImplyOppositeCase = $this->shouldNotImplyOppositeCase;
57+
$self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders;
58+
$self->rootExpr = $this->rootExpr;
59+
60+
return $self;
61+
}
62+
63+
/**
64+
* Normally, when a type-specifying extension returns SpecifiedTypes with sureTypes,
65+
* ImpossibleCheckTypeHelper will analyze whether those types are already satisfied
66+
* and conclude the check is always-true/always-false.
67+
*
68+
* When this flag is set, that analysis is skipped. Use this when the sureTypes
69+
* are a side effect of the check (e.g. str_contains narrowing haystack to non-empty-string)
70+
* rather than the determining condition.
71+
*
72+
* @api
73+
*/
74+
public function setShouldNotImplyOppositeCase(): self
75+
{
76+
$self = new self($this->sureTypes, $this->sureNotTypes);
77+
$self->overwrite = $this->overwrite;
78+
$self->shouldNotImplyOppositeCase = true;
5479
$self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders;
5580
$self->rootExpr = $this->rootExpr;
5681

@@ -64,6 +89,7 @@ public function setRootExpr(?Expr $rootExpr): self
6489
{
6590
$self = new self($this->sureTypes, $this->sureNotTypes);
6691
$self->overwrite = $this->overwrite;
92+
$self->shouldNotImplyOppositeCase = $this->shouldNotImplyOppositeCase;
6793
$self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders;
6894
$self->rootExpr = $rootExpr;
6995

@@ -77,6 +103,7 @@ public function setNewConditionalExpressionHolders(array $newConditionalExpressi
77103
{
78104
$self = new self($this->sureTypes, $this->sureNotTypes);
79105
$self->overwrite = $this->overwrite;
106+
$self->shouldNotImplyOppositeCase = $this->shouldNotImplyOppositeCase;
80107
$self->newConditionalExpressionHolders = $newConditionalExpressionHolders;
81108
$self->rootExpr = $this->rootExpr;
82109

@@ -106,6 +133,11 @@ public function shouldOverwrite(): bool
106133
return $this->overwrite;
107134
}
108135

136+
public function shouldNotImplyOppositeCase(): bool
137+
{
138+
return $this->shouldNotImplyOppositeCase;
139+
}
140+
109141
/**
110142
* @return array<string, ConditionalExpressionHolder[]>
111143
*/
@@ -128,6 +160,7 @@ public function removeExpr(string $exprString): self
128160

129161
$self = new self($sureTypes, $sureNotTypes);
130162
$self->overwrite = $this->overwrite;
163+
$self->shouldNotImplyOppositeCase = $this->shouldNotImplyOppositeCase;
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->shouldNotImplyOppositeCase || $other->shouldNotImplyOppositeCase) {
204+
$result = $result->setShouldNotImplyOppositeCase();
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->shouldNotImplyOppositeCase || $other->shouldNotImplyOppositeCase) {
244+
$result = $result->setShouldNotImplyOppositeCase();
245+
}
207246

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

239281
return $result->setRootExpr($this->rootExpr);
240282
}

‎src/Analyser/TypeSpecifier.php‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1879,7 +1879,10 @@ static function (Type $type, callable $traverse) use ($templateTypeMap, &$contai
18791879
$assertedType,
18801880
$assert->isNegated() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createTrue(),
18811881
$scope,
1882-
)->setRootExpr($containsUnresolvedTemplate || $assert->isEquality() ? $call : null);
1882+
)->setRootExpr($containsUnresolvedTemplate ? $call : null);
1883+
if ($assert->isEquality()) {
1884+
$newTypes = $newTypes->setShouldNotImplyOppositeCase();
1885+
}
18831886
$types = $types !== null ? $types->unionWith($newTypes) : $newTypes;
18841887

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

‎src/Rules/Comparison/ImpossibleCheckTypeHelper.php‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,10 @@ private function getSpecifiedType(
297297
return null;
298298
}
299299

300+
if ($specifiedTypes->shouldNotImplyOppositeCase()) {
301+
return null;
302+
}
303+
300304
$sureTypes = $specifiedTypes->getSureTypes();
301305
$sureNotTypes = $specifiedTypes->getSureNotTypes();
302306

‎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+
))->setShouldNotImplyOppositeCase();
119116
}
120117

121118
return new SpecifiedTypes();

‎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;
@@ -89,15 +84,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
8984
new IntersectionType($accessories),
9085
$context,
9186
$scope,
92-
)->setRootExpr(new BooleanAnd(
93-
new NotIdentical(
94-
$args[$needleArg]->value,
95-
new String_(''),
96-
),
97-
new FuncCall(new Name('FAUX_FUNCTION'), [
98-
new Arg($args[$needleArg]->value),
99-
]),
100-
));
87+
)->setShouldNotImplyOppositeCase();
10188
}
10289
}
10390

‎tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,12 @@ public function testNonEmptySpecifiedString(): void
526526
$this->analyse([__DIR__ . '/data/non-empty-string-impossible-type.php'], []);
527527
}
528528

529+
public function testBug14705(): void
530+
{
531+
$this->treatPhpDocTypesAsCertain = true;
532+
$this->analyse([__DIR__ . '/data/bug-14705.php'], []);
533+
}
534+
529535
public function testBug2755(): void
530536
{
531537
$this->treatPhpDocTypesAsCertain = true;
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace Bug14705;
4+
5+
class Foo
6+
{
7+
8+
/**
9+
* str_contains with non-empty-string haystack should not report always-true.
10+
*
11+
* @param non-empty-string $haystack
12+
*/
13+
public function strContainsNonEmpty(string $haystack, string $needle): void
14+
{
15+
if (str_contains($haystack, $needle)) {
16+
17+
}
18+
}
19+
20+
/**
21+
* str_starts_with with non-empty-string haystack should not report always-true.
22+
*
23+
* @param non-empty-string $haystack
24+
*/
25+
public function strStartsWithNonEmpty(string $haystack, string $needle): void
26+
{
27+
if (str_starts_with($haystack, $needle)) {
28+
29+
}
30+
}
31+
32+
/**
33+
* str_ends_with with non-empty-string haystack should not report always-true.
34+
*
35+
* @param non-empty-string $haystack
36+
*/
37+
public function strEndsWithNonEmpty(string $haystack, string $needle): void
38+
{
39+
if (str_ends_with($haystack, $needle)) {
40+
41+
}
42+
}
43+
44+
/**
45+
* strpos with non-empty-string haystack should not report always-true.
46+
*
47+
* @param non-empty-string $haystack
48+
* @param non-empty-string $needle
49+
*/
50+
public function strposNonEmpty(string $haystack, string $needle): void
51+
{
52+
if (strpos($haystack, $needle) !== false) {
53+
54+
}
55+
}
56+
57+
/**
58+
* array_key_exists with non-constant key on a non-empty-array should not report always-true.
59+
*
60+
* @param non-empty-array<string, int> $array
61+
*/
62+
public function arrayKeyExistsNonEmpty(array $array, string $key): void
63+
{
64+
if (array_key_exists($key, $array)) {
65+
66+
}
67+
}
68+
69+
}

0 commit comments

Comments
 (0)