diff --git a/src/Rules/ClassCaseSensitivityCheck.php b/src/Rules/ClassCaseSensitivityCheck.php index 27cfdb52dbc..240ee04d8a3 100644 --- a/src/Rules/ClassCaseSensitivityCheck.php +++ b/src/Rules/ClassCaseSensitivityCheck.php @@ -8,9 +8,10 @@ use PHPStan\Parser\UseAliasVisitor; use PHPStan\Reflection\ReflectionProvider; use function count; -use function implode; use function sprintf; +use function strrpos; use function strtolower; +use function substr; #[AutowiredService] final class ClassCaseSensitivityCheck @@ -105,15 +106,18 @@ public static function getNameAsWritten(Name $node): string return $resolvedName; } - $originalParts = $originalName->getParts(); - if (count($originalParts) !== 1) { + if (count($originalName->getParts()) !== 1) { return $resolvedName; } - $resolvedParts = $node->getParts(); - $resolvedParts[count($resolvedParts) - 1] = $originalParts[0]; + // the original name takes over the last part of the resolved name, + // everything up to the last separator stays as resolved + $lastSeparatorPos = strrpos($resolvedName, '\\'); + if ($lastSeparatorPos === false) { + return $originalName->toString(); + } - return implode('\\', $resolvedParts); + return substr($resolvedName, 0, $lastSeparatorPos + 1) . $originalName->toString(); } } diff --git a/src/Rules/FunctionDefinitionCheck.php b/src/Rules/FunctionDefinitionCheck.php index c1666ebdacd..1e7f68cb5c4 100644 --- a/src/Rules/FunctionDefinitionCheck.php +++ b/src/Rules/FunctionDefinitionCheck.php @@ -863,18 +863,17 @@ private function getOriginalClassNamePairsFromTypeNode(Identifier|Name|ComplexTy return []; } - $originalParts = $originalName->getParts(); + $originalPartsCount = count($originalName->getParts()); $resolvedParts = $typeNode->getParts(); - - $originalPartsCount = count($originalParts); $resolvedPartsCount = count($resolvedParts); - if ($originalPartsCount <= $resolvedPartsCount) { - $prefixParts = array_slice($resolvedParts, 0, $resolvedPartsCount - $originalPartsCount); - $originalCaseClassName = implode('\\', array_merge($prefixParts, $originalParts)); - } else { - $originalCaseClassName = $originalName->toString(); - } + // the original name takes over the last parts of the resolved name, + // the parts it does not cover stay as resolved + $prefixParts = $originalPartsCount <= $resolvedPartsCount + ? array_slice($resolvedParts, 0, $resolvedPartsCount - $originalPartsCount) + : []; + $prefix = $prefixParts === [] ? '' : implode('\\', $prefixParts) . '\\'; + $originalCaseClassName = $prefix . $originalName->toString(); if ($originalCaseClassName === $resolvedName) { return []; diff --git a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php index 34f4080a9f5..da376bc3429 100644 --- a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php +++ b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php @@ -15,6 +15,7 @@ use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; @@ -85,9 +86,14 @@ private function implode(Type $arrayType, Type $separatorType): Type $accessoryTypes = []; $valueTypeAsString = $arrayType->getIterableValueType()->toString(); if ($arrayType->isIterableAtLeastOnce()->yes()) { - if ($valueTypeAsString->isNonFalsyString()->yes() || $separatorType->isNonFalsyString()->yes()) { + // The separator only appears between elements, so it can only + // guarantee a non-empty/non-falsy result when the array has at + // least two elements. A single-element array drops the separator + // entirely (e.g. implode(',', ['']) === ''). + $separatorAppears = IntegerRangeType::createAllGreaterThanOrEqualTo(2)->isSuperTypeOf($arrayType->getArraySize())->yes(); + if ($valueTypeAsString->isNonFalsyString()->yes() || ($separatorAppears && $separatorType->isNonFalsyString()->yes())) { $accessoryTypes[] = new AccessoryNonFalsyStringType(); - } elseif ($valueTypeAsString->isNonEmptyString()->yes() || $separatorType->isNonEmptyString()->yes()) { + } elseif ($valueTypeAsString->isNonEmptyString()->yes() || ($separatorAppears && $separatorType->isNonEmptyString()->yes())) { $accessoryTypes[] = new AccessoryNonEmptyStringType(); } } diff --git a/tests/PHPStan/Analyser/nsrt/implode.php b/tests/PHPStan/Analyser/nsrt/implode.php index ecd36a0570f..da52efbe56d 100644 --- a/tests/PHPStan/Analyser/nsrt/implode.php +++ b/tests/PHPStan/Analyser/nsrt/implode.php @@ -78,6 +78,49 @@ public function unsealedConstArr($unsealed) { assertType('non-falsy-string', implode(',', $unsealed)); } + /** + * @param non-empty-array $arr + * @param non-empty-list $list + */ + public function nonEmptyArrayOfStrings(array $arr, array $list) { + // A non-empty array can have a single element, which drops the + // separator entirely (implode(',', ['']) === ''), so the non-falsy + // separator must not make the whole result non-falsy. + assertType('string', implode(',', $arr)); + assertType('string', implode('', $arr)); + assertType('string', implode(',', $list)); + } + + /** + * @param non-empty-array $arr + */ + public function nonEmptyArrayOfNonEmptyStrings(array $arr) { + assertType('non-empty-string', implode(',', $arr)); + assertType('non-empty-string', implode('', $arr)); + } + + /** + * @param array{string, string} $arr + * @param array{string, string, string} $arr3 + */ + public function atLeastTwoElements(array $arr, array $arr3) { + // At least two elements guarantees the separator appears. + assertType('non-falsy-string', implode(',', $arr)); + assertType('non-falsy-string', implode(',', $arr3)); + assertType('string', implode('', $arr)); + } + + public function bug5521() { + // https://github.com/phpstan/phpstan/issues/5521 + // A single-element array drops the separator entirely, so the result + // is just the (empty) single value, not a non-empty-string. + $foo = [null]; + assertType("''", implode(',', $foo)); + + $bar = ['']; + assertType("''", implode(',', $bar)); + } + /** @param array{'a', 'b', ...} $unsealed */ public function unsealedConstArrEmptySeparator($unsealed) { // Empty separator + a possibly-empty unsealed value type leaves no diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string.php b/tests/PHPStan/Analyser/nsrt/non-empty-string.php index c8031310aee..02e9c7f22ad 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string.php @@ -182,7 +182,7 @@ public function doFoo2(string $s, array $nonEmptyArrayWithStrings): void { assertType('string', implode($s, $nonEmptyArrayWithStrings)); assertType('string', implode('', $nonEmptyArrayWithStrings)); - assertType('non-falsy-string', implode(' ', $nonEmptyArrayWithStrings)); + assertType('string', implode(' ', $nonEmptyArrayWithStrings)); assertType('string', implode($nonEmptyArrayWithStrings)); } @@ -204,7 +204,7 @@ public function doFoo4(string $s, array $nonEmptyArrayWithNonEmptyStrings): void { assertType('non-empty-string', implode($s, $nonEmptyArrayWithNonEmptyStrings)); assertType('non-empty-string', implode('', $nonEmptyArrayWithNonEmptyStrings)); - assertType('non-falsy-string', implode(' ', $nonEmptyArrayWithNonEmptyStrings)); + assertType('non-empty-string', implode(' ', $nonEmptyArrayWithNonEmptyStrings)); assertType('non-empty-string', implode($nonEmptyArrayWithNonEmptyStrings)); } @@ -227,7 +227,9 @@ public function nonE($glue, array $a) return "xyz"; } - assertType('non-empty-string', implode($glue, $a)); + // A single-element array drops the glue entirely, so the result can + // still be an empty/falsy string even with a non-empty glue. + assertType('string', implode($glue, $a)); } public function sayHello2(int $i): void @@ -248,7 +250,7 @@ public function nonE2($glue, array $a) return "xyz"; } - assertType('non-empty-string', join($glue, $a)); + assertType('string', join($glue, $a)); } } diff --git a/tests/PHPStan/Analyser/nsrt/non-falsy-string.php b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php index 12ca4c3fac4..1e4d87d89e5 100644 --- a/tests/PHPStan/Analyser/nsrt/non-falsy-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php @@ -78,7 +78,7 @@ function concat(string $s, string $nonFalsey, $numericS, $nonEmpty, $literalStri function stringFunctions(string $s, $nonFalsey, $arrayOfNonFalsey, $nonEmptyArray, array $arr) { assertType('string', implode($nonFalsey, [])); - assertType('non-falsy-string', implode($nonFalsey, $nonEmptyArray)); + assertType('string', implode($nonFalsey, $nonEmptyArray)); assertType('non-falsy-string', implode($nonFalsey, $arrayOfNonFalsey)); assertType('non-falsy-string', implode($s, $arrayOfNonFalsey));