Skip to content

Commit 67fbfae

Browse files
committed
Refactor IntersectionType::describe()
1 parent 96bd303 commit 67fbfae

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+601
-199
lines changed

phpstan-baseline.neon

+2-2
Original file line numberDiff line numberDiff line change
@@ -1314,7 +1314,7 @@ parameters:
13141314
-
13151315
message: '#^Doing instanceof PHPStan\\Type\\ArrayType is error\-prone and deprecated\. Use Type\:\:isArray\(\) or Type\:\:getArrays\(\) instead\.$#'
13161316
identifier: phpstanApi.instanceofType
1317-
count: 1
1317+
count: 3
13181318
path: src/Type/IntersectionType.php
13191319

13201320
-
@@ -1332,7 +1332,7 @@ parameters:
13321332
-
13331333
message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#'
13341334
identifier: phpstanApi.instanceofType
1335-
count: 1
1335+
count: 3
13361336
path: src/Type/IntersectionType.php
13371337

13381338
-

src/Type/IntersectionType.php

+111-41
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Type;
44

55
use PHPStan\Php\PhpVersion;
6+
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
67
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
78
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
89
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
@@ -45,7 +46,6 @@
4546
use function ksort;
4647
use function md5;
4748
use function sprintf;
48-
use function str_starts_with;
4949
use function strcasecmp;
5050
use function strlen;
5151
use function substr;
@@ -347,6 +347,10 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes)
347347

348348
$nonEmptyStr = false;
349349
$nonFalsyStr = false;
350+
$isList = $this->isList()->yes();
351+
$isArray = $this->isArray()->yes();
352+
$isNonEmptyArray = $this->isIterableAtLeastOnce()->yes();
353+
$describedTypes = [];
350354
foreach ($this->getSortedTypes() as $i => $type) {
351355
if ($type instanceof AccessoryNonEmptyStringType
352356
|| $type instanceof AccessoryLiteralStringType
@@ -379,10 +383,45 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes)
379383
$skipTypeNames[] = 'string';
380384
continue;
381385
}
382-
if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) {
383-
$typesToDescribe[$i] = $type;
384-
$skipTypeNames[] = 'array';
385-
continue;
386+
if ($isList || $isArray) {
387+
if ($type instanceof ArrayType) {
388+
$keyType = $type->getKeyType();
389+
$valueType = $type->getItemType();
390+
if ($isList) {
391+
$isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
392+
$valueTypeDescription = '';
393+
if (!$isMixedValueType) {
394+
$valueTypeDescription = sprintf('<%s>', $valueType->describe($level));
395+
}
396+
397+
$describedTypes[$i] = ($isNonEmptyArray ? 'non-empty-list' : 'list') . $valueTypeDescription;
398+
} else {
399+
$isMixedKeyType = $keyType instanceof MixedType && $keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$keyType->isExplicitMixed();
400+
$isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
401+
$typeDescription = '';
402+
if (!$isMixedKeyType) {
403+
$typeDescription = sprintf('<%s, %s>', $keyType->describe($level), $valueType->describe($level));
404+
} elseif (!$isMixedValueType) {
405+
$typeDescription = sprintf('<%s>', $valueType->describe($level));
406+
}
407+
408+
$describedTypes[$i] = ($isNonEmptyArray ? 'non-empty-array' : 'array') . $typeDescription;
409+
}
410+
continue;
411+
} elseif ($type instanceof ConstantArrayType) {
412+
$description = $type->describe($level);
413+
$descriptionWithoutKind = substr($description, strlen('array'));
414+
$begin = $isList ? 'list' : 'array';
415+
if ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) {
416+
$begin = 'non-empty-' . $begin;
417+
}
418+
419+
$describedTypes[$i] = $begin . $descriptionWithoutKind;
420+
continue;
421+
}
422+
if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) {
423+
continue;
424+
}
386425
}
387426

388427
if ($type instanceof CallableType && $type->isCommonCallable()) {
@@ -404,7 +443,6 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes)
404443
$typesToDescribe[$i] = $type;
405444
}
406445

407-
$describedTypes = [];
408446
foreach ($baseTypes as $i => $type) {
409447
$typeDescription = $type->describe($level);
410448

@@ -418,36 +456,6 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes)
418456
}
419457
}
420458

421-
if (
422-
str_starts_with($typeDescription, 'array<')
423-
&& in_array('array', $skipTypeNames, true)
424-
) {
425-
$nonEmpty = false;
426-
$typeName = 'array';
427-
foreach ($typesToDescribe as $j => $typeToDescribe) {
428-
if (
429-
$typeToDescribe instanceof AccessoryArrayListType
430-
&& substr($typeDescription, 0, strlen('array<int<0, max>, ')) === 'array<int<0, max>, '
431-
) {
432-
$typeName = 'list';
433-
$typeDescription = 'array<' . substr($typeDescription, strlen('array<int<0, max>, '));
434-
} elseif ($typeToDescribe instanceof NonEmptyArrayType) {
435-
$nonEmpty = true;
436-
} else {
437-
continue;
438-
}
439-
440-
unset($typesToDescribe[$j]);
441-
}
442-
443-
if ($nonEmpty) {
444-
$typeName = 'non-empty-' . $typeName;
445-
}
446-
447-
$describedTypes[$i] = $typeName . '<' . substr($typeDescription, strlen('array<'));
448-
continue;
449-
}
450-
451459
if (in_array($typeDescription, $skipTypeNames, true)) {
452460
continue;
453461
}
@@ -1139,6 +1147,10 @@ public function toPhpDocNode(): TypeNode
11391147

11401148
$nonEmptyStr = false;
11411149
$nonFalsyStr = false;
1150+
$isList = $this->isList()->yes();
1151+
$isArray = $this->isArray()->yes();
1152+
$isNonEmptyArray = $this->isIterableAtLeastOnce()->yes();
1153+
$describedTypes = [];
11421154

11431155
foreach ($this->getSortedTypes() as $i => $type) {
11441156
if ($type instanceof AccessoryNonEmptyStringType
@@ -1168,11 +1180,70 @@ public function toPhpDocNode(): TypeNode
11681180
$skipTypeNames[] = 'string';
11691181
continue;
11701182
}
1171-
if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) {
1172-
$typesToDescribe[$i] = $type;
1173-
$skipTypeNames[] = 'array';
1174-
continue;
1183+
1184+
if ($isList || $isArray) {
1185+
if ($type instanceof ArrayType) {
1186+
$keyType = $type->getKeyType();
1187+
$valueType = $type->getItemType();
1188+
if ($isList) {
1189+
$isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
1190+
$identifierTypeNode = new IdentifierTypeNode($isNonEmptyArray ? 'non-empty-list' : 'list');
1191+
if (!$isMixedValueType) {
1192+
$describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [
1193+
$valueType->toPhpDocNode(),
1194+
]);
1195+
} else {
1196+
$describedTypes[$i] = $identifierTypeNode;
1197+
}
1198+
} else {
1199+
$isMixedKeyType = $keyType instanceof MixedType && $keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$keyType->isExplicitMixed();
1200+
$isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
1201+
$identifierTypeNode = new IdentifierTypeNode($isNonEmptyArray ? 'non-empty-array' : 'array');
1202+
if (!$isMixedKeyType) {
1203+
$describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [
1204+
$keyType->toPhpDocNode(),
1205+
$valueType->toPhpDocNode(),
1206+
]);
1207+
} elseif (!$isMixedValueType) {
1208+
$describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [
1209+
$valueType->toPhpDocNode(),
1210+
]);
1211+
} else {
1212+
$describedTypes[$i] = $identifierTypeNode;
1213+
}
1214+
}
1215+
continue;
1216+
} elseif ($type instanceof ConstantArrayType) {
1217+
$constantArrayTypeNode = $type->toPhpDocNode();
1218+
if ($constantArrayTypeNode instanceof ArrayShapeNode) {
1219+
$newKind = $constantArrayTypeNode->kind;
1220+
if ($isList) {
1221+
if ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) {
1222+
$newKind = ArrayShapeNode::KIND_NON_EMPTY_LIST;
1223+
} else {
1224+
$newKind = ArrayShapeNode::KIND_LIST;
1225+
}
1226+
} elseif ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) {
1227+
$newKind = ArrayShapeNode::KIND_NON_EMPTY_ARRAY;
1228+
}
1229+
1230+
if ($newKind !== $constantArrayTypeNode->kind) {
1231+
if ($constantArrayTypeNode->sealed) {
1232+
$constantArrayTypeNode = ArrayShapeNode::createSealed($constantArrayTypeNode->items, $newKind);
1233+
} else {
1234+
$constantArrayTypeNode = ArrayShapeNode::createUnsealed($constantArrayTypeNode->items, $constantArrayTypeNode->unsealedType, $newKind);
1235+
}
1236+
}
1237+
1238+
$describedTypes[$i] = $constantArrayTypeNode;
1239+
continue;
1240+
}
1241+
}
1242+
if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) {
1243+
continue;
1244+
}
11751245
}
1246+
11761247
if (!$type instanceof AccessoryType) {
11771248
$baseTypes[$i] = $type;
11781249
continue;
@@ -1186,7 +1257,6 @@ public function toPhpDocNode(): TypeNode
11861257
$typesToDescribe[$i] = $type;
11871258
}
11881259

1189-
$describedTypes = [];
11901260
foreach ($baseTypes as $i => $type) {
11911261
$typeNode = $type->toPhpDocNode();
11921262
if ($typeNode instanceof GenericTypeNode && $typeNode->type->name === 'array') {

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -4846,7 +4846,7 @@ public function dataArrayFunctions(): array
48464846
'array_pop($stringKeys)',
48474847
],
48484848
[
4849-
'array<stdClass>&hasOffsetValue(\'baz\', stdClass)',
4849+
'non-empty-array<stdClass>&hasOffsetValue(\'baz\', stdClass)',
48504850
'$stdClassesWithIsset',
48514851
],
48524852
[
@@ -8077,7 +8077,7 @@ public function dataArrayKeysInBranches(): array
80778077
'$array',
80788078
],
80798079
[
8080-
'array&hasOffsetValue(\'key\', mixed)',
8080+
'non-empty-array&hasOffsetValue(\'key\', mixed)',
80818081
'$generalArray',
80828082
],
80838083
[
@@ -8563,11 +8563,11 @@ public function dataIsset(): array
85638563
'$mixedIsset',
85648564
],
85658565
[
8566-
'array&hasOffset(\'a\')',
8566+
'non-empty-array&hasOffset(\'a\')',
85678567
'$mixedArrayKeyExists',
85688568
],
85698569
[
8570-
'array<int>&hasOffsetValue(\'a\', int)',
8570+
'non-empty-array<int>&hasOffsetValue(\'a\', int)',
85718571
'$integers',
85728572
],
85738573
[

tests/PHPStan/Analyser/TypeSpecifierTest.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -1064,7 +1064,7 @@ public function dataCondition(): iterable
10641064
new Arg(new Variable('array')),
10651065
]),
10661066
[
1067-
'$array' => 'array&hasOffset(\'foo\')',
1067+
'$array' => 'non-empty-array&hasOffset(\'foo\')',
10681068
],
10691069
[
10701070
'$array' => '~hasOffset(\'foo\')',
@@ -1112,7 +1112,7 @@ public function dataCondition(): iterable
11121112
new Arg(new Variable('array')),
11131113
]),
11141114
[
1115-
'$array' => 'array&hasOffset(\'foo\')',
1115+
'$array' => 'non-empty-array&hasOffset(\'foo\')',
11161116
],
11171117
[
11181118
'$array' => '~hasOffset(\'foo\')',

tests/PHPStan/Analyser/data/param-out.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ function foo16() {
240240
function fooShuffle() {
241241
$array = ["foo" => 123, "bar" => 456];
242242
shuffle($array);
243-
assertType('non-empty-array<0|1, 123|456>&list', $array);
243+
assertType('non-empty-list<123|456>', $array);
244244

245245
$emptyArray = [];
246246
shuffle($emptyArray);

tests/PHPStan/Analyser/nsrt/array-chunk.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ public function chunkUnionTypeLength(array $arr, $positiveRange, $positiveUnion)
6060
* @param int<50, max> $bigger50
6161
*/
6262
public function lengthIntRanges(array $arr, int $positiveInt, int $bigger50) {
63-
assertType('list<non-empty-list<mixed>>', array_chunk($arr, $positiveInt));
64-
assertType('list<non-empty-list<mixed>>', array_chunk($arr, $bigger50));
63+
assertType('list<non-empty-list>', array_chunk($arr, $positiveInt));
64+
assertType('list<non-empty-list>', array_chunk($arr, $bigger50));
6565
}
6666

6767
/**
@@ -78,11 +78,11 @@ function testLimits(array $arr, int $oneToFour, int $tooBig) {
7878
public function offsets(array $arr, array $map): void
7979
{
8080
if (array_key_exists('foo', $arr)) {
81-
assertType('non-empty-list<non-empty-list<mixed>>', array_chunk($arr, 2));
81+
assertType('non-empty-list<non-empty-list>', array_chunk($arr, 2));
8282
assertType('non-empty-list<non-empty-array>', array_chunk($arr, 2, true));
8383
}
8484
if (array_key_exists('foo', $arr) && $arr['foo'] === 'bar') {
85-
assertType('non-empty-list<non-empty-list<mixed>>', array_chunk($arr, 2));
85+
assertType('non-empty-list<non-empty-list>', array_chunk($arr, 2));
8686
assertType('non-empty-list<non-empty-array>', array_chunk($arr, 2, true));
8787
}
8888

tests/PHPStan/Analyser/nsrt/array-column-php82.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ public function testImprecise5(array $array): void
175175
assertType('list<string>', array_column($array, 'nodeName'));
176176
assertType('array<string, string>', array_column($array, 'nodeName', 'tagName'));
177177
assertType('array<string, DOMElement>', array_column($array, null, 'tagName'));
178-
assertType('list<mixed>', array_column($array, 'foo'));
178+
assertType('list', array_column($array, 'foo'));
179179
assertType('array<string, mixed>', array_column($array, 'foo', 'tagName'));
180180
assertType('array<int|string, string>', array_column($array, 'nodeName', 'foo'));
181181
assertType('array<int|string, DOMElement>', array_column($array, null, 'foo'));
@@ -187,7 +187,7 @@ public function testObjects1(array $array): void
187187
assertType('non-empty-list<string>', array_column($array, 'nodeName'));
188188
assertType('non-empty-array<string, string>', array_column($array, 'nodeName', 'tagName'));
189189
assertType('non-empty-array<string, DOMElement>', array_column($array, null, 'tagName'));
190-
assertType('list<mixed>', array_column($array, 'foo'));
190+
assertType('list', array_column($array, 'foo'));
191191
assertType('array<string, mixed>', array_column($array, 'foo', 'tagName'));
192192
assertType('non-empty-array<int|string, string>', array_column($array, 'nodeName', 'foo'));
193193
assertType('non-empty-array<int|string, DOMElement>', array_column($array, null, 'foo'));
@@ -199,7 +199,7 @@ public function testObjects2(array $array): void
199199
assertType('array{string}', array_column($array, 'nodeName'));
200200
assertType('non-empty-array<string, string>', array_column($array, 'nodeName', 'tagName'));
201201
assertType('non-empty-array<string, DOMElement>', array_column($array, null, 'tagName'));
202-
assertType('list<mixed>', array_column($array, 'foo'));
202+
assertType('list', array_column($array, 'foo'));
203203
assertType('array<string, mixed>', array_column($array, 'foo', 'tagName'));
204204
assertType('non-empty-array<int|string, string>', array_column($array, 'nodeName', 'foo'));
205205
assertType('non-empty-array<int|string, DOMElement>', array_column($array, null, 'foo'));

tests/PHPStan/Analyser/nsrt/array-flip.php

+5-5
Original file line numberDiff line numberDiff line change
@@ -71,25 +71,25 @@ function foo8($mixed)
7171
function foo10(array $array)
7272
{
7373
if (array_key_exists('foo', $array)) {
74-
assertType('array<string, int>&hasOffset(\'foo\')', $array);
74+
assertType('non-empty-array<string, int>&hasOffset(\'foo\')', $array);
7575
assertType('array<int, string>', array_flip($array));
7676
}
7777

7878
if (array_key_exists('foo', $array) && is_int($array['foo'])) {
79-
assertType("array<string, int>&hasOffsetValue('foo', int)", $array);
79+
assertType("non-empty-array<string, int>&hasOffsetValue('foo', int)", $array);
8080
assertType('array<int, string>', array_flip($array));
8181
}
8282

8383
if (array_key_exists('foo', $array) && $array['foo'] === 17) {
84-
assertType("array<string, int>&hasOffsetValue('foo', 17)", $array);
85-
assertType("array<int, string>&hasOffsetValue(17, 'foo')", array_flip($array));
84+
assertType("non-empty-array<string, int>&hasOffsetValue('foo', 17)", $array);
85+
assertType("non-empty-array<int, string>&hasOffsetValue(17, 'foo')", array_flip($array));
8686
}
8787

8888
if (
8989
array_key_exists('foo', $array) && $array['foo'] === 17
9090
&& array_key_exists('bar', $array) && $array['bar'] === 17
9191
) {
92-
assertType("array<string, int>&hasOffsetValue('bar', 17)&hasOffsetValue('foo', 17)", $array);
92+
assertType("non-empty-array<string, int>&hasOffsetValue('bar', 17)&hasOffsetValue('foo', 17)", $array);
9393
assertType("*NEVER*", array_flip($array)); // this could be array<string, int>&hasOffsetValue(17, 'bar') according to https://3v4l.org/1TAFk
9494
}
9595
}

0 commit comments

Comments
 (0)