Skip to content

Commit 562b730

Browse files
authored
Narrow string on strlen() == and === comparison with integer-range
1 parent 04d0e03 commit 562b730

File tree

3 files changed

+151
-24
lines changed

3 files changed

+151
-24
lines changed

Diff for: src/Analyser/TypeSpecifier.php

+42-22
Original file line numberDiff line numberDiff line change
@@ -1056,13 +1056,19 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type,
10561056

10571057
private function specifyTypesForConstantBinaryExpression(
10581058
Expr $exprNode,
1059-
ConstantScalarType $constantType,
1059+
Type $constantType,
10601060
TypeSpecifierContext $context,
10611061
Scope $scope,
10621062
?Expr $rootExpr,
10631063
): ?SpecifiedTypes
10641064
{
1065-
if (!$context->null() && $constantType->getValue() === false) {
1065+
$scalarValues = $constantType->getConstantScalarValues();
1066+
if (count($scalarValues) !== 1) {
1067+
return null;
1068+
}
1069+
$constValue = $scalarValues[0];
1070+
1071+
if (!$context->null() && $constValue === false) {
10661072
$types = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr);
10671073
if ($exprNode instanceof Expr\NullsafeMethodCall || $exprNode instanceof Expr\NullsafePropertyFetch) {
10681074
return $types;
@@ -1076,7 +1082,7 @@ private function specifyTypesForConstantBinaryExpression(
10761082
));
10771083
}
10781084

1079-
if (!$context->null() && $constantType->getValue() === true) {
1085+
if (!$context->null() && $constValue === true) {
10801086
$types = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr);
10811087
if ($exprNode instanceof Expr\NullsafeMethodCall || $exprNode instanceof Expr\NullsafePropertyFetch) {
10821088
return $types;
@@ -1090,10 +1096,6 @@ private function specifyTypesForConstantBinaryExpression(
10901096
));
10911097
}
10921098

1093-
if ($constantType->getValue() === null) {
1094-
return $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr);
1095-
}
1096-
10971099
if (
10981100
!$context->null()
10991101
&& $exprNode instanceof FuncCall
@@ -1102,6 +1104,10 @@ private function specifyTypesForConstantBinaryExpression(
11021104
&& in_array(strtolower((string) $exprNode->name), ['count', 'sizeof'], true)
11031105
&& $constantType instanceof ConstantIntegerType
11041106
) {
1107+
if ($constantType->getValue() < 0) {
1108+
return $this->create($exprNode->getArgs()[0]->value, new NeverType(), $context, false, $scope, $rootExpr);
1109+
}
1110+
11051111
$argType = $scope->getType($exprNode->getArgs()[0]->value);
11061112

11071113
if ($argType instanceof UnionType) {
@@ -1146,6 +1152,10 @@ private function specifyTypesForConstantBinaryExpression(
11461152
&& in_array(strtolower((string) $exprNode->name), ['strlen', 'mb_strlen'], true)
11471153
&& $constantType instanceof ConstantIntegerType
11481154
) {
1155+
if ($constantType->getValue() < 0) {
1156+
return $this->create($exprNode->getArgs()[0]->value, new NeverType(), $context, false, $scope, $rootExpr);
1157+
}
1158+
11491159
if ($context->truthy() || $constantType->getValue() === 0) {
11501160
$newContext = $context;
11511161
if ($constantType->getValue() === 0) {
@@ -1172,12 +1182,18 @@ private function specifyTypesForConstantBinaryExpression(
11721182

11731183
private function specifyTypesForConstantStringBinaryExpression(
11741184
Expr $exprNode,
1175-
ConstantStringType $constantType,
1185+
Type $constantType,
11761186
TypeSpecifierContext $context,
11771187
Scope $scope,
11781188
?Expr $rootExpr,
11791189
): ?SpecifiedTypes
11801190
{
1191+
$scalarValues = $constantType->getConstantScalarValues();
1192+
if (count($scalarValues) !== 1 || !is_string($scalarValues[0])) {
1193+
return null;
1194+
}
1195+
$constantStringValue = $scalarValues[0];
1196+
11811197
if (
11821198
$context->truthy()
11831199
&& $exprNode instanceof FuncCall
@@ -1188,12 +1204,12 @@ private function specifyTypesForConstantStringBinaryExpression(
11881204
'ucwords', 'mb_convert_case', 'mb_convert_kana',
11891205
], true)
11901206
&& isset($exprNode->getArgs()[0])
1191-
&& $constantType->getValue() !== ''
1207+
&& $constantStringValue !== ''
11921208
) {
11931209
$argType = $scope->getType($exprNode->getArgs()[0]->value);
11941210

11951211
if ($argType->isString()->yes()) {
1196-
if ($constantType->getValue() !== '0') {
1212+
if ($constantStringValue !== '0') {
11971213
return $this->create(
11981214
$exprNode->getArgs()[0]->value,
11991215
TypeCombinator::intersect($argType, new AccessoryNonFalsyStringType()),
@@ -1220,28 +1236,28 @@ private function specifyTypesForConstantStringBinaryExpression(
12201236
&& isset($exprNode->getArgs()[0])
12211237
) {
12221238
$type = null;
1223-
if ($constantType->getValue() === 'string') {
1239+
if ($constantStringValue === 'string') {
12241240
$type = new StringType();
12251241
}
1226-
if ($constantType->getValue() === 'array') {
1242+
if ($constantStringValue === 'array') {
12271243
$type = new ArrayType(new MixedType(), new MixedType());
12281244
}
1229-
if ($constantType->getValue() === 'boolean') {
1245+
if ($constantStringValue === 'boolean') {
12301246
$type = new BooleanType();
12311247
}
1232-
if (in_array($constantType->getValue(), ['resource', 'resource (closed)'], true)) {
1248+
if (in_array($constantStringValue, ['resource', 'resource (closed)'], true)) {
12331249
$type = new ResourceType();
12341250
}
1235-
if ($constantType->getValue() === 'integer') {
1251+
if ($constantStringValue === 'integer') {
12361252
$type = new IntegerType();
12371253
}
1238-
if ($constantType->getValue() === 'double') {
1254+
if ($constantStringValue === 'double') {
12391255
$type = new FloatType();
12401256
}
1241-
if ($constantType->getValue() === 'NULL') {
1257+
if ($constantStringValue === 'NULL') {
12421258
$type = new NullType();
12431259
}
1244-
if ($constantType->getValue() === 'object') {
1260+
if ($constantStringValue === 'object') {
12451261
$type = new ObjectWithoutClassType();
12461262
}
12471263

@@ -1260,7 +1276,7 @@ private function specifyTypesForConstantStringBinaryExpression(
12601276
&& isset($exprNode->getArgs()[0])
12611277
) {
12621278
$argType = $scope->getType($exprNode->getArgs()[0]->value);
1263-
$objectType = new ObjectType($constantType->getValue());
1279+
$objectType = new ObjectType($constantStringValue);
12641280
$classStringType = new GenericClassStringType($objectType);
12651281

12661282
if ($argType->isString()->yes()) {
@@ -2149,10 +2165,14 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
21492165
}
21502166
}
21512167

2152-
if (count($rightType->getConstantStrings()) > 0) {
2168+
if ($rightType->isInteger()->yes() || $rightType->isString()->yes()) {
21532169
$types = null;
2154-
foreach ($rightType->getConstantStrings() as $constantString) {
2155-
$specifiedType = $this->specifyTypesForConstantStringBinaryExpression($unwrappedLeftExpr, $constantString, $context, $scope, $rootExpr);
2170+
foreach ($rightType->getFiniteTypes() as $finiteType) {
2171+
if ($finiteType->isString()->yes()) {
2172+
$specifiedType = $this->specifyTypesForConstantStringBinaryExpression($unwrappedLeftExpr, $finiteType, $context, $scope, $rootExpr);
2173+
} else {
2174+
$specifiedType = $this->specifyTypesForConstantBinaryExpression($unwrappedLeftExpr, $finiteType, $context, $scope, $rootExpr);
2175+
}
21562176
if ($specifiedType === null) {
21572177
continue;
21582178
}

Diff for: tests/PHPStan/Analyser/nsrt/count-type.php

+46
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,50 @@ public function doFoo(
1818
assertType('int<1, max>', sizeof($nonEmpty));
1919
}
2020

21+
/**
22+
* @param int<3,5> $range
23+
* @param int<0,5> $maybeZero
24+
* @param int<-10,-5> $negative
25+
*/
26+
public function doFooBar(
27+
array $arr,
28+
int $range,
29+
int $maybeZero,
30+
int $negative
31+
)
32+
{
33+
if (count($arr) == $range) {
34+
assertType('non-empty-array', $arr);
35+
} else {
36+
assertType('array', $arr);
37+
}
38+
if (count($arr) === $range) {
39+
assertType('non-empty-array', $arr);
40+
} else {
41+
assertType('array', $arr);
42+
}
43+
44+
if (count($arr) == $maybeZero) {
45+
assertType('array', $arr);
46+
} else {
47+
assertType('non-empty-array', $arr);
48+
}
49+
if (count($arr) === $maybeZero) {
50+
assertType('array', $arr);
51+
} else {
52+
assertType('non-empty-array', $arr);
53+
}
54+
55+
if (count($arr) == $negative) {
56+
assertType('*NEVER*', $arr);
57+
} else {
58+
assertType('array', $arr);
59+
}
60+
if (count($arr) === $negative) {
61+
assertType('*NEVER*', $arr);
62+
} else {
63+
assertType('array', $arr);
64+
}
65+
}
66+
2167
}

Diff for: tests/PHPStan/Analyser/nsrt/strlen-int-range.php

+63-2
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
* @param int<2, 3> $twoOrThree
1010
* @param int<2, max> $twoOrMore
1111
* @param int<min, 3> $maxThree
12-
* @param int<10, 11> $tenOrEleven
12+
* @param 10|11 $tenOrEleven
13+
* @param 0|11 $zeroOrEleven
14+
* @param int<-10,-5> $negative
1315
*/
14-
function doFoo(string $s, $zeroToThree, $twoOrThree, $twoOrMore, int $maxThree, $tenOrEleven): void
16+
function doFoo(string $s, $zeroToThree, $twoOrThree, $twoOrMore, int $maxThree, $tenOrEleven, $zeroOrEleven, int $negative): void
1517
{
1618
if (strlen($s) >= $zeroToThree) {
1719
assertType('string', $s);
@@ -51,4 +53,63 @@ function doFoo(string $s, $zeroToThree, $twoOrThree, $twoOrMore, int $maxThree,
5153
if (strlen($s) > $tenOrEleven) {
5254
assertType('non-falsy-string', $s);
5355
}
56+
57+
if (strlen($s) == $zeroToThree) {
58+
assertType('string', $s);
59+
}
60+
if (strlen($s) === $zeroToThree) {
61+
assertType('string', $s);
62+
}
63+
64+
if (strlen($s) == $twoOrThree) {
65+
assertType('non-falsy-string', $s);
66+
}
67+
if (strlen($s) === $twoOrThree) {
68+
assertType('non-falsy-string', $s);
69+
}
70+
71+
if (strlen($s) == $oneOrMore) {
72+
assertType('string', $s); // could be non-empty-string
73+
}
74+
if (strlen($s) === $oneOrMore) {
75+
assertType('string', $s); // could be non-empty-string
76+
}
77+
78+
if (strlen($s) == $tenOrEleven) {
79+
assertType('non-falsy-string', $s);
80+
}
81+
if (strlen($s) === $tenOrEleven) {
82+
assertType('non-falsy-string', $s);
83+
}
84+
if ($tenOrEleven == strlen($s)) {
85+
assertType('non-falsy-string', $s);
86+
}
87+
if ($tenOrEleven === strlen($s)) {
88+
assertType('non-falsy-string', $s);
89+
}
90+
91+
if (strlen($s) == $maxThree) {
92+
assertType('string', $s);
93+
}
94+
if (strlen($s) === $maxThree) {
95+
assertType('string', $s);
96+
}
97+
98+
if (strlen($s) == $zeroOrEleven) {
99+
assertType('string', $s);
100+
}
101+
if (strlen($s) === $zeroOrEleven) {
102+
assertType('string', $s);
103+
}
104+
105+
if (strlen($s) == $negative) {
106+
assertType('*NEVER*', $s);
107+
} else {
108+
assertType('string', $s);
109+
}
110+
if (strlen($s) === $negative) {
111+
assertType('*NEVER*', $s);
112+
} else {
113+
assertType('string', $s);
114+
}
54115
}

0 commit comments

Comments
 (0)