Skip to content

Commit 5afb019

Browse files
committed
LiteralArrayHandler
1 parent 80b8f87 commit 5afb019

File tree

2 files changed

+152
-0
lines changed

2 files changed

+152
-0
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser\Generator\ExprHandler;
4+
5+
use Generator;
6+
use PhpParser\Node\Expr;
7+
use PhpParser\Node\Expr\Array_;
8+
use PhpParser\Node\Stmt;
9+
use PHPStan\Analyser\ExpressionContext;
10+
use PHPStan\Analyser\Generator\ExprAnalysisRequest;
11+
use PHPStan\Analyser\Generator\ExprAnalysisResult;
12+
use PHPStan\Analyser\Generator\ExprAnalysisResultStorage;
13+
use PHPStan\Analyser\Generator\ExprHandler;
14+
use PHPStan\Analyser\Generator\GeneratorScope;
15+
use PHPStan\Analyser\Generator\NodeCallbackRequest;
16+
use PHPStan\Analyser\SpecifiedTypes;
17+
use PHPStan\DependencyInjection\AutowiredService;
18+
use PHPStan\Php\PhpVersion;
19+
use PHPStan\Type\Accessory\AccessoryArrayListType;
20+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
21+
use PHPStan\Type\IntegerType;
22+
use PHPStan\Type\Type;
23+
use PHPStan\Type\TypeCombinator;
24+
use function array_merge;
25+
use function count;
26+
27+
/**
28+
* @implements ExprHandler<Array_>
29+
*/
30+
#[AutowiredService]
31+
final class LiteralArrayHandler implements ExprHandler
32+
{
33+
34+
public function __construct(private PhpVersion $phpVersion)
35+
{
36+
}
37+
38+
public function supports(Expr $expr): bool
39+
{
40+
return $expr instanceof Array_;
41+
}
42+
43+
public function analyseExpr(Stmt $stmt, Expr $expr, GeneratorScope $scope, ExprAnalysisResultStorage $storage, ExpressionContext $context): Generator
44+
{
45+
// todo oversizedArrayBuilder
46+
$arrayBuilder = ConstantArrayTypeBuilder::createEmpty();
47+
$nativeArrayBuilder = ConstantArrayTypeBuilder::createEmpty();
48+
$isList = null;
49+
50+
$hasYield = false;
51+
$throwPoints = [];
52+
$impurePoints = [];
53+
$isAlwaysTerminating = false;
54+
55+
foreach ($expr->items as $arrayItem) {
56+
yield new NodeCallbackRequest($arrayItem, $scope);
57+
$keyResult = null;
58+
if ($arrayItem->key !== null) {
59+
$keyResult = yield new ExprAnalysisRequest($stmt, $arrayItem->key, $scope, $context->enterDeep());
60+
$hasYield = $hasYield || $keyResult->hasYield;
61+
$throwPoints = array_merge($throwPoints, $keyResult->throwPoints);
62+
$impurePoints = array_merge($impurePoints, $keyResult->impurePoints);
63+
$isAlwaysTerminating = $isAlwaysTerminating || $keyResult->isAlwaysTerminating;
64+
$scope = $keyResult->scope;
65+
}
66+
67+
$valueResult = yield new ExprAnalysisRequest($stmt, $arrayItem->value, $scope, $context->enterDeep());
68+
$hasYield = $hasYield || $valueResult->hasYield;
69+
$throwPoints = array_merge($throwPoints, $valueResult->throwPoints);
70+
$impurePoints = array_merge($impurePoints, $valueResult->impurePoints);
71+
$isAlwaysTerminating = $isAlwaysTerminating || $valueResult->isAlwaysTerminating;
72+
$scope = $valueResult->scope;
73+
74+
if ($arrayItem->unpack) {
75+
$this->processUnpackedConstantArray($arrayBuilder, $valueResult->type, $isList);
76+
$this->processUnpackedConstantArray($nativeArrayBuilder, $valueResult->nativeType, $isList);
77+
} else {
78+
$arrayBuilder->setOffsetValueType(
79+
$keyResult !== null ? $keyResult->type : null,
80+
$valueResult->type,
81+
);
82+
$nativeArrayBuilder->setOffsetValueType(
83+
$keyResult !== null ? $keyResult->nativeType : null,
84+
$valueResult->nativeType,
85+
);
86+
}
87+
}
88+
89+
$arrayType = $arrayBuilder->getArray();
90+
$nativeArrayType = $nativeArrayBuilder->getArray();
91+
92+
if ($isList === true) {
93+
$arrayType = TypeCombinator::intersect($arrayType, new AccessoryArrayListType());
94+
$nativeArrayType = TypeCombinator::intersect($nativeArrayType, new AccessoryArrayListType());
95+
}
96+
97+
return new ExprAnalysisResult(
98+
$arrayType,
99+
$nativeArrayType,
100+
$scope,
101+
hasYield: $hasYield,
102+
isAlwaysTerminating: $isAlwaysTerminating,
103+
throwPoints: $throwPoints,
104+
impurePoints: $impurePoints,
105+
specifiedTruthyTypes: new SpecifiedTypes(),
106+
specifiedFalseyTypes: new SpecifiedTypes(),
107+
);
108+
}
109+
110+
private function processUnpackedConstantArray(ConstantArrayTypeBuilder $arrayBuilder, Type $valueType, ?bool &$isList): void
111+
{
112+
$constantArrays = $valueType->getConstantArrays();
113+
if (count($constantArrays) === 1) {
114+
$constantArrayType = $constantArrays[0];
115+
$hasStringKey = false;
116+
foreach ($constantArrayType->getKeyTypes() as $keyType) {
117+
if ($keyType->isString()->yes()) {
118+
$hasStringKey = true;
119+
break;
120+
}
121+
}
122+
123+
foreach ($constantArrayType->getValueTypes() as $i => $innerValueType) {
124+
if ($hasStringKey && $this->phpVersion->supportsArrayUnpackingWithStringKeys()) {
125+
$arrayBuilder->setOffsetValueType($constantArrayType->getKeyTypes()[$i], $innerValueType, $constantArrayType->isOptionalKey($i));
126+
} else {
127+
$arrayBuilder->setOffsetValueType(null, $innerValueType, $constantArrayType->isOptionalKey($i));
128+
}
129+
}
130+
} else {
131+
$arrayBuilder->degradeToGeneralArray();
132+
133+
if ($this->phpVersion->supportsArrayUnpackingWithStringKeys() && !$valueType->getIterableKeyType()->isString()->no()) {
134+
$isList = false;
135+
$offsetType = $valueType->getIterableKeyType();
136+
} else {
137+
$isList ??= $arrayBuilder->isList();
138+
$offsetType = new IntegerType();
139+
}
140+
141+
$arrayBuilder->setOffsetValueType($offsetType, $valueType->getIterableValueType(), !$valueType->isIterableAtLeastOnce()->yes());
142+
}
143+
}
144+
145+
}

tests/PHPStan/Analyser/Generator/data/gnsr.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,10 @@ function (): void {
2020
assertType('string|null', $foo->doFoo());
2121
assertType($a = '1', (int) $a);
2222
};
23+
24+
function (): void {
25+
assertType('array{foo: \'bar\'}', ['foo' => 'bar']);
26+
$a = [];
27+
assertType('array{}', $a);
28+
29+
};

0 commit comments

Comments
 (0)