Skip to content

Commit 37f3c82

Browse files
authored
Introduce array_values rule
1 parent 0b78c55 commit 37f3c82

10 files changed

+218
-2
lines changed

build/spl-autoload-functions-pre-php-7.neon

+4
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@ parameters:
44
message: "#^PHPDoc tag @var with type array\\<callable\\(\\)\\: mixed\\>\\|false is not subtype of native type list\\<callable\\(string\\)\\: void\\>\\|false\\.$#"
55
count: 2
66
path: ../src/Command/CommandHelper.php
7+
8+
-
9+
message: '#^Parameter \#1 \$array \(list<PHPStan\\Type\\Type>\) of array_values is already a list, call has no effect\.$#'
10+
path: ../src/Type/TypeCombinator.php

conf/bleedingEdge.neon

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ parameters:
77
explicitMixedViaIsArray: true
88
arrayFilter: true
99
arrayUnpacking: true
10+
arrayValues: true
1011
nodeConnectingVisitorCompatibility: false
1112
nodeConnectingVisitorRule: true
1213
disableCheckMissingIterableValueType: true

conf/config.level5.neon

+7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ parameters:
88
conditionalTags:
99
PHPStan\Rules\Functions\ArrayFilterRule:
1010
phpstan.rules.rule: %featureToggles.arrayFilter%
11+
PHPStan\Rules\Functions\ArrayValuesRule:
12+
phpstan.rules.rule: %featureToggles.arrayValues%
1113
PHPStan\Rules\Functions\CallUserFuncRule:
1214
phpstan.rules.rule: %featureToggles.callUserFunc%
1315

@@ -28,5 +30,10 @@ services:
2830
arguments:
2931
treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain%
3032

33+
-
34+
class: PHPStan\Rules\Functions\ArrayValuesRule
35+
arguments:
36+
treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain%
37+
3138
-
3239
class: PHPStan\Rules\Functions\CallUserFuncRule

conf/config.neon

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ parameters:
4141
explicitMixedViaIsArray: false
4242
arrayFilter: false
4343
arrayUnpacking: false
44+
arrayValues: false
4445
nodeConnectingVisitorCompatibility: true
4546
nodeConnectingVisitorRule: false
4647
illegalConstructorMethodCall: false

conf/parametersSchema.neon

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ parametersSchema:
3636
explicitMixedViaIsArray: bool(),
3737
arrayFilter: bool(),
3838
arrayUnpacking: bool(),
39+
arrayValues: bool(),
3940
nodeConnectingVisitorCompatibility: bool(),
4041
nodeConnectingVisitorRule: bool(),
4142
illegalConstructorMethodCall: bool(),
+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Functions;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\FuncCall;
7+
use PHPStan\Analyser\ArgumentsNormalizer;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Reflection\ParametersAcceptorSelector;
10+
use PHPStan\Reflection\ReflectionProvider;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleErrorBuilder;
13+
use PHPStan\Type\Accessory\AccessoryArrayListType;
14+
use PHPStan\Type\VerbosityLevel;
15+
use function count;
16+
use function sprintf;
17+
use function strtolower;
18+
19+
/**
20+
* @implements Rule<Node\Expr\FuncCall>
21+
*/
22+
class ArrayValuesRule implements Rule
23+
{
24+
25+
public function __construct(
26+
private readonly ReflectionProvider $reflectionProvider,
27+
private readonly bool $treatPhpDocTypesAsCertain,
28+
)
29+
{
30+
}
31+
32+
public function getNodeType(): string
33+
{
34+
return FuncCall::class;
35+
}
36+
37+
public function processNode(Node $node, Scope $scope): array
38+
{
39+
if (!($node->name instanceof Node\Name)) {
40+
return [];
41+
}
42+
43+
if (AccessoryArrayListType::isListTypeEnabled() === false) {
44+
return [];
45+
}
46+
47+
$functionName = $this->reflectionProvider->resolveFunctionName($node->name, $scope);
48+
49+
if ($functionName === null || strtolower($functionName) !== 'array_values') {
50+
return [];
51+
}
52+
53+
$functionReflection = $this->reflectionProvider->getFunction($node->name, $scope);
54+
55+
$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
56+
$scope,
57+
$node->getArgs(),
58+
$functionReflection->getVariants(),
59+
null,
60+
);
61+
62+
$normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node);
63+
64+
if ($normalizedFuncCall === null) {
65+
return [];
66+
}
67+
68+
$args = $normalizedFuncCall->getArgs();
69+
70+
if (count($args) !== 1) {
71+
return [];
72+
}
73+
74+
if ($this->treatPhpDocTypesAsCertain === true) {
75+
$arrayType = $scope->getType($args[0]->value);
76+
} else {
77+
$arrayType = $scope->getNativeType($args[0]->value);
78+
}
79+
80+
if ($arrayType->isIterableAtLeastOnce()->no()) {
81+
$message = 'Parameter #1 $array (%s) to function array_values is empty, call has no effect.';
82+
83+
return [
84+
RuleErrorBuilder::message(sprintf(
85+
$message,
86+
$arrayType->describe(VerbosityLevel::value()),
87+
))->build(),
88+
];
89+
}
90+
91+
if ($arrayType->isList()->yes()) {
92+
$message = 'Parameter #1 $array (%s) of array_values is already a list, call has no effect.';
93+
94+
return [
95+
RuleErrorBuilder::message(sprintf(
96+
$message,
97+
$arrayType->describe(VerbosityLevel::value()),
98+
))->build(),
99+
];
100+
}
101+
102+
return [];
103+
}
104+
105+
}

src/Type/Constant/ConstantArrayType.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ public function getAllArrays(): array
198198
$keys = array_merge($requiredKeys, $combination);
199199
sort($keys);
200200

201-
if ($this->isList->yes() && array_keys($keys) !== array_values($keys)) {
201+
if ($this->isList->yes() && array_keys($keys) !== $keys) {
202202
continue;
203203
}
204204

src/Type/TypeCombinator.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -1003,7 +1003,7 @@ public static function intersect(Type ...$types): Type
10031003

10041004
if ($hasOffsetValueTypeCount > 32) {
10051005
$newTypes[] = new OversizedArrayType();
1006-
$types = array_values($newTypes);
1006+
$types = $newTypes;
10071007
$typesCount = count($types);
10081008
}
10091009

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Functions;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use const PHP_VERSION_ID;
8+
9+
/**
10+
* @extends RuleTestCase<ArrayValuesRule>
11+
*/
12+
class ArrayValuesRuleTest extends RuleTestCase
13+
{
14+
15+
private bool $treatPhpDocTypesAsCertain = true;
16+
17+
protected function getRule(): Rule
18+
{
19+
return new ArrayValuesRule($this->createReflectionProvider(), $this->treatPhpDocTypesAsCertain);
20+
}
21+
22+
public function testFile(): void
23+
{
24+
$expectedErrors = [
25+
[
26+
'Parameter #1 $array (array{0, 1, 3}) of array_values is already a list, call has no effect.',
27+
8,
28+
],
29+
[
30+
'Parameter #1 $array (array{1, 3}) of array_values is already a list, call has no effect.',
31+
9,
32+
],
33+
[
34+
'Parameter #1 $array (array{\'test\'}) of array_values is already a list, call has no effect.',
35+
10,
36+
],
37+
[
38+
'Parameter #1 $array (array{\'\', \'test\'}) of array_values is already a list, call has no effect.',
39+
12,
40+
],
41+
[
42+
'Parameter #1 $array (list<int>) of array_values is already a list, call has no effect.',
43+
14,
44+
],
45+
[
46+
'Parameter #1 $array (array{0}) of array_values is already a list, call has no effect.',
47+
17,
48+
],
49+
[
50+
'Parameter #1 $array (array{null, null}) of array_values is already a list, call has no effect.',
51+
19,
52+
],
53+
[
54+
'Parameter #1 $array (array{null, 0}) of array_values is already a list, call has no effect.',
55+
20,
56+
],
57+
[
58+
'Parameter #1 $array (array{}) to function array_values is empty, call has no effect.',
59+
21,
60+
],
61+
];
62+
63+
if (PHP_VERSION_ID >= 80000) {
64+
$expectedErrors[] = [
65+
'Parameter #1 $array (list<int>) of array_values is already a list, call has no effect.',
66+
24,
67+
];
68+
}
69+
70+
$this->analyse([__DIR__ . '/data/array_values_list.php'], $expectedErrors);
71+
}
72+
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/** @var list<int> $list */
4+
$list = [1, 2, 3];
5+
/** @var list<int> $list */
6+
$array = ['a' => 1, 'b' => 2, 'c' => 3];
7+
8+
array_values([0,1,3]);
9+
array_values([1,3]);
10+
array_values(['test']);
11+
array_values(['a' => 'test']);
12+
array_values(['', 'test']);
13+
array_values(['a' => '', 'b' => 'test']);
14+
array_values($list);
15+
array_values($array);
16+
17+
array_values([0]);
18+
array_values(['a' => null, 'b' => null]);
19+
array_values([null, null]);
20+
array_values([null, 0]);
21+
array_values([]);
22+
23+
array_values(array: $array);
24+
array_values(array: $list);

0 commit comments

Comments
 (0)