diff --git a/src/Type/Php/StrlenFunctionTypeSpecifyingExtension.php b/src/Type/Php/StrlenFunctionTypeSpecifyingExtension.php new file mode 100644 index 0000000000..ff074b92c2 --- /dev/null +++ b/src/Type/Php/StrlenFunctionTypeSpecifyingExtension.php @@ -0,0 +1,65 @@ +null() + && count($node->getArgs()) >= 1 + && in_array($functionReflection->getName(), ['strlen', 'mb_strlen'], true); + } + + public function specifyTypes( + FunctionReflection $functionReflection, + FuncCall $node, + Scope $scope, + TypeSpecifierContext $context, + ): SpecifiedTypes + { + if (!$scope->getType($node->getArgs()[0]->value)->isString()->yes()) { + return new SpecifiedTypes([], []); + } + + $argSpecifiedTypes = $this->typeSpecifier->create($node->getArgs()[0]->value, new AccessoryNonEmptyStringType(), $context, $scope); + + if ($context->truthy()) { + return $this->typeSpecifier->create($node, StaticTypeFactory::falsey(), TypeSpecifierContext::createFalse(), $scope) + ->setRootExpr($node) + ->unionWith($argSpecifiedTypes); + } + + return $this->typeSpecifier->create($node, StaticTypeFactory::truthy(), TypeSpecifierContext::createFalse(), $scope) + ->setRootExpr($node) + ->unionWith($argSpecifiedTypes); + } + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13688.php b/tests/PHPStan/Analyser/nsrt/bug-13688.php new file mode 100644 index 0000000000..91a6c2af57 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13688.php @@ -0,0 +1,140 @@ + 0) { + assertType('non-empty-string', $input); + } +} + +function doBar(string $s): void +{ + $len = strlen($s); + if ($len) { + assertType('non-empty-string', $s); + } else { + assertType("''", $s); + } +} + +/** + * @param ''|':' $input + */ +function doBaz(string $input): void +{ + $inputLen = strlen($input); + if ($inputLen > 0) { + assertType("':'", $input); + assertType('1', $inputLen); + } +} + +function directTruthy(string $s): void +{ + if (strlen($s)) { + assertType('non-empty-string', $s); + } else { + assertType("''", $s); + } + assertType('string', $s); +} + +function directNegation(string $s): void +{ + if (!strlen($s)) { + assertType("''", $s); + } else { + assertType('non-empty-string', $s); + } + assertType('string', $s); +} + +function mbStrlenTruthy(string $s): void +{ + if (mb_strlen($s)) { + assertType('non-empty-string', $s); + } else { + assertType("''", $s); + } +} + +function mbStrlenNegation(string $s): void +{ + if (!mb_strlen($s)) { + assertType("''", $s); + } else { + assertType('non-empty-string', $s); + } +} + +function booleanAnd(string $a, string $b): void +{ + if (strlen($a) && strlen($b)) { + assertType('non-empty-string', $a); + assertType('non-empty-string', $b); + } +} + +function booleanOr(string $a, string $b): void +{ + if (!strlen($a) || !strlen($b)) { + return; + } + assertType('non-empty-string', $a); + assertType('non-empty-string', $b); +} + +function ternary(string $s): void +{ + $result = strlen($s) ? 'non-empty' : 'empty'; + assertType("'empty'|'non-empty'", $result); +} + +/** @param int|string $intOrString */ +function nonStringInput($intOrString): void +{ + if (strlen($intOrString)) { + assertType('int|string', $intOrString); + } +} + +/** @param float|string $floatOrString */ +function nonStringInputFloat($floatOrString): void +{ + if (strlen($floatOrString)) { + assertType('float|string', $floatOrString); + } +} + +/** @param bool|string $boolOrString */ +function nonStringInputBool($boolOrString): void +{ + if (strlen($boolOrString)) { + assertType('bool|string', $boolOrString); + } +} + +function looseComparisonTrue(string $s): void +{ + if (strlen($s) == true) { + assertType('non-empty-string', $s); + } else { + assertType("''", $s); + } +} + +function looseComparisonFalse(string $s): void +{ + if (strlen($s) == false) { + assertType("''", $s); + } else { + assertType('non-empty-string', $s); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/narrow-cast.php b/tests/PHPStan/Analyser/nsrt/narrow-cast.php index 5687c0b23a..b86b3781b2 100644 --- a/tests/PHPStan/Analyser/nsrt/narrow-cast.php +++ b/tests/PHPStan/Analyser/nsrt/narrow-cast.php @@ -7,9 +7,9 @@ /** @param array $arr */ function doFoo(string $x, array $arr): void { if ((bool) strlen($x)) { - assertType('string', $x); // could be non-empty-string + assertType('non-empty-string', $x); } else { - assertType('string', $x); + assertType("''", $x); } assertType('string', $x); diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 31570cfb12..4eb98a13a1 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1343,4 +1343,9 @@ public function testArrayFindKeyExisting(): void ]); } + public function testBug13688(): void + { + $this->analyse([__DIR__ . '/data/bug-13688.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-13688.php b/tests/PHPStan/Rules/Arrays/data/bug-13688.php new file mode 100644 index 0000000000..19a7d57a7a --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-13688.php @@ -0,0 +1,12 @@ + 0 && $input[$inputLen-1] === ':'; + echo $hasTrailingColon ? "{$input} has trailing colon\n" : "{$input} does not have trailing colon\n"; +}