-
Notifications
You must be signed in to change notification settings - Fork 481
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Type\Php; | ||
|
||
use PhpParser\Node\Expr\FuncCall; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\Reflection\FunctionReflection; | ||
use PHPStan\Type\Accessory\AccessoryArrayListType; | ||
use PHPStan\Type\Accessory\AccessoryLowercaseStringType; | ||
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; | ||
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; | ||
use PHPStan\Type\Accessory\AccessoryNumericStringType; | ||
use PHPStan\Type\Accessory\AccessoryUppercaseStringType; | ||
use PHPStan\Type\Accessory\NonEmptyArrayType; | ||
use PHPStan\Type\ArrayType; | ||
use PHPStan\Type\Constant\ConstantArrayTypeBuilder; | ||
use PHPStan\Type\Constant\ConstantStringType; | ||
use PHPStan\Type\DynamicFunctionReturnTypeExtension; | ||
use PHPStan\Type\StringType; | ||
use PHPStan\Type\Type; | ||
use PHPStan\Type\TypeCombinator; | ||
use PHPStan\Type\TypeTraverser; | ||
use PHPStan\Type\TypeUtils; | ||
use PHPStan\Type\UnionType; | ||
use function array_map; | ||
use function count; | ||
use function strtolower; | ||
use function strtoupper; | ||
use const CASE_LOWER; | ||
use const CASE_UPPER; | ||
|
||
final class ArrayChangeKeyCaseFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension | ||
{ | ||
|
||
public function isFunctionSupported(FunctionReflection $functionReflection): bool | ||
{ | ||
return $functionReflection->getName() === 'array_change_key_case'; | ||
} | ||
|
||
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type | ||
{ | ||
if (!isset($functionCall->getArgs()[0])) { | ||
return null; | ||
} | ||
|
||
$arrayType = $scope->getType($functionCall->getArgs()[0]->value); | ||
if (!isset($functionCall->getArgs()[1])) { | ||
$case = CASE_LOWER; | ||
} else { | ||
$caseType = $scope->getType($functionCall->getArgs()[1]->value); | ||
$scalarValues = $caseType->getConstantScalarValues(); | ||
if (count($scalarValues) === 1) { | ||
$case = (int) $scalarValues[0]; | ||
} else { | ||
$case = null; | ||
} | ||
} | ||
|
||
$constantArrays = $arrayType->getConstantArrays(); | ||
if (count($constantArrays) > 0) { | ||
$arrayTypes = []; | ||
foreach ($constantArrays as $constantArray) { | ||
$newConstantArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); | ||
$valueTypes = $constantArray->getValueTypes(); | ||
foreach ($constantArray->getKeyTypes() as $i => $keyType) { | ||
$valueType = $valueTypes[$i]; | ||
|
||
$constantStrings = $keyType->getConstantStrings(); | ||
if (count($constantStrings) > 0) { | ||
$keyType = TypeCombinator::union( | ||
...array_map( | ||
fn (ConstantStringType $type): Type => $this->mapConstantString($type, $case), | ||
$constantStrings, | ||
), | ||
); | ||
} | ||
|
||
$newConstantArrayBuilder->setOffsetValueType( | ||
$keyType, | ||
$valueType, | ||
$constantArray->isOptionalKey($i), | ||
); | ||
} | ||
$newConstantArrayType = $newConstantArrayBuilder->getArray(); | ||
if ($constantArray->isList()->yes()) { | ||
$newConstantArrayType = AccessoryArrayListType::intersectWith($newConstantArrayType); | ||
Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php GitHub Actions / PHPStan (8.1, ubuntu-latest)
Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php GitHub Actions / PHPStan with result cache (8.4)
Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php GitHub Actions / PHPStan with result cache (8.3)
Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php GitHub Actions / PHPStan (8.4, ubuntu-latest)
Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php GitHub Actions / PHPStan (8.2, ubuntu-latest)
Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php GitHub Actions / PHPStan (8.3, ubuntu-latest)
Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php GitHub Actions / PHPStan with result cache (8.2)
Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php GitHub Actions / PHPStan (8.0, ubuntu-latest)
Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php GitHub Actions / PHPStan with result cache (8.1)
Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php GitHub Actions / PHPStan (7.4, ubuntu-latest)
Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php GitHub Actions / PHPStan (8.1, windows-latest)
Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php GitHub Actions / PHPStan (8.3, windows-latest)
Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php GitHub Actions / PHPStan (8.4, windows-latest)
Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php GitHub Actions / PHPStan (8.2, windows-latest)
Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php GitHub Actions / PHPStan (8.0, windows-latest)
|
||
} | ||
$arrayTypes[] = $newConstantArrayType; | ||
} | ||
|
||
$newArrayType = TypeCombinator::union(...$arrayTypes); | ||
} else { | ||
$keysType = $arrayType->getIterableKeyType(); | ||
|
||
$keysType = TypeTraverser::map($keysType, function (Type $type, callable $traverse) use ($case): Type { | ||
if ($type instanceof UnionType) { | ||
return $traverse($type); | ||
} | ||
|
||
$constantStrings = $type->getConstantStrings(); | ||
if (count($constantStrings) > 0) { | ||
return TypeCombinator::union( | ||
...array_map( | ||
fn (ConstantStringType $type): Type => $this->mapConstantString($type, $case), | ||
$constantStrings, | ||
), | ||
); | ||
} | ||
|
||
if ($type->isString()->yes()) { | ||
$types = [new StringType()]; | ||
if ($type->isNonFalsyString()->yes()) { | ||
$types[] = new AccessoryNonFalsyStringType(); | ||
} elseif ($type->isNonEmptyString()->yes()) { | ||
$types[] = new AccessoryNonEmptyStringType(); | ||
} | ||
if ($type->isNumericString()->yes()) { | ||
$types[] = new AccessoryNumericStringType(); | ||
} | ||
if ($case === CASE_LOWER) { | ||
$types[] = new AccessoryLowercaseStringType(); | ||
} elseif ($case === CASE_UPPER) { | ||
$types[] = new AccessoryUppercaseStringType(); | ||
} | ||
|
||
return TypeCombinator::intersect(...$types); | ||
} | ||
|
||
return $type; | ||
}); | ||
|
||
$newArrayType = TypeCombinator::intersect(new ArrayType( | ||
$keysType, | ||
$arrayType->getIterableValueType(), | ||
), ...TypeUtils::getAccessoryTypes($arrayType)); | ||
} | ||
|
||
if ($arrayType->isIterableAtLeastOnce()->yes()) { | ||
$newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType()); | ||
} | ||
|
||
return $newArrayType; | ||
} | ||
|
||
private function mapConstantString(ConstantStringType $type, ?int $case): Type | ||
{ | ||
if ($case === CASE_LOWER) { | ||
return new ConstantStringType(strtolower($type->getValue())); | ||
} elseif ($case === CASE_UPPER) { | ||
return new ConstantStringType(strtoupper($type->getValue())); | ||
} | ||
|
||
return TypeCombinator::union( | ||
new ConstantStringType(strtolower($type->getValue())), | ||
new ConstantStringType(strtoupper($type->getValue())), | ||
); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace ArrayChangeKeyCase; | ||
|
||
use function PHPStan\Testing\assertType; | ||
|
||
class HelloWorld | ||
{ | ||
/** | ||
* @param array<string> $arr1 | ||
* @param array<string, string> $arr2 | ||
* @param array<string|int, string> $arr3 | ||
* @param array<int, string> $arr4 | ||
* @param array<lowercase-string, string> $arr5 | ||
* @param array<lowercase-string&non-falsy-string, string> $arr6 | ||
* @param array<non-empty-string, string> $arr7 | ||
* @param array<literal-string, string> $arr8 | ||
* @param array{foo: 1, bar?: 2} $arr9 | ||
* @param array<'foo'|'bar', string> $arr10 | ||
* @param list<string> $list | ||
* @param non-empty-array<string> $nonEmpty | ||
*/ | ||
public function sayHello( | ||
array $arr1, | ||
array $arr2, | ||
array $arr3, | ||
array $arr4, | ||
array $arr5, | ||
array $arr6, | ||
array $arr7, | ||
array $arr8, | ||
array $arr9, | ||
array $arr10, | ||
array $list, | ||
array $nonEmpty, | ||
int $case | ||
): void { | ||
assertType('array<string>', array_change_key_case($arr1)); | ||
assertType('array<string>', array_change_key_case($arr1, CASE_LOWER)); | ||
assertType('array<string>', array_change_key_case($arr1, CASE_UPPER)); | ||
assertType('array<string>', array_change_key_case($arr1, $case)); | ||
|
||
assertType('array<lowercase-string, string>', array_change_key_case($arr2)); | ||
assertType('array<lowercase-string, string>', array_change_key_case($arr2, CASE_LOWER)); | ||
assertType('array<uppercase-string, string>', array_change_key_case($arr2, CASE_UPPER)); | ||
assertType('array<string, string>', array_change_key_case($arr2, $case)); | ||
|
||
assertType('array<int|lowercase-string, string>', array_change_key_case($arr3)); | ||
assertType('array<int|lowercase-string, string>', array_change_key_case($arr3, CASE_LOWER)); | ||
assertType('array<int|uppercase-string, string>', array_change_key_case($arr3, CASE_UPPER)); | ||
assertType('array<int|string, string>', array_change_key_case($arr3, $case)); | ||
|
||
assertType('array<int, string>', array_change_key_case($arr4)); | ||
assertType('array<int, string>', array_change_key_case($arr4, CASE_LOWER)); | ||
assertType('array<int, string>', array_change_key_case($arr4, CASE_UPPER)); | ||
assertType('array<int, string>', array_change_key_case($arr4, $case)); | ||
|
||
assertType('array<lowercase-string, string>', array_change_key_case($arr5)); | ||
assertType('array<lowercase-string, string>', array_change_key_case($arr5, CASE_LOWER)); | ||
assertType('array<uppercase-string, string>', array_change_key_case($arr5, CASE_UPPER)); | ||
assertType('array<string, string>', array_change_key_case($arr5, $case)); | ||
|
||
assertType('array<lowercase-string&non-falsy-string, string>', array_change_key_case($arr6)); | ||
assertType('array<lowercase-string&non-falsy-string, string>', array_change_key_case($arr6, CASE_LOWER)); | ||
assertType('array<non-falsy-string&uppercase-string, string>', array_change_key_case($arr6, CASE_UPPER)); | ||
assertType('array<non-falsy-string, string>', array_change_key_case($arr6, $case)); | ||
|
||
assertType('array<lowercase-string&non-empty-string, string>', array_change_key_case($arr7)); | ||
assertType('array<lowercase-string&non-empty-string, string>', array_change_key_case($arr7, CASE_LOWER)); | ||
assertType('array<non-empty-string&uppercase-string, string>', array_change_key_case($arr7, CASE_UPPER)); | ||
assertType('array<non-empty-string, string>', array_change_key_case($arr7, $case)); | ||
|
||
assertType('array<lowercase-string, string>', array_change_key_case($arr8)); | ||
assertType('array<lowercase-string, string>', array_change_key_case($arr8, CASE_LOWER)); | ||
assertType('array<uppercase-string, string>', array_change_key_case($arr8, CASE_UPPER)); | ||
assertType('array<string, string>', array_change_key_case($arr8, $case)); | ||
|
||
assertType('array{foo: 1, bar?: 2}', array_change_key_case($arr9)); | ||
assertType('array{foo: 1, bar?: 2}', array_change_key_case($arr9, CASE_LOWER)); | ||
assertType('array{FOO: 1, BAR?: 2}', array_change_key_case($arr9, CASE_UPPER)); | ||
assertType("non-empty-array<'BAR'|'bar'|'FOO'|'foo', 1|2>", array_change_key_case($arr9, $case)); | ||
|
||
assertType("array<'bar'|'foo', string>", array_change_key_case($arr10)); | ||
assertType("array<'bar'|'foo', string>", array_change_key_case($arr10, CASE_LOWER)); | ||
assertType("array<'BAR'|'FOO', string>", array_change_key_case($arr10, CASE_UPPER)); | ||
assertType("array<'BAR'|'bar'|'FOO'|'foo', string>", array_change_key_case($arr10, $case)); | ||
|
||
assertType('list<string>', array_change_key_case($list)); | ||
assertType('list<string>', array_change_key_case($list, CASE_LOWER)); | ||
assertType('list<string>', array_change_key_case($list, CASE_UPPER)); | ||
assertType('list<string>', array_change_key_case($list, $case)); | ||
|
||
assertType('non-empty-array<string>', array_change_key_case($nonEmpty)); | ||
assertType('non-empty-array<string>', array_change_key_case($nonEmpty, CASE_LOWER)); | ||
assertType('non-empty-array<string>', array_change_key_case($nonEmpty, CASE_UPPER)); | ||
assertType('non-empty-array<string>', array_change_key_case($nonEmpty, $case)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace Bug10960; | ||
|
||
/** | ||
* @param array{foo: string} $foo | ||
* | ||
* @return array{FOO: string} | ||
*/ | ||
function upperCaseKey(array $foo): array | ||
{ | ||
return array_change_key_case($foo, CASE_UPPER); | ||
} | ||
|
||
/** | ||
* @param array{FOO: string} $foo | ||
* | ||
* @return array{foo: string} | ||
*/ | ||
function lowerCaseKey(array $foo): array | ||
{ | ||
return array_change_key_case($foo, CASE_LOWER); | ||
} | ||
|
||
upperCaseKey(['foo' => 'bar']); | ||
lowerCaseKey(['FOO' => 'bar']); |