diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 6221164a741..56a5ce36d37 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -242,6 +242,27 @@ public function specifyTypesInCondition( } } } + + if ( + $exprNode instanceof FuncCall + && $exprNode->name instanceof Name + && strtolower($exprNode->name->toString()) === 'substr' + && isset($exprNode->getArgs()[0]) + && $constantType instanceof ConstantStringType + && $constantType->getValue() !== '' + ) { + $argType = $scope->getType($exprNode->getArgs()[0]->value); + + if ($argType->isString()->yes() && !$argType->isNonEmptyString()->yes()) { + return $this->create( + $exprNode->getArgs()[0]->value, + TypeCombinator::intersect($argType, new AccessoryNonEmptyStringType()), + $context, + false, + $scope, + ); + } + } } $rightType = $scope->getType($expr->right); diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index b5f4d49854d..06795798a9c 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -911,6 +911,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/constant-array-type-identical.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-string-str-containing-fns.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6609.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-string-substr-specifying.php'); } /** diff --git a/tests/PHPStan/Analyser/data/non-empty-string-substr-specifying.php b/tests/PHPStan/Analyser/data/non-empty-string-substr-specifying.php new file mode 100644 index 00000000000..d6bf60cb279 --- /dev/null +++ b/tests/PHPStan/Analyser/data/non-empty-string-substr-specifying.php @@ -0,0 +1,63 @@ +