Skip to content

Commit 564f79f

Browse files
committed
Fix native type of array after array_push()
Closes phpstan/phpstan#9403
1 parent 903ffc6 commit 564f79f

File tree

5 files changed

+158
-100
lines changed

5 files changed

+158
-100
lines changed

phpstan-baseline.neon

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ parameters:
5151
count: 1
5252
path: src/Analyser/MutatingScope.php
5353

54+
-
55+
message: """
56+
#^Call to deprecated method doNotTreatPhpDocTypesAsCertain\\(\\) of class PHPStan\\\\Analyser\\\\MutatingScope\\:
57+
Use getNativeType\\(\\)$#
58+
"""
59+
count: 1
60+
path: src/Analyser/NodeScopeResolver.php
61+
5462
-
5563
message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#"
5664
count: 3

src/Analyser/NodeScopeResolver.php

Lines changed: 101 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1978,99 +1978,11 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression
19781978
&& in_array($functionReflection->getName(), ['array_push', 'array_unshift'], true)
19791979
&& count($expr->getArgs()) >= 2
19801980
) {
1981-
$arrayArg = $expr->getArgs()[0]->value;
1982-
$arrayType = $scope->getType($arrayArg);
1983-
$callArgs = array_slice($expr->getArgs(), 1);
1984-
1985-
/**
1986-
* @param Arg[] $callArgs
1987-
* @param callable(?Type, Type, bool): void $setOffsetValueType
1988-
*/
1989-
$setOffsetValueTypes = static function (Scope $scope, array $callArgs, callable $setOffsetValueType, ?bool &$nonConstantArrayWasUnpacked = null): void {
1990-
foreach ($callArgs as $callArg) {
1991-
$callArgType = $scope->getType($callArg->value);
1992-
if ($callArg->unpack) {
1993-
if (count($callArgType->getConstantArrays()) === 1) {
1994-
$iterableValueTypes = $callArgType->getConstantArrays()[0]->getValueTypes();
1995-
} else {
1996-
$iterableValueTypes = [$callArgType->getIterableValueType()];
1997-
$nonConstantArrayWasUnpacked = true;
1998-
}
1999-
2000-
$isOptional = !$callArgType->isIterableAtLeastOnce()->yes();
2001-
foreach ($iterableValueTypes as $iterableValueType) {
2002-
if ($iterableValueType instanceof UnionType) {
2003-
foreach ($iterableValueType->getTypes() as $innerType) {
2004-
$setOffsetValueType(null, $innerType, $isOptional);
2005-
}
2006-
} else {
2007-
$setOffsetValueType(null, $iterableValueType, $isOptional);
2008-
}
2009-
}
2010-
continue;
2011-
}
2012-
$setOffsetValueType(null, $callArgType, false);
2013-
}
2014-
};
2015-
2016-
$constantArrays = $arrayType->getConstantArrays();
2017-
if (count($constantArrays) > 0) {
2018-
$newArrayTypes = [];
2019-
$prepend = $functionReflection->getName() === 'array_unshift';
2020-
foreach ($constantArrays as $constantArray) {
2021-
$arrayTypeBuilder = $prepend ? ConstantArrayTypeBuilder::createEmpty() : ConstantArrayTypeBuilder::createFromConstantArray($constantArray);
2022-
2023-
$setOffsetValueTypes(
2024-
$scope,
2025-
$callArgs,
2026-
static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arrayTypeBuilder): void {
2027-
$arrayTypeBuilder->setOffsetValueType($offsetType, $valueType, $optional);
2028-
},
2029-
$nonConstantArrayWasUnpacked,
2030-
);
2031-
2032-
if ($prepend) {
2033-
$keyTypes = $constantArray->getKeyTypes();
2034-
$valueTypes = $constantArray->getValueTypes();
2035-
foreach ($keyTypes as $k => $keyType) {
2036-
$arrayTypeBuilder->setOffsetValueType(
2037-
count($keyType->getConstantStrings()) === 1 ? $keyType->getConstantStrings()[0] : null,
2038-
$valueTypes[$k],
2039-
$constantArray->isOptionalKey($k),
2040-
);
2041-
}
2042-
}
1981+
$arrayType = $this->getArrayFunctionAppendingType($functionReflection, $scope, $expr);
1982+
$arrayNativeType = $this->getArrayFunctionAppendingType($functionReflection, $scope->doNotTreatPhpDocTypesAsCertain(), $expr);
20431983

2044-
$constantArray = $arrayTypeBuilder->getArray();
2045-
2046-
if ($constantArray->isConstantArray()->yes() && $nonConstantArrayWasUnpacked) {
2047-
$array = new ArrayType($constantArray->generalize(GeneralizePrecision::lessSpecific())->getIterableKeyType(), $constantArray->getIterableValueType());
2048-
$constantArray = $constantArray->isIterableAtLeastOnce()->yes()
2049-
? TypeCombinator::intersect($array, new NonEmptyArrayType())
2050-
: $array;
2051-
}
2052-
2053-
$newArrayTypes[] = $constantArray;
2054-
}
2055-
2056-
$arrayType = TypeCombinator::union(...$newArrayTypes);
2057-
} else {
2058-
$setOffsetValueTypes(
2059-
$scope,
2060-
$callArgs,
2061-
static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arrayType): void {
2062-
$isIterableAtLeastOnce = $arrayType->isIterableAtLeastOnce()->yes() || !$optional;
2063-
$arrayType = $arrayType->setOffsetValueType($offsetType, $valueType);
2064-
if ($isIterableAtLeastOnce) {
2065-
return;
2066-
}
2067-
2068-
$arrayType = new ArrayType($arrayType->getIterableKeyType(), $arrayType->getIterableValueType());
2069-
},
2070-
);
2071-
}
2072-
2073-
$scope = $scope->invalidateExpression($arrayArg)->assignExpression($arrayArg, $arrayType, $scope->getNativeType($arrayArg));
1984+
$arrayArg = $expr->getArgs()[0]->value;
1985+
$scope = $scope->invalidateExpression($arrayArg)->assignExpression($arrayArg, $arrayType, $arrayNativeType);
20741986
}
20751987

20761988
if (
@@ -2927,6 +2839,103 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void {
29272839
);
29282840
}
29292841

2842+
private function getArrayFunctionAppendingType(FunctionReflection $functionReflection, Scope $scope, FuncCall $expr): Type
2843+
{
2844+
$arrayArg = $expr->getArgs()[0]->value;
2845+
$arrayType = $scope->getType($arrayArg);
2846+
$callArgs = array_slice($expr->getArgs(), 1);
2847+
2848+
/**
2849+
* @param Arg[] $callArgs
2850+
* @param callable(?Type, Type, bool): void $setOffsetValueType
2851+
*/
2852+
$setOffsetValueTypes = static function (Scope $scope, array $callArgs, callable $setOffsetValueType, ?bool &$nonConstantArrayWasUnpacked = null): void {
2853+
foreach ($callArgs as $callArg) {
2854+
$callArgType = $scope->getType($callArg->value);
2855+
if ($callArg->unpack) {
2856+
if (count($callArgType->getConstantArrays()) === 1) {
2857+
$iterableValueTypes = $callArgType->getConstantArrays()[0]->getValueTypes();
2858+
} else {
2859+
$iterableValueTypes = [$callArgType->getIterableValueType()];
2860+
$nonConstantArrayWasUnpacked = true;
2861+
}
2862+
2863+
$isOptional = !$callArgType->isIterableAtLeastOnce()->yes();
2864+
foreach ($iterableValueTypes as $iterableValueType) {
2865+
if ($iterableValueType instanceof UnionType) {
2866+
foreach ($iterableValueType->getTypes() as $innerType) {
2867+
$setOffsetValueType(null, $innerType, $isOptional);
2868+
}
2869+
} else {
2870+
$setOffsetValueType(null, $iterableValueType, $isOptional);
2871+
}
2872+
}
2873+
continue;
2874+
}
2875+
$setOffsetValueType(null, $callArgType, false);
2876+
}
2877+
};
2878+
2879+
$constantArrays = $arrayType->getConstantArrays();
2880+
if (count($constantArrays) > 0) {
2881+
$newArrayTypes = [];
2882+
$prepend = $functionReflection->getName() === 'array_unshift';
2883+
foreach ($constantArrays as $constantArray) {
2884+
$arrayTypeBuilder = $prepend ? ConstantArrayTypeBuilder::createEmpty() : ConstantArrayTypeBuilder::createFromConstantArray($constantArray);
2885+
2886+
$setOffsetValueTypes(
2887+
$scope,
2888+
$callArgs,
2889+
static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arrayTypeBuilder): void {
2890+
$arrayTypeBuilder->setOffsetValueType($offsetType, $valueType, $optional);
2891+
},
2892+
$nonConstantArrayWasUnpacked,
2893+
);
2894+
2895+
if ($prepend) {
2896+
$keyTypes = $constantArray->getKeyTypes();
2897+
$valueTypes = $constantArray->getValueTypes();
2898+
foreach ($keyTypes as $k => $keyType) {
2899+
$arrayTypeBuilder->setOffsetValueType(
2900+
count($keyType->getConstantStrings()) === 1 ? $keyType->getConstantStrings()[0] : null,
2901+
$valueTypes[$k],
2902+
$constantArray->isOptionalKey($k),
2903+
);
2904+
}
2905+
}
2906+
2907+
$constantArray = $arrayTypeBuilder->getArray();
2908+
2909+
if ($constantArray->isConstantArray()->yes() && $nonConstantArrayWasUnpacked) {
2910+
$array = new ArrayType($constantArray->generalize(GeneralizePrecision::lessSpecific())->getIterableKeyType(), $constantArray->getIterableValueType());
2911+
$constantArray = $constantArray->isIterableAtLeastOnce()->yes()
2912+
? TypeCombinator::intersect($array, new NonEmptyArrayType())
2913+
: $array;
2914+
}
2915+
2916+
$newArrayTypes[] = $constantArray;
2917+
}
2918+
2919+
return TypeCombinator::union(...$newArrayTypes);
2920+
}
2921+
2922+
$setOffsetValueTypes(
2923+
$scope,
2924+
$callArgs,
2925+
static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arrayType): void {
2926+
$isIterableAtLeastOnce = $arrayType->isIterableAtLeastOnce()->yes() || !$optional;
2927+
$arrayType = $arrayType->setOffsetValueType($offsetType, $valueType);
2928+
if ($isIterableAtLeastOnce) {
2929+
return;
2930+
}
2931+
2932+
$arrayType = new ArrayType($arrayType->getIterableKeyType(), $arrayType->getIterableValueType());
2933+
},
2934+
);
2935+
2936+
return $arrayType;
2937+
}
2938+
29302939
private function getFunctionThrowPoint(
29312940
FunctionReflection $functionReflection,
29322941
?ParametersAcceptor $parametersAcceptor,

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,6 +1247,7 @@ public function dataFileAsserts(): iterable
12471247
yield from $this->gatherAssertTypes(__DIR__ . '/data/invalid-type-aliases.php');
12481248
yield from $this->gatherAssertTypes(__DIR__ . '/data/asymmetric-properties.php');
12491249
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9062.php');
1250+
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Variables/data/bug-9403.php');
12501251
yield from $this->gatherAssertTypes(__DIR__ . '/data/object-shape.php');
12511252
yield from $this->gatherAssertTypes(__DIR__ . '/data/memcache-get.php');
12521253
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4302b.php');

tests/PHPStan/Rules/Variables/EmptyRuleTest.php

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,6 @@ public function testBug6974(): void
9696
'Variable $a in empty() always exists and is always falsy.',
9797
12,
9898
],
99-
[
100-
'Variable $a in empty() always exists and is always falsy.',
101-
21,
102-
],
103-
[
104-
'Variable $a in empty() always exists and is always falsy.',
105-
30,
106-
],
10799
]);
108100
}
109101

@@ -221,4 +213,21 @@ public function testBug9126(): void
221213
$this->analyse([__DIR__ . '/data/bug-9126.php'], []);
222214
}
223215

216+
public function dataBug9403(): iterable
217+
{
218+
yield [true];
219+
yield [false];
220+
}
221+
222+
/**
223+
* @dataProvider dataBug9403
224+
*/
225+
public function testBug9403(bool $treatPhpDocTypesAsCertain): void
226+
{
227+
$this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain;
228+
$this->strictUnnecessaryNullsafePropertyFetch = false;
229+
230+
$this->analyse([__DIR__ . '/data/bug-9403.php'], []);
231+
}
232+
224233
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Bug9403;
4+
5+
use function PHPStan\Testing\assertNativeType;
6+
use function PHPStan\Testing\assertType;
7+
8+
class HelloWorld
9+
{
10+
11+
/**
12+
* @param int $max
13+
* @return int[]
14+
*/
15+
public function testMe(int $max): array
16+
{
17+
$result = [];
18+
for ($i = 0; $i < $max; $i++) {
19+
array_push($result, $i);
20+
}
21+
22+
assertType('list<int<0, max>>', $result);
23+
assertNativeType('list<int<0, max>>', $result);
24+
25+
if (!empty($result)) {
26+
rsort($result);
27+
}
28+
return $result;
29+
}
30+
31+
}

0 commit comments

Comments
 (0)