Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions src/Rules/ClassCaseSensitivityCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
}

}
17 changes: 8 additions & 9 deletions src/Rules/FunctionDefinitionCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 [];
Expand Down
10 changes: 8 additions & 2 deletions src/Type/Php/ImplodeFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
}
Expand Down
43 changes: 43 additions & 0 deletions tests/PHPStan/Analyser/nsrt/implode.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,49 @@ public function unsealedConstArr($unsealed) {
assertType('non-falsy-string', implode(',', $unsealed));
}

/**
* @param non-empty-array<string> $arr
* @param non-empty-list<string> $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<non-empty-string> $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', ...<int, string>} $unsealed */
public function unsealedConstArrEmptySeparator($unsealed) {
// Empty separator + a possibly-empty unsealed value type leaves no
Expand Down
10 changes: 6 additions & 4 deletions tests/PHPStan/Analyser/nsrt/non-empty-string.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand All @@ -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));
}

Expand All @@ -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
Expand All @@ -248,7 +250,7 @@ public function nonE2($glue, array $a)
return "xyz";
}

assertType('non-empty-string', join($glue, $a));
assertType('string', join($glue, $a));
}

}
Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/non-falsy-string.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down
Loading