Skip to content

Commit 0d827f8

Browse files
committed
Specfiy root expression from type specifying extensions
1 parent c6b7ee9 commit 0d827f8

File tree

4 files changed

+78
-25
lines changed

4 files changed

+78
-25
lines changed

src/Analyser/TypeSpecifier.php

+20-3
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,7 @@ public function specifyTypesInCondition(
691691
continue;
692692
}
693693

694-
return $extension->specifyTypes($functionReflection, $expr, $scope, $context);
694+
return $this->specifyRootExpr($extension->specifyTypes($functionReflection, $expr, $scope, $context), $scope, $context);
695695
}
696696

697697
if (count($expr->getArgs()) > 0) {
@@ -719,7 +719,7 @@ public function specifyTypesInCondition(
719719
continue;
720720
}
721721

722-
return $extension->specifyTypes($methodReflection, $expr, $scope, $context);
722+
return $this->specifyRootExpr($extension->specifyTypes($methodReflection, $expr, $scope, $context), $scope, $context);
723723
}
724724

725725
if (count($expr->getArgs()) > 0) {
@@ -753,7 +753,7 @@ public function specifyTypesInCondition(
753753
continue;
754754
}
755755

756-
return $extension->specifyTypes($staticMethodReflection, $expr, $scope, $context);
756+
return $this->specifyRootExpr($extension->specifyTypes($staticMethodReflection, $expr, $scope, $context), $scope, $context);
757757
}
758758
}
759759

@@ -1421,4 +1421,21 @@ private function getTypeSpecifyingExtensionsForType(array $extensions, string $c
14211421
return array_merge(...$extensionsForClass);
14221422
}
14231423

1424+
private function specifyRootExpr(SpecifiedTypes $specifiedTypes, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
1425+
{
1426+
$rootExpr = $specifiedTypes->getRootExpr();
1427+
if ($rootExpr === null || $context->null()) {
1428+
return $specifiedTypes;
1429+
}
1430+
1431+
$rootExprType = $scope->getType($rootExpr);
1432+
if (!$rootExprType instanceof BooleanType || $rootExprType instanceof ConstantBooleanType) {
1433+
return $specifiedTypes;
1434+
}
1435+
1436+
return $specifiedTypes->unionWith(
1437+
$this->create($rootExpr, new ConstantBooleanType(true), $context),
1438+
);
1439+
}
1440+
14241441
}

src/Type/Php/StrContainingTypeSpecifyingExtension.php

+22-21
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace PHPStan\Type\Php;
44

5-
use PhpParser\Node\Arg;
65
use PhpParser\Node\Expr\BinaryOp\BooleanAnd;
76
use PhpParser\Node\Expr\BinaryOp\NotIdentical;
87
use PhpParser\Node\Expr\FuncCall;
@@ -27,17 +26,17 @@
2726
final class StrContainingTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension
2827
{
2928

30-
/** @var array<string, array{0: int, 1: int}> */
29+
/** @var array<string, array{0: int, 1: int, 2: bool}> */
3130
private array $strContainingFunctions = [
32-
'fnmatch' => [1, 0],
33-
'str_contains' => [0, 1],
34-
'str_starts_with' => [0, 1],
35-
'str_ends_with' => [0, 1],
36-
'strpos' => [0, 1],
37-
'strrpos' => [0, 1],
38-
'stripos' => [0, 1],
39-
'strripos' => [0, 1],
40-
'strstr' => [0, 1],
31+
'fnmatch' => [1, 0, true],
32+
'str_contains' => [0, 1, true],
33+
'str_starts_with' => [0, 1, true],
34+
'str_ends_with' => [0, 1, true],
35+
'strpos' => [0, 1, false],
36+
'strrpos' => [0, 1, false],
37+
'stripos' => [0, 1, false],
38+
'strripos' => [0, 1, false],
39+
'strstr' => [0, 1, false],
4140
];
4241

4342
private TypeSpecifier $typeSpecifier;
@@ -58,7 +57,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
5857
$args = $node->getArgs();
5958

6059
if (count($args) >= 2) {
61-
[$hackstackArg, $needleArg] = $this->strContainingFunctions[strtolower($functionReflection->getName())];
60+
[$hackstackArg, $needleArg, $evaluatesToBoolean] = $this->strContainingFunctions[strtolower($functionReflection->getName())];
6261

6362
$haystackType = $scope->getType($args[$hackstackArg]->value);
6463
$needleType = $scope->getType($args[$needleArg]->value);
@@ -76,21 +75,23 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
7675
$accessories[] = new AccessoryNumericStringType();
7776
}
7877

78+
$rootExpr = $evaluatesToBoolean
79+
? new BooleanAnd(
80+
new NotIdentical(
81+
$args[$needleArg]->value,
82+
new String_(''),
83+
),
84+
new FuncCall(new Name('FAUX_FUNCTION_' . $functionReflection->getName()), $args),
85+
)
86+
: new FuncCall(new Name('FAUX_FUNCTION_' . $functionReflection->getName()), $args);
87+
7988
return $this->typeSpecifier->create(
8089
$args[$hackstackArg]->value,
8190
new IntersectionType($accessories),
8291
$context,
8392
false,
8493
$scope,
85-
new BooleanAnd(
86-
new NotIdentical(
87-
$args[$needleArg]->value,
88-
new String_(''),
89-
),
90-
new FuncCall(new Name('FAUX_FUNCTION'), [
91-
new Arg($args[$needleArg]->value),
92-
]),
93-
),
94+
$rootExpr,
9495
);
9596
}
9697
}

tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php

+10-1
Original file line numberDiff line numberDiff line change
@@ -532,7 +532,16 @@ public function testNonEmptySpecifiedString(): void
532532
{
533533
$this->checkAlwaysTrueCheckTypeFunctionCall = true;
534534
$this->treatPhpDocTypesAsCertain = true;
535-
$this->analyse([__DIR__ . '/data/non-empty-string-impossible-type.php'], []);
535+
$this->analyse([__DIR__ . '/data/non-empty-string-impossible-type.php'], [
536+
[
537+
'Call to function str_contains() with non-empty-string and \'foo\' will always evaluate to true.',
538+
26,
539+
],
540+
[
541+
'Call to function str_contains() with non-empty-string and \'foo\' will always evaluate to true.',
542+
36,
543+
],
544+
]);
536545
}
537546

538547
public function testBug2755(): void

tests/PHPStan/Rules/Comparison/data/non-empty-string-impossible-type.php

+26
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,30 @@ private function isPrefixedInterface(string $shortClassName): bool
1919

2020
return ctype_lower($shortClassName[2]);
2121
}
22+
23+
public function strContains(string $a, string $b, string $c, string $d, string $e): void
24+
{
25+
if (str_contains($a, 'foo')) {
26+
if (str_contains($a, 'foo')) {
27+
}
28+
}
29+
30+
if (!str_contains($b, 'foo')) {
31+
if (!str_contains($b, 'foo')) {
32+
}
33+
}
34+
35+
if (str_contains($c, 'foo')) {
36+
if (!str_contains($c, 'foo')) {
37+
}
38+
}
39+
40+
if (!str_contains($d, 'foo')) {
41+
if (str_contains($d, 'foo')) {
42+
}
43+
}
44+
45+
if (str_contains($e, 'bar')) {
46+
}
47+
}
2248
}

0 commit comments

Comments
 (0)