diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index c0c03073e0..d4333827d5 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -45,6 +45,7 @@ use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\BooleanType; @@ -1045,6 +1046,13 @@ private function resolveType(Expr $node): Type return $leftStringType->append($rightStringType); } + if ($leftStringType->isNonEmptyString()->or($rightStringType->isNonEmptyString())->yes()) { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + return new StringType(); } @@ -1316,22 +1324,40 @@ private function resolveType(Expr $node): Type } elseif ($node instanceof String_) { return new ConstantStringType($node->value); } elseif ($node instanceof Node\Scalar\Encapsed) { - $constantString = new ConstantStringType(''); + $parts = []; foreach ($node->parts as $part) { if ($part instanceof EncapsedStringPart) { - $partStringType = new ConstantStringType($part->value); - } else { - $partStringType = $this->getType($part)->toString(); - if ($partStringType instanceof ErrorType) { - return new ErrorType(); - } - if (!$partStringType instanceof ConstantStringType) { - return new StringType(); + $parts[] = new ConstantStringType($part->value); + continue; + } + + $partStringType = $this->getType($part)->toString(); + if ($partStringType instanceof ErrorType) { + return new ErrorType(); + } + + $parts[] = $partStringType; + } + + $constantString = new ConstantStringType(''); + foreach ($parts as $part) { + if ($part instanceof ConstantStringType) { + $constantString = $constantString->append($part); + continue; + } + + foreach ($parts as $partType) { + if ($partType->isNonEmptyString()->yes()) { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); } } - $constantString = $constantString->append($partStringType); + return new StringType(); } + return $constantString; } elseif ($node instanceof DNumber) { return new ConstantFloatType($node->value); diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 17f98104ae..3744a661ac 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -2496,11 +2496,11 @@ public function dataBinaryOperations(): array 'min(1, 2.2, 3.3)', ], [ - 'string', + 'non-empty-string', '"Hello $world"', ], [ - 'string', + 'non-empty-string', '$string .= "str"', ], [ @@ -3072,15 +3072,15 @@ public function dataBinaryOperations(): array '$decrementedFooString', ], [ - 'string', + 'non-empty-string', '$conditionalString . $conditionalString', ], [ - 'string', + 'non-empty-string', '$conditionalString . $anotherConditionalString', ], [ - 'string', + 'non-empty-string', '$anotherConditionalString . $conditionalString', ], [ diff --git a/tests/PHPStan/Analyser/data/non-empty-string.php b/tests/PHPStan/Analyser/data/non-empty-string.php index 2191ce4149..2a441112e3 100644 --- a/tests/PHPStan/Analyser/data/non-empty-string.php +++ b/tests/PHPStan/Analyser/data/non-empty-string.php @@ -219,3 +219,22 @@ public function nonE2($glue, array $a) { } } + +class LiteralString +{ + + function x(string $tableName, string $original): void { + assertType('non-empty-string', "from `$tableName`"); + } + + /** + * @param non-empty-string $nonEmpty + */ + function concat(string $s, string $nonEmpty): void + { + assertType('string', $s . ''); + assertType('non-empty-string', $nonEmpty . ''); + assertType('non-empty-string', $nonEmpty . $s); + } + +} diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index c09852b378..ae41bc2f3a 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -563,11 +563,11 @@ public function testArrayReduceCallback(): void 5, ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): string given.', + 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, int): non-empty-string|null, Closure(string, int): non-empty-string given.', 13, ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): string given.', + 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, int): non-empty-string|null, Closure(string, int): non-empty-string given.', 22, ], ]); @@ -584,11 +584,11 @@ public function testArrayReduceArrowFunctionCallback(): void 5, ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): string given.', + 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, int): non-empty-string|null, Closure(string, int): non-empty-string given.', 11, ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): string given.', + 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, int): non-empty-string|null, Closure(string, int): non-empty-string given.', 18, ], ]);