Skip to content

Commit

Permalink
Merge branch refs/heads/1.12.x into 2.1.x
Browse files Browse the repository at this point in the history
  • Loading branch information
phpstan-bot authored Jan 16, 2025
2 parents 5a6f45e + b9894fa commit 06507a5
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 2 deletions.
5 changes: 5 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,11 @@ services:
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Type\Php\ArrayChangeKeyCaseFunctionReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Type\Php\ArrayIntersectKeyFunctionReturnTypeExtension
tags:
Expand Down
1 change: 0 additions & 1 deletion src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ final class ArgumentBasedFunctionReturnTypeExtension implements DynamicFunctionR

private const FUNCTION_NAMES = [
'array_unique' => 0,
'array_change_key_case' => 0,
'array_diff_assoc' => 0,
'array_diff_key' => 0,
'array_diff_uassoc' => 0,
Expand Down
159 changes: 159 additions & 0 deletions src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php
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

View workflow job for this annotation

GitHub Actions / PHPStan (8.1, ubuntu-latest)

Call to an undefined static method PHPStan\Type\Accessory\AccessoryArrayListType::intersectWith().

Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.4)

Call to an undefined static method PHPStan\Type\Accessory\AccessoryArrayListType::intersectWith().

Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.3)

Call to an undefined static method PHPStan\Type\Accessory\AccessoryArrayListType::intersectWith().

Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.4, ubuntu-latest)

Call to an undefined static method PHPStan\Type\Accessory\AccessoryArrayListType::intersectWith().

Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.2, ubuntu-latest)

Call to an undefined static method PHPStan\Type\Accessory\AccessoryArrayListType::intersectWith().

Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, ubuntu-latest)

Call to an undefined static method PHPStan\Type\Accessory\AccessoryArrayListType::intersectWith().

Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.2)

Call to an undefined static method PHPStan\Type\Accessory\AccessoryArrayListType::intersectWith().

Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, ubuntu-latest)

Call to an undefined static method PHPStan\Type\Accessory\AccessoryArrayListType::intersectWith().

Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.1)

Call to an undefined static method PHPStan\Type\Accessory\AccessoryArrayListType::intersectWith().

Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.4, ubuntu-latest)

Call to an undefined static method PHPStan\Type\Accessory\AccessoryArrayListType::intersectWith().

Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1, windows-latest)

Call to an undefined static method PHPStan\Type\Accessory\AccessoryArrayListType::intersectWith().

Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, windows-latest)

Call to an undefined static method PHPStan\Type\Accessory\AccessoryArrayListType::intersectWith().

Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.4, windows-latest)

Call to an undefined static method PHPStan\Type\Accessory\AccessoryArrayListType::intersectWith().

Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.2, windows-latest)

Call to an undefined static method PHPStan\Type\Accessory\AccessoryArrayListType::intersectWith().

Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, windows-latest)

Call to an undefined static method PHPStan\Type\Accessory\AccessoryArrayListType::intersectWith().

Check failure on line 86 in src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.4, windows-latest)

Call to an undefined static method PHPStan\Type\Accessory\AccessoryArrayListType::intersectWith().
}
$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())),
);
}

}
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4577,7 +4577,7 @@ public function dataArrayFunctions(): array
'$reducedToInt',
],
[
'array<0|1|2, 1|2|3>',
'array{1, 2, 3}',
'array_change_key_case($integers)',
],
[
Expand Down
98 changes: 98 additions & 0 deletions tests/PHPStan/Analyser/nsrt/array-change-key-case.php
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));
}
}
7 changes: 7 additions & 0 deletions tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,13 @@ public function testBug10732(): void
$this->analyse([__DIR__ . '/data/bug-10732.php'], []);
}

public function testBug10960(): void
{
$this->checkExplicitMixed = true;
$this->checkNullables = true;
$this->analyse([__DIR__ . '/data/bug-10960.php'], []);
}

public function testBug11518(): void
{
$this->checkExplicitMixed = true;
Expand Down
26 changes: 26 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-10960.php
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']);

0 comments on commit 06507a5

Please sign in to comment.