Skip to content

Commit

Permalink
Support multi-byte string function variants
Browse files Browse the repository at this point in the history
  • Loading branch information
staabm committed May 9, 2024
1 parent 173587f commit 80b46e3
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 11 deletions.
6 changes: 5 additions & 1 deletion src/Analyser/TypeSpecifier.php
Expand Up @@ -1016,7 +1016,11 @@ private function specifyTypesForConstantStringBinaryExpression(
$context->truthy()
&& $exprNode instanceof FuncCall
&& $exprNode->name instanceof Name
&& in_array(strtolower($exprNode->name->toString()), ['substr', 'strstr', 'stristr', 'strchr', 'strrchr', 'strtolower', 'strtoupper', 'mb_strtolower', 'mb_strtoupper', 'ucfirst', 'lcfirst', 'ucwords', 'mb_convert_case', 'mb_convert_kana'], true)
&& in_array(strtolower($exprNode->name->toString()), [
'substr', 'strstr', 'stristr', 'strchr', 'strrchr', 'strtolower', 'strtoupper',
'mb_substr', 'mb_strstr', 'mb_stristr', 'mb_strchr', 'mb_strrchr', 'mb_strtolower', 'mb_strtoupper',
'ucfirst', 'lcfirst', 'ucwords', 'mb_convert_case', 'mb_convert_kana',
], true)
&& isset($exprNode->getArgs()[0])
&& $constantType->getValue() !== ''
) {
Expand Down
5 changes: 5 additions & 0 deletions src/Type/Php/StrContainingTypeSpecifyingExtension.php
Expand Up @@ -38,6 +38,11 @@ final class StrContainingTypeSpecifyingExtension implements FunctionTypeSpecifyi
'stripos' => [0, 1],
'strripos' => [0, 1],
'strstr' => [0, 1],
'mb_strpos' => [0, 1],
'mb_strrpos' => [0, 1],
'mb_stripos' => [0, 1],
'mb_strripos' => [0, 1],
'mb_strstr' => [0, 1],
];

private TypeSpecifier $typeSpecifier;
Expand Down
23 changes: 13 additions & 10 deletions src/Type/Php/SubstrDynamicReturnTypeExtension.php
Expand Up @@ -17,15 +17,17 @@
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;
use function in_array;
use function is_bool;
use function mb_substr;
use function substr;

class SubstrDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'substr';
return in_array($functionReflection->getName(), ['substr', 'mb_substr'], true);
}

public function getTypeFromFunctionCall(
Expand Down Expand Up @@ -62,16 +64,17 @@ public function getTypeFromFunctionCall(
$results = [];
foreach ($constantStrings as $constantString) {
if ($length !== null) {
$substr = substr(
$constantString->getValue(),
$offset->getValue(),
$length->getValue(),
);
if ($functionReflection->getName() === 'mb_substr') {
$substr = mb_substr($constantString->getValue(), $offset->getValue(), $length->getValue());
} else {
$substr = substr($constantString->getValue(), $offset->getValue(), $length->getValue());
}
} else {
$substr = substr(
$constantString->getValue(),
$offset->getValue(),
);
if ($functionReflection->getName() === 'mb_substr') {
$substr = mb_substr($constantString->getValue(), $offset->getValue());
} else {
$substr = substr($constantString->getValue(), $offset->getValue());
}
}

if (is_bool($substr)) {
Expand Down
Expand Up @@ -131,6 +131,58 @@ public function variants(string $s) {
assertType('non-falsy-string', $s);
}
assertType('string', $s);

if (mb_strpos($s, ':') !== false) {
assertType('non-falsy-string', $s);
}
assertType('string', $s);
if (mb_strpos($s, ':') === false) {
assertType('string', $s);
}
assertType('string', $s);

if (mb_strpos($s, ':') === 5) {
assertType('string', $s); // could be non-empty-string
}
assertType('string', $s);
if (mb_strpos($s, ':') !== 5) {
assertType('string', $s);
}
assertType('string', $s);

if (mb_strrpos($s, ':') !== false) {
assertType('non-falsy-string', $s);
}
assertType('string', $s);

if (mb_stripos($s, ':') !== false) {
assertType('non-falsy-string', $s);
}
assertType('string', $s);

if (mb_strripos($s, ':') !== false) {
assertType('non-falsy-string', $s);
}
assertType('string', $s);

if (mb_strstr($s, ':') === 'hallo') {
assertType('non-falsy-string', $s);
}
assertType('string', $s);
if (mb_strstr($s, ':', true) === 'hallo') {
assertType('non-falsy-string', $s);
}
assertType('string', $s);
if (mb_strstr($s, ':', true) !== false) {
assertType('non-falsy-string', $s);
}
assertType('string', $s);
if (mb_strstr($s, ':', true) === false) {
assertType('string', $s);
} else {
assertType('non-falsy-string', $s);
}
assertType('string', $s);
}

}
12 changes: 12 additions & 0 deletions tests/PHPStan/Analyser/data/non-empty-string-strstr-specifying.php
Expand Up @@ -39,6 +39,10 @@ public function nonEmptyStrstr(string $s, string $needle, bool $before_needle):
assertType('non-falsy-string', $s);
}
assertType('string', $s);
if (mb_strstr($s, $needle, $before_needle) === 'hallo') {
assertType('non-falsy-string', $s);
}
assertType('string', $s);

if (strstr($s, $needle, $before_needle) !== 'hallo') {
assertType('string', $s);
Expand Down Expand Up @@ -81,6 +85,10 @@ public function nonEmptyStristr(string $s, string $needle, bool $before_needle):
if ('hallo' === stristr($s, 'abc')) {
assertType('non-falsy-string', $s);
}
assertType('string', $s);
if ('hallo' === mb_stristr($s, 'abc')) {
assertType('non-falsy-string', $s);
}

if (stristr($s, $needle, $before_needle) == '') {
assertType('string', $s);
Expand All @@ -107,6 +115,10 @@ public function nonEmptyStrchr(string $s, string $needle, bool $before_needle):
if ('hallo' === strchr($s, 'abc')) {
assertType('non-falsy-string', $s);
}
assertType('string', $s);
if ('hallo' === mb_strchr($s, 'abc')) {
assertType('non-falsy-string', $s);
}

if (strchr($s, $needle, $before_needle) == '') {
assertType('string', $s);
Expand Down
28 changes: 28 additions & 0 deletions tests/PHPStan/Analyser/data/non-empty-string-substr.php
Expand Up @@ -31,4 +31,32 @@ public function doSubstr(string $s, $nonEmpty, $positiveInt, $postiveRange, $neg
assertType('non-empty-string', substr($nonEmpty, 0, $positiveInt));
}

/**
* @param non-empty-string $nonEmpty
* @param positive-int $positiveInt
* @param 1|2|3 $postiveRange
* @param -1|-2|-3 $negativeRange
*/
public function doMbSubstr(string $s, $nonEmpty, $positiveInt, $postiveRange, $negativeRange): void
{
assertType('string', mb_substr($s, 5));

assertType('string', mb_substr($s, -5));
assertType('non-empty-string', mb_substr($nonEmpty, -5));
assertType('non-empty-string', mb_substr($nonEmpty, $negativeRange));

assertType('string', mb_substr($s, 0, 5));
assertType('non-empty-string', mb_substr($nonEmpty, 0, 5));
assertType('non-empty-string', mb_substr($nonEmpty, 0, $postiveRange));

assertType('string', mb_substr($nonEmpty, 0, -5));

assertType('string', mb_substr($s, 0, $positiveInt));
assertType('non-empty-string', mb_substr($nonEmpty, 0, $positiveInt));

assertType('non-falsy-string', mb_substr("déjà_vu", 0, $positiveInt));
assertType("'déjà_vu'", mb_substr("déjà_vu", 0));
assertType("'déj'", mb_substr("déjà_vu", 0, 3));
}

}

0 comments on commit 80b46e3

Please sign in to comment.